【Golang】LINE Notifyで画像を送る

先日の記事では、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