package main import ( "bufio" "bytes" "encoding/json" "fmt" "html" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "os" "os/exec" "strconv" "strings" "time" ) func handleMessage(client *http.Client, botToken string, message *Message) error { if message.Text == "" { return nil } split := strings.SplitN(message.Text, "\n", 2) if len(split) == 0 { return nil } command := exec.Command("sh", "-c", split[0]) if len(split) == 2 { command.Stdin = strings.NewReader(split[1]) } stdout, err := ioutil.TempFile("", "ridi*") if err != nil { return fmt.Errorf("failed to create temporary file: %s", err) } defer os.Remove(stdout.Name()) command.Stdout = stdout command.Stderr = stdout err = command.Start() if err != nil { return fmt.Errorf("failed to start process: %s", err) } resp, err := client.Get(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%d&text=%%2B%%2B%%2B executing process %d %%2B%%2B%%2B&parse_mode=html&reply_to_message_id=%d&allow_sending_without_reply=true", botToken, message.Chat.Id, command.Process.Pid, message.MessageId)) if err != nil { return fmt.Errorf("failed to send executing process message: %s", err) } rawJson, err := parseBody(resp) if err != nil { return fmt.Errorf("failed to parse sendMessage response: %s", err) } rawJsonBytes, err := rawJson.MarshalJSON() if err != nil { return fmt.Errorf("failed to marshal raw json bytes for sendMessage response: %s", err) } var reply Message err = json.Unmarshal(rawJsonBytes, &reply) if err != nil { return fmt.Errorf("failed to parse raw json bytes for sendMessage response: %s", err) } err = command.Wait() exitCode := 0 if err != nil { if ee, ok := err.(*exec.ExitError); ok { exitCode = ee.ExitCode() } else { return fmt.Errorf("error occured while waiting for command: %s", err) } } exitCodeMessage := fmt.Sprintf("+++ exited with %d +++", exitCode) fileSize, err := stdout.Seek(0, 2) if err != nil { return fmt.Errorf("failed to seek to end of file: %s", err) } _, err = stdout.Seek(0, 0) if err != nil { return fmt.Errorf("failed to seek to start of file: %s", err) } reader := bufio.NewReader(stdout) if fileSize > 4096-1-int64(len(exitCodeMessage)) { var buf bytes.Buffer w := multipart.NewWriter(&buf) fw, err := w.CreateFormFile("document", "output.txt") if err != nil { return fmt.Errorf("failed to create output.txt header: %s", err) } _, err = io.Copy(fw, reader) if err != nil { return fmt.Errorf("failed to copy output.txt: %s", err) } w.Close() resp, err = client.Get(fmt.Sprintf("https://api.telegram.org/bot%s/editMessageText?chat_id=%d&message_id=%d&text=%%2B%%2B%%2B uploading output %%2B%%2B%%2B&parse_mode=html", botToken, message.Chat.Id, reply.MessageId)) // not returning since this is non-crucial if err != nil { fmt.Fprintf(os.Stderr, "failed to edit message: %s\n", err) } // deviating from checking response for errors because we just wanna see if it failed if resp.StatusCode != 200 { return fmt.Errorf("received %d when editing message", resp.StatusCode) } resp, err = client.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendDocument?chat_id=%d&caption=%s&parse_mode=html&reply_to_message_id=%d&allow_sending_without_reply=true", botToken, message.Chat.Id, url.QueryEscape(exitCodeMessage), message.MessageId), w.FormDataContentType(), &buf) if err != nil { return fmt.Errorf("failed to send output.txt: %s", err) } if resp.StatusCode != 200 { return fmt.Errorf("received %d when sending output.txt", resp.StatusCode) } resp, err = client.Get(fmt.Sprintf("https://api.telegram.org/bot%s/deleteMessage?chat_id=%d&message_id=%d", botToken, message.Chat.Id, reply.MessageId)) if err != nil { return fmt.Errorf("failed to delete message: %s", err) } if resp.StatusCode != 200 { return fmt.Errorf("received %d when deleting message", resp.StatusCode) } return nil } var builder strings.Builder _, err = reader.WriteTo(&builder) if err != nil { return fmt.Errorf("failed to copy output.txt: %s", err) } resp, err = client.Get(fmt.Sprintf("https://api.telegram.org/bot%s/editMessageText?chat_id=%d&message_id=%d&parse_mode=html&text=%s%%0A
%s
", botToken, message.Chat.Id, reply.MessageId, url.QueryEscape(exitCodeMessage), url.QueryEscape(html.EscapeString(builder.String())))) if err != nil { return fmt.Errorf("failed to edit message: %s", err) } if resp.StatusCode != 200 { return fmt.Errorf("received %d when editing message", resp.StatusCode) } return nil } func handleMessageWithErrors(client *http.Client, botToken string, message *Message) { err := handleMessage(client, botToken, message) if err != nil { fmt.Fprintln(os.Stderr, err) resp, err := client.Get(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%d&text=%s&parse_mode=html&reply_to_message_id=%d&allow_sending_without_reply=true", botToken, message.Chat.Id, url.QueryEscape(html.EscapeString(err.Error())), message.MessageId)) if err != nil { fmt.Fprintf(os.Stderr, "failed to send message about error: %s\n", err) return } if resp.StatusCode != 200 { fmt.Fprintf(os.Stderr, "received %d when sending message about error\n", resp.StatusCode) return } } } func main() { botToken := os.Getenv("BOT_TOKEN") if botToken == "" { fmt.Fprintf(os.Stderr, "BOT_TOKEN is empty\n") os.Exit(1) } allowedChatsStr := strings.Split(os.Getenv("ALLOWED_CHATS"), " ") if allowedChatsStr[0] == "" { fmt.Fprintf(os.Stderr, "ALLOWED_CHATS is empty\n") os.Exit(1) } var allowedChats []int64 for i := 0; i < len(allowedChatsStr); i++ { chatId, err := strconv.ParseInt(allowedChatsStr[i], 10, 64) if err != nil { fmt.Fprintf(os.Stderr, "failed to parse %s as a chat id: %s\n", allowedChatsStr[i], err) os.Exit(1) } allowedChats = append(allowedChats, chatId) } client := &http.Client{ Timeout: time.Duration(time.Second * 10), } updates, err := getUpdates(client, botToken, 0, -1) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var updatesOffset int64 = -1 if len(updates) > 0 { updatesOffset = updates[0].UpdateId } fmt.Println("listening for updates") for { time.Sleep(time.Duration(time.Second)) updates, err = getUpdates(client, botToken, 1, updatesOffset+1) if err != nil { fmt.Fprintln(os.Stderr, err) continue } for i := 0; i < len(updates); i++ { update := updates[i] if update.UpdateId > updatesOffset { updatesOffset = update.UpdateId } if !chatIsAllowed(allowedChats, update.Message.Chat.Id) { fmt.Printf("received message from unauthorized chat %d\n", update.Message.Chat.Id) continue } if update.Message == nil { fmt.Println("received non-message update") continue } go handleMessageWithErrors(client, botToken, update.Message) } } }