├── doc └── demo.png ├── config.json.example ├── go.mod ├── .vscode └── launch.json ├── internal └── botconfig │ └── botconfig.go ├── LICENSE ├── .gitignore ├── pkg ├── dispatcher │ └── dispatcher.go └── larkee │ ├── message.go │ └── client.go ├── main.go ├── README.md ├── go.sum └── commands.go /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrchi/lark-dalle3-bot/HEAD/doc/demo.png -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "bing_cookie": "", 3 | "gpt_api_key": "", 4 | "lark_verification_token": "", 5 | "lark_event_encrypt_key": "", 6 | "lark_app_id": "", 7 | "lark_app_secret": "", 8 | "lark_log_level": 2, 9 | "lark_event_server_addr": "localhost:8000", 10 | "is_feishu": true 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mrchi/lark-dalle3-bot 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/larksuite/oapi-sdk-go/v3 v3.0.31 7 | github.com/mrchi/bing-dalle3 v0.0.0-20231107052543-f8fc5d7a4cfe 8 | github.com/sashabaranov/go-openai v1.17.5 9 | ) 10 | 11 | require ( 12 | github.com/PuerkitoBio/goquery v1.8.1 // indirect 13 | github.com/andybalholm/cascadia v1.3.2 // indirect 14 | golang.org/x/net v0.17.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}", 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /internal/botconfig/botconfig.go: -------------------------------------------------------------------------------- 1 | package botconfig 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type BotConfig struct { 10 | BingCookie string `json:"bing_cookie"` 11 | LarkVerificationToken string `json:"lark_verification_token"` 12 | LarkEventEncryptKey string `json:"lark_event_encrypt_key"` 13 | LarkAppID string `json:"lark_app_id"` 14 | LarkAppSecret string `json:"lark_app_secret"` 15 | LarkLogLevel int `json:"lark_log_level"` 16 | LarkEventServerAddr string `json:"lark_event_server_addr"` 17 | IsFeishu bool `json:"is_feishu"` 18 | GPTAPIKey string `json:"gpt_api_key"` 19 | } 20 | 21 | func ReadConfigFromFile(filePath string) (*BotConfig, error) { 22 | var config BotConfig 23 | file, err := os.Open(filePath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer file.Close() 28 | 29 | content, err := io.ReadAll(file) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if err := json.Unmarshal(content, &config); err != nil { 35 | return nil, err 36 | } 37 | return &config, nil 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mrchi 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,go 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### macOS ### 28 | # General 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | # Icon must end with two \r 34 | Icon 35 | 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | 56 | ### macOS Patch ### 57 | # iCloud generated files 58 | *.icloud 59 | 60 | ### VisualStudioCode ### 61 | .vscode/* 62 | !.vscode/settings.json 63 | !.vscode/tasks.json 64 | !.vscode/launch.json 65 | !.vscode/extensions.json 66 | !.vscode/*.code-snippets 67 | 68 | # Local History for Visual Studio Code 69 | .history/ 70 | 71 | # Built Visual Studio Code Extensions 72 | *.vsix 73 | 74 | ### VisualStudioCode Patch ### 75 | # Ignore all local history of files 76 | .history 77 | .ionide 78 | 79 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,go 80 | 81 | config.json 82 | -------------------------------------------------------------------------------- /pkg/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/mrchi/lark-dalle3-bot/pkg/larkee" 8 | ) 9 | 10 | type Command struct { 11 | Prefix string 12 | HelpMsg string 13 | Execute func(prompt string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) 14 | } 15 | 16 | type CommandDispatcher struct { 17 | larkeeClient *larkee.LarkClient 18 | prefixes []string 19 | helpMsgs []string 20 | prefixCommandMap map[string]Command 21 | commandHelpExecute func(helpMsgs []string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) 22 | } 23 | 24 | func NewCommandDispatcher( 25 | larkeeClient *larkee.LarkClient, 26 | commandHelpExecute func(helpMsgs []string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string), 27 | commands ...Command, 28 | ) *CommandDispatcher { 29 | dispatcher := CommandDispatcher{larkeeClient: larkeeClient, commandHelpExecute: commandHelpExecute, prefixCommandMap: make(map[string]Command)} 30 | for _, command := range commands { 31 | dispatcher.prefixes = append(dispatcher.prefixes, command.Prefix) 32 | dispatcher.helpMsgs = append(dispatcher.helpMsgs, command.HelpMsg) 33 | dispatcher.prefixCommandMap[command.Prefix] = command 34 | } 35 | // 按照前缀长度逆序排序,避免出现 /a /ab /abc 时,/ab 会被 /a 匹配的情况 36 | sort.SliceStable(dispatcher.prefixes, func(i, j int) bool { 37 | return len(dispatcher.prefixes[i]) > len(dispatcher.prefixes[j]) 38 | }) 39 | return &dispatcher 40 | } 41 | 42 | func (dispatcher *CommandDispatcher) Dispatch(text string, messageId string, tanantKey string) { 43 | for _, prefix := range dispatcher.prefixes { 44 | if strings.HasPrefix(text, prefix) { 45 | prompt := strings.TrimSpace(strings.TrimPrefix(text, prefix)) 46 | go dispatcher.prefixCommandMap[prefix].Execute(prompt, dispatcher.larkeeClient, messageId, tanantKey) 47 | return 48 | } 49 | } 50 | go dispatcher.commandHelpExecute(dispatcher.helpMsgs, dispatcher.larkeeClient, messageId, tanantKey) 51 | } 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | 10 | larkcore "github.com/larksuite/oapi-sdk-go/v3/core" 11 | "github.com/larksuite/oapi-sdk-go/v3/core/httpserverext" 12 | larkevent "github.com/larksuite/oapi-sdk-go/v3/event" 13 | "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher" 14 | larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" 15 | bingdalle3 "github.com/mrchi/bing-dalle3" 16 | "github.com/mrchi/lark-dalle3-bot/internal/botconfig" 17 | cmddispatcher "github.com/mrchi/lark-dalle3-bot/pkg/dispatcher" 18 | larkee "github.com/mrchi/lark-dalle3-bot/pkg/larkee" 19 | "github.com/sashabaranov/go-openai" 20 | ) 21 | 22 | var ( 23 | config *botconfig.BotConfig 24 | bingClient *bingdalle3.BingDalle3 25 | gptClient *openai.Client 26 | ) 27 | 28 | func init() { 29 | var err error 30 | config, err = botconfig.ReadConfigFromFile("./config.json") 31 | if err != nil { 32 | panic(err) 33 | } 34 | bingClient = bingdalle3.NewBingDalle3(config.BingCookie) 35 | gptClient = openai.NewClient(config.GPTAPIKey) 36 | } 37 | 38 | func main() { 39 | var larkeeClient *larkee.LarkClient 40 | if config.IsFeishu { 41 | larkeeClient = larkee.NewFeishuClient(config.LarkAppID, config.LarkAppSecret, larkcore.LogLevel(config.LarkLogLevel)) 42 | log.Println("Initialize client for Feishu") 43 | } else { 44 | larkeeClient = larkee.NewLarkClient(config.LarkAppID, config.LarkAppSecret, larkcore.LogLevel(config.LarkLogLevel)) 45 | log.Println("Initialize client for Lark") 46 | } 47 | commandDispatcher := cmddispatcher.NewCommandDispatcher(larkeeClient, commandHelpExecute, commandBalance, commandPrompt, commandPromptPro) 48 | 49 | larkEventDispatcher := dispatcher.NewEventDispatcher(config.LarkVerificationToken, config.LarkEventEncryptKey) 50 | larkEventDispatcher.OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error { 51 | // 获取文本消息内容 52 | var msgContent larkee.LarkTextMessage 53 | json.Unmarshal([]byte(*event.Event.Message.Content), &msgContent) 54 | // 过滤 @ 信息 55 | text := regexp.MustCompile(`\s*@_all|@_user_\d+\s*`).ReplaceAllString(msgContent.Text, "") 56 | 57 | commandDispatcher.Dispatch(text, *event.Event.Message.MessageId, event.TenantKey()) 58 | return nil 59 | }, 60 | ) 61 | 62 | urlPath := "/dalle3" 63 | http.HandleFunc( 64 | urlPath, 65 | httpserverext.NewEventHandlerFunc( 66 | larkEventDispatcher, 67 | larkevent.WithLogLevel(larkcore.LogLevel(config.LarkLogLevel)), 68 | ), 69 | ) 70 | log.Printf("Start server at %s, url path is %s\n", config.LarkEventServerAddr, urlPath) 71 | http.ListenAndServe(config.LarkEventServerAddr, nil) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/larkee/message.go: -------------------------------------------------------------------------------- 1 | package larkee 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type LarkTextMessage struct { 8 | Text string `json:"text"` 9 | } 10 | 11 | type LarkInteractiveMessage struct { 12 | Elements []any `json:"elements"` 13 | } 14 | 15 | type LarkInteractiveMsgText struct { 16 | Tag string `json:"tag"` 17 | Content string `json:"content"` 18 | } 19 | 20 | type LarkInteractiveMsgColumnSet struct { 21 | Tag string `json:"tag"` 22 | Columns []LarkInteractiveMsgColumn `json:"columns"` 23 | } 24 | 25 | type LarkInteractiveMsgColumn struct { 26 | Tag string `json:"tag"` 27 | Width string `json:"width"` 28 | Weight int `json:"weight"` 29 | VerticalAlign string `json:"vertical_align"` 30 | Elements []LarkInteractiveMsgImg `json:"elements"` 31 | } 32 | 33 | type LarkInteractiveMsgImg struct { 34 | Tag string `json:"tag"` 35 | ImgKey string `json:"img_key"` 36 | Alt LarkInteractiveMsgText `json:"alt"` 37 | Mode string `json:"mode"` 38 | Preview bool `json:"preview"` 39 | } 40 | 41 | func NewLarkTextMessageContent(text string) (string, error) { 42 | msg := LarkTextMessage{Text: text} 43 | msgContent, err := json.Marshal(msg) 44 | if err != nil { 45 | return "", err 46 | } 47 | return string(msgContent), nil 48 | } 49 | 50 | func NewLarkImagesInteractiveContent(prompt string, imageKeys []string) (string, error) { 51 | textModule := LarkInteractiveMsgText{Tag: "markdown", Content: prompt} 52 | msg := LarkInteractiveMessage{Elements: []any{textModule}} 53 | 54 | // 每列展示 2 个图片 55 | for i := 0; i < len(imageKeys); i += 2 { 56 | columnSet := LarkInteractiveMsgColumnSet{Tag: "column_set"} 57 | columnSet.Columns = append( 58 | columnSet.Columns, 59 | LarkInteractiveMsgColumn{ 60 | Tag: "column", 61 | Width: "weighted", 62 | Weight: 1, 63 | VerticalAlign: "top", 64 | Elements: []LarkInteractiveMsgImg{ 65 | { 66 | Tag: "img", 67 | ImgKey: imageKeys[i], 68 | Alt: LarkInteractiveMsgText{Tag: "plain_text", Content: ""}, 69 | Mode: "fit_horizontal", 70 | Preview: true, 71 | }, 72 | }, 73 | }, 74 | ) 75 | if i+1 < len(imageKeys) { 76 | columnSet.Columns = append( 77 | columnSet.Columns, 78 | LarkInteractiveMsgColumn{ 79 | Tag: "column", 80 | Width: "weighted", 81 | Weight: 1, 82 | VerticalAlign: "top", 83 | Elements: []LarkInteractiveMsgImg{ 84 | { 85 | Tag: "img", 86 | ImgKey: imageKeys[i+1], 87 | Alt: LarkInteractiveMsgText{Tag: "plain_text", Content: ""}, 88 | Mode: "fit_horizontal", 89 | Preview: true, 90 | }, 91 | }, 92 | }, 93 | ) 94 | } 95 | msg.Elements = append(msg.Elements, columnSet) 96 | } 97 | 98 | msgContent, err := json.Marshal(msg) 99 | if err != nil { 100 | return "", err 101 | } 102 | return string(msgContent), nil 103 | } 104 | 105 | func NewLarkMarkdownContent(content string) (string, error) { 106 | textModule := LarkInteractiveMsgText{Tag: "markdown", Content: content} 107 | msg := LarkInteractiveMessage{Elements: []any{textModule}} 108 | 109 | msgContent, err := json.Marshal(msg) 110 | if err != nil { 111 | return "", err 112 | } 113 | return string(msgContent), nil 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lark-dalle3-bot 2 | 3 | This bot integrates with Feishu/Lark and generates images utilizing the [Image Creator from Microsoft Bing](https://www.bing.com/images/create) service. 4 | 5 | ![Alt text](doc/demo.png) 6 | 7 | ## Usage 8 | 9 | - **/balance** Get tokens balance of Bing cookie 10 | - **/prompt \** Create image with prompt 11 | - **/prompt_pro \** Create image with prompt revised by GPT 12 | 13 | ## Deployment 14 | 15 | ### Running the Bot with Default Configurations 16 | 17 | To make your bot accessible online, you can leverage services like [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/). 18 | 19 | 1. Compile the bot: 20 | ```bash 21 | go build . 22 | ``` 23 | 2. Initiate a config file from the provided template and modify the `lark_event_server_addr` as needed: 24 | ```bash 25 | cp config.example.json config.json 26 | ``` 27 | 3. (Optional) For Lark users, set config `is_feishu` to `false`. 28 | 4. Execute the program: 29 | ```bash 30 | ./lark-dalle3-bot 31 | ``` 32 | 33 | This step ensures that the server is reachable when setting up event subscriptions soon. 34 | 35 | ### Creating a Feishu/Lark Bot Application 36 | 37 | 1. On the [Feishu Open Platform](https://open.feishu.cn/app), start by creating a custom app. 38 | 2. In menu `Add features`, activate the `Bot` feature. 39 | 3. In menu `Permissions & Scopes`, add the following `API Scopes`: 40 | - `im:message` 41 | - `im:resource` 42 | - `im:message.group_at_msg:readonly` 43 | - `im:message.p2p_msg:readonly` 44 | 4. In menu `Event Subscriptions`: 45 | 1. Set `Request URL` with `http(s)://:/dalle3`. 46 | 2. Add the `im.message.receive_v1` event. 47 | 3. Grant these permissions to the new event: 48 | - `Obtain private messages sent to the bot` 49 | - `Obtain group messages mentioning the bot` 50 | 5. Publish your app through the `Version Management & Release` tab. 51 | 52 | **For Lark users, please visit [Lark Open Platform](https://open.larksuite.com/app).** 53 | 54 | ### Bot Configuration and Restart 55 | 56 | From the Feishu/Lark Open Platform: 57 | 1. Acquire the `App ID` and `App Secret` from menu`Credentials & Basic Info`. 58 | 2. Acquire the `Encrypt Key` and `Verification Token` from menu`Event Subscriptions`. 59 | 60 | From Bing: 61 | 1. Follow the instructions in the README at [yihong0618/BingImageCreator](https://github.com/yihong0618/BingImageCreator) to obtain your Bing cookie. 62 | 63 | **Only for Lark users, please set `is_feishu` to `false` in config file.** 64 | 65 | Update your `config.json` with these credentials, then rerun your bot and enjoy the magic. 66 | 67 | ## Related works 68 | 69 | - [mrchi/bing\-dalle3: Golang implementation of yihong0618/BingImageCreator](https://github.com/mrchi/bing-dalle3) 70 | 71 | ## Acknowledgments 72 | 73 | - [yihong0618/tg_bing_dalle: A Telegram bot for DALL-E 3 with Bing](https://github.com/yihong0618/tg_bing_dalle/) 74 | - [yihong0618/BingImageCreator: High-quality image generation by Microsoft's reverse-engineered API.](https://github.com/yihong0618/BingImageCreator) 75 | 76 | ## Disclaimer 77 | 78 | > Translated by GPT4. 79 | 80 | If you use this project for services open to the public in China, you are required to comply with the relevant laws and regulations, and you may need to obtain the necessary permits or complete relevant procedures. This project is only for technical communication and does not assume responsibility for any legal risks that may arise during your use. 81 | -------------------------------------------------------------------------------- /pkg/larkee/client.go: -------------------------------------------------------------------------------- 1 | package larkee 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | 8 | lark "github.com/larksuite/oapi-sdk-go/v3" 9 | larkcore "github.com/larksuite/oapi-sdk-go/v3/core" 10 | larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" 11 | ) 12 | 13 | type LarkAPIError struct { 14 | Code int 15 | Msg string 16 | } 17 | 18 | func (e LarkAPIError) Error() string { 19 | return fmt.Sprintf("%d: %s", e.Code, e.Msg) 20 | } 21 | 22 | type LarkClient struct { 23 | client *lark.Client 24 | } 25 | 26 | func (lc *LarkClient) ReplyTextMessage(content string, messageId string, tenantKey string) error { 27 | msgContent, err := NewLarkTextMessageContent(content) 28 | if err != nil { 29 | return err 30 | } 31 | msgBody := larkim.NewReplyMessageReqBodyBuilder(). 32 | MsgType(larkim.MsgTypeText).Content(msgContent). 33 | Build() 34 | req := larkim.NewReplyMessageReqBuilder(). 35 | MessageId(messageId).Body(msgBody). 36 | Build() 37 | resp, err := lc.client.Im.Message.Reply(context.Background(), req, larkcore.WithTenantKey(tenantKey)) 38 | if err != nil { 39 | return err 40 | } else if !resp.Success() { 41 | return LarkAPIError{Code: resp.Code, Msg: resp.Msg} 42 | } else { 43 | return nil 44 | } 45 | } 46 | 47 | func (lc *LarkClient) UploadImage(image *[]byte) (string, error) { 48 | reader := bytes.NewReader(*image) 49 | reqBody := larkim.NewCreateImageReqBodyBuilder().ImageType(larkim.ImageTypeMessage).Image(reader).Build() 50 | req := larkim.NewCreateImageReqBuilder().Body(reqBody).Build() 51 | resp, err := lc.client.Im.Image.Create(context.Background(), req) 52 | if err != nil { 53 | return "", err 54 | } else if !resp.Success() { 55 | return "", LarkAPIError{Code: resp.Code, Msg: resp.Msg} 56 | } else { 57 | return *resp.Data.ImageKey, nil 58 | } 59 | } 60 | 61 | func (lc *LarkClient) ReplyImagesInteractiveMessage(prompt string, imageKeys []string, messageId string, tenantKey string) error { 62 | msgContent, err := NewLarkImagesInteractiveContent(prompt, imageKeys) 63 | if err != nil { 64 | return err 65 | } 66 | msgBody := larkim.NewReplyMessageReqBodyBuilder(). 67 | MsgType(larkim.MsgTypeInteractive).Content(msgContent). 68 | Build() 69 | req := larkim.NewReplyMessageReqBuilder(). 70 | MessageId(messageId).Body(msgBody). 71 | Build() 72 | resp, err := lc.client.Im.Message.Reply(context.Background(), req, larkcore.WithTenantKey(tenantKey)) 73 | if err != nil { 74 | return err 75 | } else if !resp.Success() { 76 | return LarkAPIError{Code: resp.Code, Msg: resp.Msg} 77 | } else { 78 | return nil 79 | } 80 | } 81 | 82 | func (lc *LarkClient) ReplyMarkdownMessage(content string, messageId string, tenantKey string) error { 83 | msgContent, err := NewLarkMarkdownContent(content) 84 | if err != nil { 85 | return err 86 | } 87 | msgBody := larkim.NewReplyMessageReqBodyBuilder(). 88 | MsgType(larkim.MsgTypeInteractive).Content(msgContent). 89 | Build() 90 | req := larkim.NewReplyMessageReqBuilder(). 91 | MessageId(messageId).Body(msgBody). 92 | Build() 93 | resp, err := lc.client.Im.Message.Reply(context.Background(), req, larkcore.WithTenantKey(tenantKey)) 94 | if err != nil { 95 | return err 96 | } else if !resp.Success() { 97 | return LarkAPIError{Code: resp.Code, Msg: resp.Msg} 98 | } else { 99 | return nil 100 | } 101 | } 102 | 103 | func NewLarkClient(appId string, appSecret string, logLevel larkcore.LogLevel) *LarkClient { 104 | client := lark.NewClient(appId, appSecret, lark.WithLogLevel(logLevel), lark.WithOpenBaseUrl(lark.LarkBaseUrl)) 105 | return &LarkClient{client: client} 106 | } 107 | 108 | func NewFeishuClient(appId string, appSecret string, logLevel larkcore.LogLevel) *LarkClient { 109 | client := lark.NewClient(appId, appSecret, lark.WithLogLevel(logLevel), lark.WithOpenBaseUrl(lark.FeishuBaseUrl)) 110 | return &LarkClient{client: client} 111 | } 112 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= 2 | github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= 3 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 4 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 5 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 6 | github.com/larksuite/oapi-sdk-go/v3 v3.0.31 h1:a2TEpdG26Eco3ngIk86bxzEqWrkfZrW11JKDPjyWQ9A= 7 | github.com/larksuite/oapi-sdk-go/v3 v3.0.31/go.mod h1:F4MLXkfdc/7WAJPLy4lJ0R6VqCxKgqWYS1uYY84p3SI= 8 | github.com/mrchi/bing-dalle3 v0.0.0-20231107052543-f8fc5d7a4cfe h1:s7B7fH0Q/WafSpre+3yfPt/JlBBcqqsept61k6s+XEI= 9 | github.com/mrchi/bing-dalle3 v0.0.0-20231107052543-f8fc5d7a4cfe/go.mod h1:UgEeIiExJhCRPX5zUYbo/19pHjNksX3h+H98KpJBdKo= 10 | github.com/sashabaranov/go-openai v1.17.5 h1:ItBzlrrfTtkFWOFlgfOhk3y/xRBC4PJol4gdbiK7hgg= 11 | github.com/sashabaranov/go-openai v1.17.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 12 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 14 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 15 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 16 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 17 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 18 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 19 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 20 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 21 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 22 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 23 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 24 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 25 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 38 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 39 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 40 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 43 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 45 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 46 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 49 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 50 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 51 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/mrchi/lark-dalle3-bot/pkg/dispatcher" 11 | larkee "github.com/mrchi/lark-dalle3-bot/pkg/larkee" 12 | "github.com/sashabaranov/go-openai" 13 | ) 14 | 15 | func createImageAndReply(prompt string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) { 16 | // 判断 prompt 不为空 17 | if prompt == "" { 18 | larkeeClient.ReplyTextMessage("[Error]Prompt is empty", messageId, tanantKey) 19 | return 20 | } 21 | 22 | // 提交创建请求 23 | writingId, err := bingClient.CreateImage(prompt) 24 | if err != nil { 25 | larkeeClient.ReplyTextMessage(fmt.Sprintf("[Error]%s", err.Error()), messageId, tanantKey) 26 | return 27 | } 28 | 29 | // 返回一些提示信息 30 | messages := []string{"Request submitted\nWriting ID is " + writingId} 31 | balance, err := bingClient.GetTokenBalance() 32 | var balanceMsg string 33 | if err != nil { 34 | balanceMsg = fmt.Sprintf("[Error]Failed get token balance, %s", err.Error()) 35 | } else if balance == 0 { 36 | balanceMsg = "Tokens are exhausted, generation will take longer and may fail" 37 | } else { 38 | balanceMsg = fmt.Sprintf("There are %d token(s) left", balance) 39 | } 40 | messages = append(messages, balanceMsg) 41 | larkeeClient.ReplyTextMessage(strings.Join(messages, "\n"), messageId, tanantKey) 42 | 43 | // 获取生成结果 44 | imageUrls, err := bingClient.QueryResult(writingId, prompt) 45 | if err != nil { 46 | larkeeClient.ReplyTextMessage(fmt.Sprintf("[Error]%s", err.Error()), messageId, tanantKey) 47 | return 48 | } 49 | 50 | var wg sync.WaitGroup 51 | wg.Add(len(imageUrls)) 52 | imageKeys := make([]string, len(imageUrls)) 53 | for idx, imageUrl := range imageUrls { 54 | go func(idx int, imageUrl string) { 55 | defer wg.Done() 56 | reader, err := bingClient.DownloadImage(imageUrl) 57 | if err != nil { 58 | log.Printf("Download image failed, %s", err.Error()) 59 | return 60 | } 61 | imageKey, err := larkeeClient.UploadImage(reader) 62 | if err != nil { 63 | log.Printf("Upload image failed, %s", err.Error()) 64 | return 65 | } 66 | imageKeys[idx] = imageKey 67 | }(idx, imageUrl) 68 | } 69 | wg.Wait() 70 | larkeeClient.ReplyImagesInteractiveMessage(prompt, imageKeys, messageId, tanantKey) 71 | } 72 | 73 | var commandBalance = dispatcher.Command{ 74 | Prefix: "/balance", 75 | HelpMsg: "**/balance** Get tokens balance of Bing cookie", 76 | Execute: func(prompt string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) { 77 | balance, err := bingClient.GetTokenBalance() 78 | var replyMsg string 79 | if err != nil { 80 | replyMsg = fmt.Sprintf("[Error]%s", err.Error()) 81 | } else if balance == 0 { 82 | replyMsg = "Tokens are exhausted, generation will take longer and may fail" 83 | } else { 84 | replyMsg = fmt.Sprintf("There are %d token(s) left", balance) 85 | } 86 | larkeeClient.ReplyTextMessage(replyMsg, messageId, tanantKey) 87 | }, 88 | } 89 | 90 | var commandPrompt = dispatcher.Command{ 91 | Prefix: "/prompt", 92 | HelpMsg: "**/prompt <Your prompt>** Create image with prompt", 93 | Execute: createImageAndReply, 94 | } 95 | 96 | var commandPromptPro = dispatcher.Command{ 97 | Prefix: "/prompt_pro", 98 | HelpMsg: `**/prompt_pro <Your prompt>** Create image with prompt revised by GPT`, 99 | Execute: func(prompt string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) { 100 | // 判断 prompt 不为空 101 | if prompt == "" { 102 | larkeeClient.ReplyTextMessage("[Error]Prompt is empty", messageId, tanantKey) 103 | return 104 | } 105 | 106 | // Revise prompt by GPT 107 | gptResp, err := gptClient.CreateChatCompletion( 108 | context.Background(), 109 | openai.ChatCompletionRequest{ 110 | Model: openai.GPT3Dot5Turbo, 111 | Messages: []openai.ChatCompletionMessage{ 112 | { 113 | Role: openai.ChatMessageRoleUser, 114 | Content: fmt.Sprintf("Revise `%s` to a concrete DALL·E prompt", prompt), 115 | }, 116 | }, 117 | }, 118 | ) 119 | if err == nil { 120 | prompt = gptResp.Choices[0].Message.Content 121 | larkeeClient.ReplyTextMessage( 122 | fmt.Sprintf("GPT revising successful\n\n%s", prompt), 123 | messageId, tanantKey, 124 | ) 125 | } else { 126 | larkeeClient.ReplyTextMessage( 127 | fmt.Sprintf("GPT revising failed, fallback to original prompt\n[Error]%s", err), 128 | messageId, tanantKey, 129 | ) 130 | } 131 | createImageAndReply(prompt, larkeeClient, messageId, tanantKey) 132 | }, 133 | } 134 | 135 | func commandHelpExecute(helpMsgs []string, larkeeClient *larkee.LarkClient, messageId string, tanantKey string) { 136 | msg := "Welcome to use DALL·E 3 bot. We now support the following commands:\n\n" + strings.Join(helpMsgs, "\n") 137 | larkeeClient.ReplyMarkdownMessage(msg, messageId, tanantKey) 138 | } 139 | --------------------------------------------------------------------------------