// This is the main package package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "path" "runtime/debug" "strconv" "strings" "sync" "time" "gitea.balki.me/telegram-msgchkbox/glist" ) var apiToken string func main() { apiToken = os.Getenv("CHKBOT_API_TOKEN") if apiToken == "" { log.Panicln("CHKBOT_API_TOKEN is empty") } port := func() int { portStr := os.Getenv("CHKBOT_PORT") if port, err := strconv.Atoi(portStr); err == nil { return port } return 28923 }() dataPath := func() string { dataPath := os.Getenv("CHKBOT_DATA_PATH") if dataPath == "" { return "." } if err := os.MkdirAll(dataPath, 0755); err != nil { log.Panicf("Failed to create datapath, path: %s, err: %s\n", dataPath, err) } return dataPath }() glist.DataPath = dataPath commit := func() string { if bi, ok := debug.ReadBuildInfo(); ok { for _, s := range bi.Settings { if s.Key == "vcs.revision" { return s.Value[:8] } } } return "unknown" }() log.Printf("Grocery List bot (%s) starting with datapath:%s, port:%d\n", commit, dataPath, port) var chats sync.Map if err := loadData(dataPath, &chats); err != nil { log.Panicf("failed to load data, err: %s\n", err) } botPath := fmt.Sprintf("/bot%s", apiToken) http.HandleFunc(botPath, func(w http.ResponseWriter, r *http.Request) { defer func() { if _, err := w.Write([]byte("ok")); err != nil { log.Println(err) } }() body, err := io.ReadAll(r.Body) if err != nil { log.Println(err) return } log.Println(string(body)) update := struct { Message *struct { ID int `json:"message_id"` Chat struct { ID int `json:"id"` } Text string } CallbackQuery *struct { ID string `json:"id"` Message struct { ID int `json:"message_id"` Chat struct { ID int `json:"id"` } } Data string } `json:"callback_query"` }{} if err := json.Unmarshal(body, &update); err != nil { log.Println(err) return } // Ignore if Text is empty or is a command if update.Message != nil && update.Message.Text != "" && update.Message.Text[0] != '/' { chatID := update.Message.Chat.ID g, _ := chats.LoadOrStore(chatID, glist.NewGList(chatID)) gl := g.(*glist.GList) go handleTextAdded(gl, update.Message.Text) } else if update.CallbackQuery != nil { defer func() { go answerCallbackQuery(update.CallbackQuery.ID) }() chatID := update.CallbackQuery.Message.Chat.ID g, ok := chats.Load(chatID) if !ok { log.Printf("Chat not found: %d\n", chatID) return } gl := g.(*glist.GList) go handleButtonClick(gl, update.CallbackQuery.Message.ID, update.CallbackQuery.Data) } }) log.Panicln(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } func handleTextAdded(gl *glist.GList, text string) { gl.Mutex.Lock() defer gl.Mutex.Unlock() gl.Add(text) count := gl.AllMsgCounter + 1 gl.AllMsgCounter = count time.AfterFunc(10*time.Second, func() { gl.Mutex.Lock() defer gl.Mutex.Unlock() if count == gl.AllMsgCounter { resp := sendList(gl, "sendMessage") if resp == nil { return } response := struct { Ok bool `json:"ok"` Result struct { MessageID int `json:"message_id"` } `json:"result"` }{} if err := json.Unmarshal(resp, &response); err != nil { log.Println(err) return } if !response.Ok { log.Println("not ok") return } if gl.MessageID != nil { deleteMessage(gl.ChatID, *gl.MessageID) } gl.MessageID = &response.Result.MessageID } }) } func handleButtonClick(gl *glist.GList, messageID int, text string) { gl.Mutex.Lock() defer gl.Mutex.Unlock() gl.MessageID = &messageID if text == "clear" { gl.ClearChecked() } else { gl.Toggle(text) } if len(gl.Items) == 0 { deleteMessage(gl.ChatID, messageID) gl.MessageID = nil } else { sendList(gl, "editMessageText") } } func sendList(gl *glist.GList, method string) []byte { url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", apiToken, method) sendMsgReq, err := gl.GenSendListReq() if err != nil { log.Println(err) return nil } resp, err := http.Post(url, "application/json", bytes.NewReader(sendMsgReq)) if err != nil { log.Println(err) return nil } return logBody(resp.Body) } func answerCallbackQuery(callbackQueryID string) { answerURL := fmt.Sprintf("https://api.telegram.org/bot%s/answerCallbackQuery?callback_query_id=%stext=ok", apiToken, callbackQueryID) resp, err := http.Get(answerURL) if err != nil { log.Println(err) return } logBody(resp.Body) } func deleteMessage(chatID int, messageID int) { deleteURL := fmt.Sprintf("https://api.telegram.org/bot%s/deleteMessage?chat_id=%d&message_id=%d", apiToken, chatID, messageID) resp, err := http.Get(deleteURL) if err != nil { log.Println(err) return } body := logBody(resp.Body) ok := struct { Ok bool `json:"ok"` }{} if err := json.Unmarshal(body, &ok); err != nil { log.Println(err) return } //Old messages can't be deleted, so edit text instead if !ok.Ok { updateURL := fmt.Sprintf("https://api.telegram.org/bot%s/editMessageText?chat_id=%d&message_id=%d&text=%s", apiToken, chatID, messageID, "") resp, err := http.Get(updateURL) if err != nil { log.Println(err) return } logBody(resp.Body) } } func logBody(respBody io.ReadCloser) []byte { defer func() { err := respBody.Close() if err != nil { log.Println(err) } }() body, err := io.ReadAll(respBody) if err != nil { log.Println(err) return nil } log.Println(string(body)) return body } func loadData(dataPath string, chats *sync.Map) error { items, err := os.ReadDir(dataPath) if err != nil { return err } for _, de := range items { if de.IsDir() { continue } name := de.Name() if !strings.HasPrefix(name, "chkchat") { continue } var gl glist.GList data, err := os.ReadFile(path.Join(dataPath, name)) if err != nil { return fmt.Errorf("failed read file, name: %s, err:%w", name, err) } err = json.Unmarshal(data, &gl) if err != nil { return fmt.Errorf("failed to parse data, data:%s, err:%w", data, err) } chats.Store(gl.ChatID, &gl) } return nil }