package main import ( "bytes" "encoding/json" "errors" "fmt" "html" "io" "log" "mime" "mime/multipart" "net/http" "net/textproto" "strings" "time" telegram "github.com/go-telegram-bot-api/telegram-bot-api" filetype "gopkg.in/h2non/filetype.v1" ) func (this *NTFServer) ContentedEnabled() bool { return this.contentedMaxBytes > 0 } func (this *NTFServer) uploadAsyncComplete(userID int64, typeName string, conUrl string, err error, textPrefix string) { if err != nil { log.Printf("Upload failed for %s: %s", typeName, err.Error()) this.callOnMainThread <- func() { this.GroupChatSayHTML("Can't upload " + typeName + " for native users: " + html.EscapeString(err.Error()) + "") } return } // Upload success if len(textPrefix) > 0 { textPrefix = strings.Trim(textPrefix, ` `) + " " } this.callOnMainThread <- func() { // n.b. this will fail if the user has disconnected by the time the upload completed this.HubSay(userID, textPrefix+conUrl) } } func (this *NTFServer) ContentedUploadFallbackSync(FileID string, FileSize int64, thumb *telegram.PhotoSize) (string, error) { if FileSize < this.contentedMaxBytes { return this.ContentedUploadSync(FileID, FileSize) } else if thumb != nil && int64(thumb.FileSize) < this.contentedMaxBytes { return this.ContentedUploadSync(thumb.FileID, int64(thumb.FileSize)) } else { return "", errors.New("File too big and/or bad thumbnail") } } func (this *NTFServer) ContentedUploadBestSync(thumbs []telegram.PhotoSize) (string, error) { bestKnownIdx := -1 bestKnownMpx := int64(-1) for idx, img := range thumbs { if int64(img.FileSize) > this.contentedMaxBytes { continue // no good } // Highest total pixel count mpx := int64(img.Width) * int64(img.Height) if mpx > bestKnownMpx { bestKnownIdx = idx bestKnownMpx = mpx } } if bestKnownIdx == -1 { return "", errors.New("The file was too large for the image host server") } return this.ContentedUploadSync(thumbs[bestKnownIdx].FileID, int64(thumbs[bestKnownIdx].FileSize)) } func (this *NTFServer) ContentedUploadSync(fileId string, expectSizeBytes int64) (string, error) { // If file fits under size limit, take it if expectSizeBytes > this.contentedMaxBytes { return "", errors.New("The file was too large for the image host server (and no smaller thumbnail is available)") } // Prepare to download the file from telegram url, err := this.bot.GetFileDirectURL(fileId) if err != nil { return "", fmt.Errorf("TelegramGetFileDirectURL: %s", err.Error()) } if this.verbose { log.Printf("Downloading telegram file %s", url) } resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("HttpGet: %s", err.Error()) } defer resp.Body.Close() if this.verbose { log.Printf("Downloaded telegram file response headers: %#v\n", resp.Header) } // Download fileBuff := bytes.Buffer{} _, err = io.CopyN(&fileBuff, resp.Body, expectSizeBytes) // CopyN asserts either err!=nil, or copiedBytes == expectSizeBytes. So we're safe if err != nil { return "", fmt.Errorf("CopyN: %s", err.Error()) } // Determine a suitable pseudo-filename from the telegram content-type // Telegram seems to always supply application/octet-stream // If Telegram gives us something else, trust it; but otherwise, we can probably do better fileName := fmt.Sprintf("telegram-upload-%d", time.Now().Unix()) fileContentType := resp.Header.Get("Content-Type") // is it blank? who knows? if fileContentType == `application/octet-stream` || fileContentType == `` { // Autodetect something better, if possible if contentTypeMatches, err := filetype.Match(fileBuff.Bytes()); err == nil { fileContentType = contentTypeMatches.MIME.Value fileName += `.` + contentTypeMatches.Extension // no leading period } } else { // We were supplied a good mime type, just need to find a good file extension if fileExts, err := mime.ExtensionsByType(fileContentType); err == nil && len(fileExts) > 0 { fileName += fileExts[0] // Just pick the first one. They all contain a leading period } } // Contented uploads are in multipart mime format uploadBody := bytes.Buffer{} formValues := multipart.NewWriter(&uploadBody) // Adapted from (mime/multipart)Writer.CreateFormFile to support a custom interior content type multipartFileHeader := make(textproto.MIMEHeader) multipartFileHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "f", fileName)) multipartFileHeader.Set("Content-Type", fileContentType) filePart, err := formValues.CreatePart(multipartFileHeader) if err != nil { return "", err } _, err = io.CopyN(filePart, bytes.NewReader(fileBuff.Bytes()), expectSizeBytes) if err != nil { return "", err } // Upload err = formValues.Close() if err != nil { return "", fmt.Errorf("Close: %s", err.Error()) } req, err := http.NewRequest("POST", this.config.ContentedURL+"upload", &uploadBody) if err != nil { return "", err } req.Header.Set("Content-Type", formValues.FormDataContentType()) // it's "multipart/form-data; boundary=xxxx" with a magic string uploadResp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("Upload failed: %s", err.Error()) } defer uploadResp.Body.Close() // Read back the response from contented // Should be a 200 error code, JSON content type, array of strings of shortcodes to the uploaded file(s) if uploadResp.StatusCode != 200 { return "", fmt.Errorf("Unexpected HTTP %d response back from file hosting server", uploadResp.StatusCode) } shortCodes := make([]string, 0, 1) err = json.NewDecoder(uploadResp.Body).Decode(&shortCodes) if err != nil { return "", fmt.Errorf("Decoding response from file hosting server: %s", err.Error()) } if len(shortCodes) != 1 || len(shortCodes[0]) == 0 { return "", errors.New("Unexpected blank response back from file hosting server") } // DONE return this.config.ContentedURL + "p/" + shortCodes[0], nil }