package main import ( "bytes" "encoding/json" "fmt" "io" "log" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" ) const maxAPIBase = "https://platform-api.max.ru" type maxUpdatesResponse struct { Updates []maxUpdate `json:"updates"` Marker *int64 `json:"marker"` } type maxUpdate struct { UpdateType string `json:"update_type"` Timestamp int64 `json:"timestamp"` Message *maxMessage `json:"message"` } type maxMessage struct { Sender *maxUser `json:"sender"` Recipient *maxRecipient `json:"recipient"` Body *maxMessageBody `json:"body"` } type maxUser struct { UserID int64 `json:"user_id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` IsBot bool `json:"is_bot"` } type maxRecipient struct { ChatID int64 `json:"chat_id"` ChatType string `json:"chat_type"` UserID int64 `json:"user_id"` } type maxMessageBody struct { Text string `json:"text"` Attachments []maxAttachment `json:"attachments"` } type maxAttachment struct { Type string `json:"type"` Payload json.RawMessage `json:"payload"` } type maxAttachmentURL struct { URL string `json:"url"` } func (b *Bridge) maxRequest(method, path string, query url.Values, body any) ([]byte, error) { if b.MAXToken == "" { return nil, fmt.Errorf("MAX_TOKEN не задан") } reqURL := maxAPIBase + path if len(query) > 0 { reqURL += "?" + query.Encode() } var bodyReader io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, reqURL, bodyReader) if err != nil { return nil, err } req.Header.Set("Authorization", b.MAXToken) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("MAX API %s %s: %d %s", method, path, resp.StatusCode, string(respBody)) } return respBody, nil } func (b *Bridge) SendToMAX(chatID int64, text string) error { query := url.Values{"chat_id": {strconv.FormatInt(chatID, 10)}} _, err := b.maxRequest(http.MethodPost, "/messages", query, map[string]any{"text": text}) return err } type maxUploadURLResponse struct { URL string `json:"url"` Token string `json:"token"` } // SendVideoFileToMAX загружает локальный файл как video и отправляет в чат. func (b *Bridge) SendVideoFileToMAX(chatID int64, filePath, text string) error { query := url.Values{"type": {"video"}} data, err := b.maxRequest(http.MethodPost, "/uploads", query, nil) if err != nil { return err } var up maxUploadURLResponse if err := json.Unmarshal(data, &up); err != nil { return err } if up.URL == "" || up.Token == "" { return fmt.Errorf("MAX /uploads: пустой url или token: %s", string(data)) } if err := b.uploadFileToMAXURL(up.URL, filePath); err != nil { return err } body := map[string]any{ "text": text, "attachments": []map[string]any{ { "type": "video", "payload": map[string]string{ "token": up.Token, }, }, }, } msgQuery := url.Values{"chat_id": {strconv.FormatInt(chatID, 10)}} // После загрузки MAX может ещё обрабатывать файл — повторяем при attachment.not.ready var lastErr error for attempt := 0; attempt < 8; attempt++ { if attempt > 0 { wait := time.Duration(attempt*attempt) * time.Second log.Printf("[MAX] ожидание обработки видео %s…", wait) time.Sleep(wait) } _, lastErr = b.maxRequest(http.MethodPost, "/messages", msgQuery, body) if lastErr == nil { return nil } if !strings.Contains(lastErr.Error(), "attachment.not.ready") && !strings.Contains(lastErr.Error(), "not.processed") { return lastErr } } return lastErr } func (b *Bridge) uploadFileToMAXURL(uploadURL, filePath string) error { f, err := os.Open(filePath) if err != nil { return err } defer f.Close() pr, pw := io.Pipe() w := multipart.NewWriter(pw) errCh := make(chan error, 1) go func() { defer pw.Close() part, err := w.CreateFormFile("data", filepath.Base(filePath)) if err != nil { errCh <- err _ = pw.CloseWithError(err) return } if _, err := io.Copy(part, f); err != nil { errCh <- err _ = pw.CloseWithError(err) return } errCh <- w.Close() }() req, err := http.NewRequest(http.MethodPost, uploadURL, pr) if err != nil { _ = pr.Close() return err } req.Header.Set("Content-Type", w.FormDataContentType()) if b.MAXToken != "" { req.Header.Set("Authorization", b.MAXToken) } resp, err := http.DefaultClient.Do(req) writeErr := <-errCh if err != nil { return err } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if writeErr != nil { return writeErr } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("MAX upload file: %d %s", resp.StatusCode, string(respBody)) } return nil } func (b *Bridge) GetMAXUpdates(marker *int64, timeout int) (*maxUpdatesResponse, error) { query := url.Values{ "limit": {"100"}, "types": {"message_created"}, } if timeout > 0 { query.Set("timeout", strconv.Itoa(timeout)) } if marker != nil { query.Set("marker", strconv.FormatInt(*marker, 10)) } data, err := b.maxRequest(http.MethodGet, "/updates", query, nil) if err != nil { return nil, err } var resp maxUpdatesResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, err } return &resp, nil } func (b *Bridge) SubscribeMAXWebhook(webhookURL, secret string) error { body := map[string]any{ "url": webhookURL, "update_types": []string{"message_created"}, } if secret != "" { body["secret"] = secret } _, err := b.maxRequest(http.MethodPost, "/subscriptions", nil, body) return err } func maxUserDisplayName(u *maxUser) string { if u == nil { return "unknown" } name := u.FirstName if u.LastName != "" { if name != "" { name += " " } name += u.LastName } if name == "" && u.Username != "" { name = u.Username } if name == "" { return strconv.FormatInt(u.UserID, 10) } return name } func maxMessageChatID(msg *maxMessage) int64 { if msg == nil || msg.Recipient == nil { return 0 } return msg.Recipient.ChatID } func maxExtractMediaURLs(msg *maxMessage) []string { if msg == nil || msg.Body == nil { return nil } var urls []string for _, att := range msg.Body.Attachments { switch att.Type { case "image", "video", "audio", "file", "sticker": var payload maxAttachmentURL if err := json.Unmarshal(att.Payload, &payload); err == nil && payload.URL != "" { urls = append(urls, payload.URL) } } } return urls }