Skip to content

Commit 637019c

Browse files
authored
Send error from chatgpt.SendMessage, add tgbot package, conversations management by chatGPT (#43)
* ChatGPT as conversation manager * fix: sending of error from SendMessage. Separate bot into its own package * Resolve conflicts - add edit interval to tgbot.New
1 parent 02dd479 commit 637019c

File tree

3 files changed

+163
-128
lines changed

3 files changed

+163
-128
lines changed

main.go

+34-115
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,13 @@ import (
99
"syscall"
1010
"time"
1111

12-
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
1312
"github.com/joho/godotenv"
1413
"github.com/m1guelpf/chatgpt-telegram/src/chatgpt"
1514
"github.com/m1guelpf/chatgpt-telegram/src/config"
16-
"github.com/m1guelpf/chatgpt-telegram/src/markdown"
17-
"github.com/m1guelpf/chatgpt-telegram/src/ratelimit"
1815
"github.com/m1guelpf/chatgpt-telegram/src/session"
16+
"github.com/m1guelpf/chatgpt-telegram/src/tgbot"
1917
)
2018

21-
type Conversation struct {
22-
ConversationID string
23-
LastMessageID string
24-
}
25-
2619
func main() {
2720
config, err := config.Init()
2821
if err != nil {
@@ -49,7 +42,17 @@ func main() {
4942
log.Fatalf("Couldn't load .env file: %v", err)
5043
}
5144

52-
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_TOKEN"))
45+
editInterval := 1 * time.Second
46+
if os.Getenv("EDIT_WAIT_SECONDS") != "" {
47+
editSecond, err := strconv.ParseInt(os.Getenv("EDIT_WAIT_SECONDS"), 10, 64)
48+
if err != nil {
49+
log.Printf("Couldn't convert your edit seconds setting into int: %v", err)
50+
editSecond = 1
51+
}
52+
editInterval = time.Duration(editSecond) * time.Second
53+
}
54+
55+
bot, err := tgbot.New(os.Getenv("TELEGRAM_TOKEN"), editInterval)
5356
if err != nil {
5457
log.Fatalf("Couldn't start Telegram bot: %v", err)
5558
}
@@ -58,140 +61,56 @@ func main() {
5861
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
5962
go func() {
6063
<-c
61-
bot.StopReceivingUpdates()
64+
bot.Stop()
6265
os.Exit(0)
6366
}()
6467

65-
updateConfig := tgbotapi.NewUpdate(0)
66-
updateConfig.Timeout = 30
67-
updates := bot.GetUpdatesChan(updateConfig)
68-
69-
log.Printf("Started Telegram bot! Message @%s to start.", bot.Self.UserName)
68+
log.Printf("Started Telegram bot! Message @%s to start.", bot.Username)
7069

71-
userConversations := make(map[int64]Conversation)
72-
73-
editInterval := 1 * time.Second
74-
if os.Getenv("EDIT_WAIT_SECONDS") != "" {
75-
editSecond, err := strconv.ParseInt(os.Getenv("EDIT_WAIT_SECONDS"), 10, 64)
76-
if err != nil {
77-
log.Printf("Couldn't convert your edit seconds setting into int: %v", err)
78-
editSecond = 1
79-
}
80-
editInterval = time.Duration(editSecond) * time.Second
81-
}
82-
83-
for update := range updates {
70+
for update := range bot.GetUpdatesChan() {
8471
if update.Message == nil {
8572
continue
8673
}
8774

88-
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
89-
msg.ReplyToMessageID = update.Message.MessageID
90-
msg.ParseMode = "Markdown"
75+
var (
76+
updateText = update.Message.Text
77+
updateChatID = update.Message.Chat.ID
78+
updateMessageID = update.Message.MessageID
79+
)
9180

9281
userId := strconv.FormatInt(update.Message.Chat.ID, 10)
9382
if os.Getenv("TELEGRAM_ID") != "" && userId != os.Getenv("TELEGRAM_ID") {
94-
msg.Text = "You are not authorized to use this bot."
95-
bot.Send(msg)
83+
bot.Send(updateChatID, updateMessageID, "You are not authorized to use this bot.")
9684
continue
9785
}
9886

99-
bot.Request(tgbotapi.NewChatAction(update.Message.Chat.ID, "typing"))
10087
if !update.Message.IsCommand() {
101-
feed, err := chatGPT.SendMessage(update.Message.Text, userConversations[update.Message.Chat.ID].ConversationID, userConversations[update.Message.Chat.ID].LastMessageID)
102-
if err != nil {
103-
msg.Text = fmt.Sprintf("Error: %v", err)
104-
}
105-
106-
var message tgbotapi.Message
107-
var lastResp string
108-
109-
debouncedType := ratelimit.Debounce((10 * time.Second), func() {
110-
bot.Request(tgbotapi.NewChatAction(update.Message.Chat.ID, "typing"))
111-
})
112-
113-
debouncedEdit := ratelimit.DebounceWithArgs(editInterval, func(text interface{}, messageId interface{}) {
114-
_, err = bot.Request(tgbotapi.EditMessageTextConfig{
115-
BaseEdit: tgbotapi.BaseEdit{
116-
ChatID: msg.ChatID,
117-
MessageID: messageId.(int),
118-
},
119-
Text: text.(string),
120-
ParseMode: "Markdown",
121-
})
122-
123-
if err != nil {
124-
if err.Error() == "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message" {
125-
return
126-
}
127-
128-
log.Printf("Couldn't edit message: %v", err)
129-
}
130-
})
131-
132-
pollResponse:
133-
for {
134-
debouncedType()
135-
136-
select {
137-
case response, ok := <-feed:
138-
if !ok {
139-
break pollResponse
140-
}
141-
142-
userConversations[update.Message.Chat.ID] = Conversation{
143-
LastMessageID: response.MessageId,
144-
ConversationID: response.ConversationId,
145-
}
146-
lastResp = markdown.EnsureFormatting(response.Message)
147-
msg.Text = lastResp
148-
149-
if message.MessageID == 0 {
150-
message, err = bot.Send(msg)
151-
if err != nil {
152-
log.Fatalf("Couldn't send message: %v", err)
153-
}
154-
} else {
155-
debouncedEdit(lastResp, message.MessageID)
156-
}
157-
}
158-
}
159-
160-
_, err = bot.Request(tgbotapi.EditMessageTextConfig{
161-
BaseEdit: tgbotapi.BaseEdit{
162-
ChatID: msg.ChatID,
163-
MessageID: message.MessageID,
164-
},
165-
Text: lastResp,
166-
ParseMode: "Markdown",
167-
})
88+
bot.SendTyping(updateChatID)
16889

90+
feed, err := chatGPT.SendMessage(updateText, updateChatID)
16991
if err != nil {
170-
if err.Error() == "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message" {
171-
continue
172-
}
173-
174-
log.Printf("Couldn't perform final edit on message: %v", err)
92+
bot.Send(updateChatID, updateMessageID, fmt.Sprintf("Error: %v", err))
93+
} else {
94+
bot.SendAsLiveOutput(updateChatID, updateMessageID, feed)
17595
}
176-
17796
continue
17897
}
17998

99+
var text string
180100
switch update.Message.Command() {
181101
case "help":
182-
msg.Text = "Send a message to start talking with ChatGPT. You can use /reload at any point to clear the conversation history and start from scratch (don't worry, it won't delete the Telegram messages)."
102+
text = "Send a message to start talking with ChatGPT. You can use /reload at any point to clear the conversation history and start from scratch (don't worry, it won't delete the Telegram messages)."
183103
case "start":
184-
msg.Text = "Send a message to start talking with ChatGPT. You can use /reload at any point to clear the conversation history and start from scratch (don't worry, it won't delete the Telegram messages)."
104+
text = "Send a message to start talking with ChatGPT. You can use /reload at any point to clear the conversation history and start from scratch (don't worry, it won't delete the Telegram messages)."
185105
case "reload":
186-
userConversations[update.Message.Chat.ID] = Conversation{}
187-
msg.Text = "Started a new conversation. Enjoy!"
106+
chatGPT.ResetConversation(updateChatID)
107+
text = "Started a new conversation. Enjoy!"
188108
default:
189-
continue
109+
text = "Unknown command. Send /help to see a list of commands."
190110
}
191111

192-
if _, err := bot.Send(msg); err != nil {
193-
log.Printf("Couldn't send message: %v", err)
194-
continue
112+
if _, err := bot.Send(updateChatID, updateMessageID, text); err != nil {
113+
log.Printf("Error sending message: %v", err)
195114
}
196115
}
197116
}

src/chatgpt/chatgpt.go

+24-13
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ import (
1616
const KEY_ACCESS_TOKEN = "accessToken"
1717
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
1818

19+
type Conversation struct {
20+
ID string
21+
LastMessageID string
22+
}
23+
1924
type ChatGPT struct {
2025
SessionToken string
2126
AccessTokenMap expirymap.ExpiryMap
27+
conversations map[int64]Conversation
2228
}
2329

2430
type SessionResult struct {
@@ -39,15 +45,14 @@ type MessageResponse struct {
3945
}
4046

4147
type ChatResponse struct {
42-
Message string
43-
MessageId string
44-
ConversationId string
48+
Message string
4549
}
4650

47-
func Init(config config.Config) ChatGPT {
48-
return ChatGPT{
51+
func Init(config config.Config) *ChatGPT {
52+
return &ChatGPT{
4953
AccessTokenMap: expirymap.New(),
5054
SessionToken: config.OpenAISession,
55+
conversations: make(map[int64]Conversation),
5156
}
5257
}
5358

@@ -61,8 +66,11 @@ func (c *ChatGPT) EnsureAuth() error {
6166
return err
6267
}
6368

64-
func (c *ChatGPT) SendMessage(message string, conversationId string, messageId string) (chan ChatResponse, error) {
65-
r := make(chan ChatResponse)
69+
func (c *ChatGPT) ResetConversation(chatID int64) {
70+
c.conversations[chatID] = Conversation{}
71+
}
72+
73+
func (c *ChatGPT) SendMessage(message string, tgChatID int64) (chan ChatResponse, error) {
6674
accessToken, err := c.refreshAccessToken()
6775
if err != nil {
6876
return nil, errors.New(fmt.Sprintf("Couldn't get access token: %v", err))
@@ -75,11 +83,14 @@ func (c *ChatGPT) SendMessage(message string, conversationId string, messageId s
7583
"Authorization": fmt.Sprintf("Bearer %s", accessToken),
7684
}
7785

78-
err = client.Connect(message, conversationId, messageId)
86+
convo := c.conversations[tgChatID]
87+
err = client.Connect(message, convo.ID, convo.LastMessageID)
7988
if err != nil {
8089
return nil, errors.New(fmt.Sprintf("Couldn't connect to ChatGPT: %v", err))
8190
}
8291

92+
r := make(chan ChatResponse)
93+
8394
go func() {
8495
defer close(r)
8596
mainLoop:
@@ -98,11 +109,11 @@ func (c *ChatGPT) SendMessage(message string, conversationId string, messageId s
98109
}
99110

100111
if len(res.Message.Content.Parts) > 0 {
101-
r <- ChatResponse{
102-
MessageId: res.Message.ID,
103-
ConversationId: res.ConversationId,
104-
Message: res.Message.Content.Parts[0],
105-
}
112+
convo.ID = res.ConversationId
113+
convo.LastMessageID = res.Message.ID
114+
c.conversations[tgChatID] = convo
115+
116+
r <- ChatResponse{Message: res.Message.Content.Parts[0]}
106117
}
107118
}
108119
}

src/tgbot/bot.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package tgbot
2+
3+
import (
4+
"log"
5+
"time"
6+
7+
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
8+
"github.com/m1guelpf/chatgpt-telegram/src/chatgpt"
9+
"github.com/m1guelpf/chatgpt-telegram/src/markdown"
10+
"github.com/m1guelpf/chatgpt-telegram/src/ratelimit"
11+
)
12+
13+
type Bot struct {
14+
Username string
15+
api *tgbotapi.BotAPI
16+
editInterval time.Duration
17+
}
18+
19+
func New(token string, editInterval time.Duration) (*Bot, error) {
20+
api, err := tgbotapi.NewBotAPI(token)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
return &Bot{
26+
Username: api.Self.UserName,
27+
api: api,
28+
editInterval: editInterval,
29+
}, nil
30+
}
31+
32+
func (b *Bot) GetUpdatesChan() tgbotapi.UpdatesChannel {
33+
cfg := tgbotapi.NewUpdate(0)
34+
cfg.Timeout = 30
35+
return b.api.GetUpdatesChan(cfg)
36+
}
37+
38+
func (b *Bot) Stop() {
39+
b.api.StopReceivingUpdates()
40+
}
41+
42+
func (b *Bot) Send(chatID int64, replyTo int, text string) (tgbotapi.Message, error) {
43+
text = markdown.EnsureFormatting(text)
44+
msg := tgbotapi.NewMessage(chatID, text)
45+
msg.ReplyToMessageID = replyTo
46+
return b.api.Send(msg)
47+
}
48+
49+
func (b *Bot) SendEdit(chatID int64, messageID int, text string) error {
50+
text = markdown.EnsureFormatting(text)
51+
msg := tgbotapi.NewEditMessageText(chatID, messageID, text)
52+
msg.ParseMode = "Markdown"
53+
if _, err := b.api.Send(msg); err != nil {
54+
if err.Error() == "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message" {
55+
return nil
56+
}
57+
return err
58+
}
59+
return nil
60+
}
61+
62+
func (b *Bot) SendTyping(chatID int64) {
63+
if _, err := b.api.Request(tgbotapi.NewChatAction(chatID, "typing")); err != nil {
64+
log.Printf("Couldn't send typing action: %v", err)
65+
}
66+
}
67+
68+
func (b *Bot) SendAsLiveOutput(chatID int64, replyTo int, feed chan chatgpt.ChatResponse) {
69+
debouncedType := ratelimit.Debounce(10*time.Second, func() { b.SendTyping(chatID) })
70+
debouncedEdit := ratelimit.DebounceWithArgs(b.editInterval, func(text interface{}, messageId interface{}) {
71+
if err := b.SendEdit(chatID, messageId.(int), text.(string)); err != nil {
72+
log.Printf("Couldn't edit message: %v", err)
73+
}
74+
})
75+
76+
var message tgbotapi.Message
77+
var lastResp string
78+
79+
pollResponse:
80+
for {
81+
debouncedType()
82+
83+
select {
84+
case response, ok := <-feed:
85+
if !ok {
86+
break pollResponse
87+
}
88+
89+
lastResp = response.Message
90+
91+
if message.MessageID == 0 {
92+
var err error
93+
if message, err = b.Send(chatID, replyTo, lastResp); err != nil {
94+
log.Fatalf("Couldn't send message: %v", err)
95+
}
96+
} else {
97+
debouncedEdit(lastResp, message.MessageID)
98+
}
99+
}
100+
}
101+
102+
if err := b.SendEdit(chatID, message.MessageID, lastResp); err != nil {
103+
log.Printf("Couldn't perform final edit on message: %v", err)
104+
}
105+
}

0 commit comments

Comments
 (0)