先日の記事では、LINE Notifyでメッセージを送りました。
今回は画像を送ってみます。
少し検索してみても、メッセージを送る記事ばかりで画像を送るのはあまりないですね。
LINE公式のSDKでもimageFile
で検索しても、実装されていないようなので、自分で実装することにしました。
ハマったところ
LINE Notify API Documentにもやり方が書いてあるし、すぐできると思ったのですが、multipart/form-data
でハマりました。
LINE NotifyにSticker送信機能と画像アップロード機能が追加されました に書いてあるようにcurlで以下のように画像を送信できます。
$ curl -X POST https://notify-api.line.me/api/notify -H 'Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN' -F 'message=test' -F 'imageFile=@/PATH/TO/IMAGE/cony.jpg'
curlした結果をパケットキャプチャ取得して、http streamを見てみると以下のようになってるんですね。
Content-Type: multipart/form-data; boundary=------------------------1ac14b01bb1c4782 --------------------------1ac14b01bb1c4782 Content-Disposition: form-data; name="message" test --------------------------1ac14b01bb1c4782 Content-Disposition: form-data; name="imageFile"; filename="sample.jpg" Content-Type: image/jpeg (略)
LINE Notify API Documentのリクエスト方法にmultipart/form-data
が書いてあるのは、ここで使うためだったようです。
Content-Type application/x-www-form-urlencoded OR multipart/form-data
恥ずかしながら、multipart/form-data
を使ったことがなかったので、調べたところ
[PRG]いまさら聞けないHTTPマルチパートフォームデータ送信
にわかりやすく書いてありました。
Content-Type
のフォーマットは以下で、boundary
で指定される文字列によって区切ってあげることで、複数のデータをhttpリクエスト中に付与できるようになっています。
Content-Type: multipart/form-data; boundary=「バウンダリ文字列」\r\n
golangには、標準のmime/multipartパッケージがあるので、こちらを利用します。さらに今回は、imageFile
と独自ヘッダを付けたいので、
net/textprotoパッケージも使います。
実装
実装は以下です。
package main import ( "bytes" "errors" "image/jpeg" "image/png" "io" "mime/multipart" "net/http" "net/textproto" "os" "path/filepath" ) var ( URL = "https://notify-api.line.me/api/notify" ) func main() { msg := "send an image" filename := "./tmp.jpg" accessToken := "<YOUR ACCESS TOKEN>" f, err := os.Open(filename) if err != nil { panic(err) } defer f.Close() c := &http.Client{} var b bytes.Buffer w := multipart.NewWriter(&b) fw, err := w.CreateFormField("message") if err != nil { panic(err) } if _, err = fw.Write([]byte(msg)); err != nil { panic(err) } part := make(textproto.MIMEHeader) part.Set("Content-Disposition", `form-data; name="imageFile"; filename=`+filename) img, format, err := checkImageFormat(f, filename) if err != nil { panic(err) } if format == "jpeg" { part.Set("Content-Type", "image/jpeg") } else if format == "png" { part.Set("Content-Type", "image/png") } else { panic("LINE Notify supports only jpeg/png image format") } fw, err = w.CreatePart(part) if err != nil { panic(err) } io.Copy(fw, img) w.Close() // boundaryの書き込み req, err := http.NewRequest("POST", URL, &b) if err != nil { panic(err) } req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.Do(req) if err != nil { panic(err) } if resp.StatusCode != 200 { panic("failed to send image, get http status code: " + resp.Status) } } // checkImageFormat validates an image file is not illegal and // returns image as io.Reader and file format. func checkImageFormat(r io.Reader, filename string) (io.Reader, string, error) { ext := filepath.Ext(filename) var b bytes.Buffer if ext == ".jpeg" || ext == ".jpg" || ext == ".JPEG" || ext == ".JPG" { ext = "jpeg" img, err := jpeg.Decode(r) if err != nil { return nil, "", err } if err := jpeg.Encode(&b, img, &jpeg.Options{Quality: 100}); err != nil { return nil, "", err } } else if ext == ".png" || ext == ".PNG" { ext = "png" img, err := jpeg.Decode(r) if err != nil { return nil, "", err } if err = png.Encode(&b, img); err != nil { return nil, "", err } } else { return nil, "", errors.New("Image format must be jpeg or png") } return &b, ext, nil }
sdkにしたものはGithubに上げてあります。 使い方はREADMEに記載しています(LINE -> 画像を送る)。
References
- GolangでLINE Notify - 逆さまにした
- line-bot-sdk-go
- LINE Notify API Document
- LINE NotifyにSticker送信機能と画像アップロード機能が追加されました
- [PRG]いまさら聞けないHTTPマルチパートフォームデータ送信
- golang の mime/multipart で独自の Content-Type を付ける。
- golang POST data using the Content-Type multipart/form-data - stackoverflow
- mime/multipart - The Go Programming Language
- net/textproto - The Go Programming Language