├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── main.go └── utils └── config.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | go.sum 14 | config.yaml 15 | Crisp_Telegram_bot 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tony Zou 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 | # Crisp Telegram Bot 2 | 3 | A telegram bot built with golang to help integrate Crisp into Telegram. 4 | 5 | Currently Supports: 6 | - Forward user messages from crisp to admins on telegram. 7 | - Reply user messages directly on telegram. 8 | 9 | Will Support: 10 | - Integration with Slack 11 | - Detailed visitor info 12 | 13 | ## Getting Started 14 | 15 | 1. Get your crisp API credentials from [Crisp API token generator](https://go.crisp.chat/account/token/) 16 | 1. Create a bot with [BotFather](https://t.me/botfather), save the token for later use. 17 | 1. Build & Run. 18 | 19 | ## Requirements 20 | Redis server is used for storing relation between Telegram messages and Crisp messages. 21 | 22 | ## Installing & Deployment 23 | 24 | ### Use prebuilt binary 25 | Download from [release page](). 26 | 27 | ### Built on your own 28 | `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build` 29 | 30 | Replace GOOS GOARCH with your server architecture. 31 | 32 | ## config.yaml 33 | 34 | ``` 35 | debug: true 36 | redis: 37 | host: localhost:6379 38 | db: 0 39 | password: '' 40 | crisp: 41 | identifier: 049sk12f-8349-8274-9d91-f21jv91kafa7 42 | key: 078f2106a5d89179gkqn38e5e82e3c7j30ajfkelqnvd874fb2378573499ff505 43 | telegram: 44 | key: 45 | admins: 46 | - 93847124 47 | ``` 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT License. 52 | 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tonyzzzzzz/Crisp_Telegram_bot 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/crisp-im/go-crisp-api v3.3.2+incompatible 7 | github.com/go-redis/redis v6.15.5+incompatible 8 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible 9 | github.com/gorilla/websocket v1.4.1 // indirect 10 | github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f // indirect 11 | github.com/jinzhu/gorm v1.9.11 12 | github.com/spf13/viper v1.4.0 13 | github.com/technoweenie/multipartstreamer v1.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/crisp-im/go-crisp-api/crisp" 11 | "github.com/go-redis/redis" 12 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" 13 | "github.com/spf13/viper" 14 | "github.com/tonyzzzzzz/Crisp_Telegram_bot/utils" 15 | ) 16 | 17 | var bot *tgbotapi.BotAPI 18 | var client *crisp.Client 19 | var redisClient *redis.Client 20 | var config *viper.Viper 21 | 22 | // CrispMessageInfo stores the original message 23 | type CrispMessageInfo struct { 24 | WebsiteID string 25 | SessionID string 26 | } 27 | 28 | // MarshalBinary serializes CrispMessageInfo into binary 29 | func (s *CrispMessageInfo) MarshalBinary() ([]byte, error) { 30 | return json.Marshal(s) 31 | } 32 | 33 | // UnmarshalBinary deserializes CrispMessageInfo into struct 34 | func (s *CrispMessageInfo) UnmarshalBinary(data []byte) error { 35 | return json.Unmarshal(data, s) 36 | } 37 | 38 | func contains(s []interface{}, e int64) bool { 39 | for _, a := range s { 40 | if int64(a.(int)) == e { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | func replyToUser(update *tgbotapi.Update) { 48 | if update.Message.ReplyToMessage == nil { 49 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "请回复一个消息") 50 | bot.Send(msg) 51 | return 52 | } 53 | 54 | res, err := redisClient.Get(strconv.Itoa(update.Message.ReplyToMessage.MessageID)).Result() 55 | 56 | if err != nil { 57 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "ERROR: "+err.Error()) 58 | bot.Send(msg) 59 | return 60 | } 61 | 62 | var msgInfo CrispMessageInfo 63 | err = json.Unmarshal([]byte(res), &msgInfo) 64 | 65 | if err := json.Unmarshal([]byte(res), &msgInfo); err != nil { 66 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "ERROR: "+err.Error()) 67 | bot.Send(msg) 68 | return 69 | } 70 | 71 | if update.Message.Text != "" { 72 | client.Website.SendTextMessageInConversation(msgInfo.WebsiteID, msgInfo.SessionID, crisp.ConversationTextMessageNew{ 73 | Type: "text", 74 | From: "operator", 75 | Origin: "chat", 76 | Content: update.Message.Text, 77 | }) 78 | } 79 | 80 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "回复成功!") 81 | bot.Send(msg) 82 | } 83 | 84 | func sendMsgToAdmins(text string, WebsiteID string, SessionID string) { 85 | for _, id := range config.Get("admins").([]interface{}) { 86 | msg := tgbotapi.NewMessage(int64(id.(int)), text) 87 | msg.ParseMode = "Markdown" 88 | sent, _ := bot.Send(msg) 89 | 90 | redisClient.Set(strconv.Itoa(sent.MessageID), &CrispMessageInfo{ 91 | WebsiteID, 92 | SessionID, 93 | }, 12*time.Hour) 94 | } 95 | } 96 | 97 | func init() { 98 | config = utils.GetConfig() 99 | 100 | log.Printf("Initializing Redis...") 101 | 102 | redisClient = redis.NewClient(&redis.Options{ 103 | Addr: config.GetString("redis.host"), 104 | Password: config.GetString("redis.password"), 105 | DB: config.GetInt("redis.db"), 106 | }) 107 | 108 | var err error 109 | 110 | _, err = redisClient.Ping().Result() 111 | if err != nil { 112 | log.Panic(err) 113 | } 114 | 115 | log.Printf("Initializing Bot...") 116 | 117 | bot, err = tgbotapi.NewBotAPI(config.GetString("telegram.key")) 118 | 119 | if err != nil { 120 | log.Panic(err) 121 | } 122 | 123 | bot.Debug = config.GetBool("debug") 124 | bot.RemoveWebhook() 125 | 126 | log.Printf("Authorized on account %s", bot.Self.UserName) 127 | 128 | log.Printf("Initializing Crisp Listner") 129 | client = crisp.New() 130 | // Set authentication parameters 131 | client.Authenticate(config.GetString("crisp.identifier"), config.GetString("crisp.key")) 132 | 133 | // Connect to realtime events backend and listen (only to 'message:send' namespace) 134 | client.Events.Listen( 135 | []string{ 136 | "message:send", 137 | }, 138 | 139 | func(reg *crisp.EventsRegister) { 140 | // Socket is connected: now listening for events 141 | 142 | // Notice: if the realtime socket breaks at any point, this function will be called again upon reconnect (to re-bind events) 143 | // Thus, ensure you only use this to register handlers 144 | 145 | // Register handler on 'message:send/text' namespace 146 | reg.On("message:send/text", func(evt crisp.EventsReceiveTextMessage) { 147 | text := fmt.Sprintf(`*%s(%s): *%s`, *evt.User.Nickname, *evt.User.UserID, *evt.Content) 148 | sendMsgToAdmins(text, *evt.WebsiteID, *evt.SessionID) 149 | }) 150 | 151 | // Register handler on 'message:send/file' namespace 152 | reg.On("message:send/file", func(evt crisp.EventsReceiveFileMessage) { 153 | text := fmt.Sprintf(`*%s(%s): *[File](%s)`, *evt.User.Nickname, *evt.User.UserID, evt.Content.URL) 154 | sendMsgToAdmins(text, *evt.WebsiteID, *evt.SessionID) 155 | }) 156 | 157 | // Register handler on 'message:send/animation' namespace 158 | reg.On("message:send/animation", func(evt crisp.EventsReceiveAnimationMessage) { 159 | text := fmt.Sprintf(`*%s(%s): *[Animation](%s)`, *evt.User.Nickname, *evt.User.UserID, evt.Content.URL) 160 | sendMsgToAdmins(text, *evt.WebsiteID, *evt.SessionID) 161 | }) 162 | }, 163 | 164 | func() { 165 | log.Printf("Crisp listener disconnected, reconnecting...") 166 | }, 167 | 168 | func() { 169 | log.Fatal("Crisp listener error, check your API key or internet connection?") 170 | }, 171 | ) 172 | } 173 | 174 | func main() { 175 | var updates tgbotapi.UpdatesChannel 176 | 177 | log.Print("Start pooling") 178 | u := tgbotapi.NewUpdate(0) 179 | u.Timeout = 60 180 | 181 | updates, _ = bot.GetUpdatesChan(u) 182 | 183 | for update := range updates { 184 | if update.Message == nil { 185 | continue 186 | } 187 | 188 | log.Printf("%s %s: %s", update.Message.From.FirstName, update.Message.From.LastName, update.Message.Text) 189 | 190 | switch update.Message.Command() { 191 | case "start": 192 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Blinkload Telegram 客服助手") 193 | msg.ParseMode = "Markdown" 194 | bot.Send(msg) 195 | } 196 | 197 | if contains(config.Get("admins").([]interface{}), int64(update.Message.From.ID)) { 198 | replyToUser(&update) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // GetConfig returns the global config 11 | func GetConfig() *viper.Viper { 12 | c := viper.New() 13 | c.SetConfigType("yaml") 14 | c.SetConfigName("config") 15 | c.AddConfigPath(".") 16 | c.AutomaticEnv() 17 | 18 | c.SetDefault("debug", true) 19 | c.SetDefault("admins", []interface{}{}) 20 | 21 | c.SetDefault("redis.host", "localhost:6379") 22 | c.SetDefault("redis.db", 0) 23 | c.SetDefault("redis.password", "") 24 | 25 | c.SetDefault("crisp.identifier", "") 26 | c.SetDefault("crisp.key", "") 27 | 28 | c.SetDefault("telegram.key", "") 29 | 30 | replacer := strings.NewReplacer(".", "_") 31 | c.SetEnvKeyReplacer(replacer) 32 | 33 | if err := c.ReadInConfig(); err != nil { 34 | log.Fatal(err.Error()) 35 | } 36 | 37 | return c 38 | } 39 | --------------------------------------------------------------------------------