github.com/line/line-bot-sdk-go/v7@v7.21.0/linebot/client.go (about)

     1  // Copyright 2016 LINE Corporation
     2  //
     3  // LINE Corporation licenses this file to you under the Apache License,
     4  // version 2.0 (the "License"); you may not use this file except in compliance
     5  // with the License. You may obtain a copy of the License at:
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    11  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    12  // License for the specific language governing permissions and limitations
    13  // under the License.
    14  
    15  package linebot
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"mime/multipart"
    24  	"net/http"
    25  	"net/textproto"
    26  	"net/url"
    27  	"path"
    28  	"time"
    29  )
    30  
    31  // APIEndpoint constants
    32  const (
    33  	APIEndpointBase     = "https://api.line.me"
    34  	APIEndpointBaseData = "https://api-data.line.me"
    35  
    36  	APIEndpointPushMessage                = "/v2/bot/message/push"
    37  	APIEndpointBroadcastMessage           = "/v2/bot/message/broadcast"
    38  	APIEndpointReplyMessage               = "/v2/bot/message/reply"
    39  	APIEndpointMulticast                  = "/v2/bot/message/multicast"
    40  	APIEndpointNarrowcast                 = "/v2/bot/message/narrowcast"
    41  	APIEndpointValidatePushMessage        = "/v2/bot/message/validate/push"
    42  	APIEndpointValidateBroadcastMessage   = "/v2/bot/message/validate/broadcast"
    43  	APIEndpointValidateReplyMessage       = "/v2/bot/message/validate/reply"
    44  	APIEndpointValidateMulticastMessage   = "/v2/bot/message/validate/multicast"
    45  	APIEndpointValidateNarrowcastMessage  = "/v2/bot/message/validate/narrowcast"
    46  	APIEndpointGetMessageContent          = "/v2/bot/message/%s/content"
    47  	APIEndpointGetMessageQuota            = "/v2/bot/message/quota"
    48  	APIEndpointGetMessageConsumption      = "/v2/bot/message/quota/consumption"
    49  	APIEndpointGetMessageQuotaConsumption = "/v2/bot/message/quota/consumption"
    50  	APIEndpointLeaveGroup                 = "/v2/bot/group/%s/leave"
    51  	APIEndpointLeaveRoom                  = "/v2/bot/room/%s/leave"
    52  	APIEndpointGetProfile                 = "/v2/bot/profile/%s"
    53  	APIEndpointGetFollowerIDs             = "/v2/bot/followers/ids"
    54  	APIEndpointGetGroupMemberProfile      = "/v2/bot/group/%s/member/%s"
    55  	APIEndpointGetRoomMemberProfile       = "/v2/bot/room/%s/member/%s"
    56  	APIEndpointGetGroupMemberIDs          = "/v2/bot/group/%s/members/ids"
    57  	APIEndpointGetRoomMemberIDs           = "/v2/bot/room/%s/members/ids"
    58  	APIEndpointGetGroupMemberCount        = "/v2/bot/group/%s/members/count"
    59  	APIEndpointGetRoomMemberCount         = "/v2/bot/room/%s/members/count"
    60  	APIEndpointGetGroupSummary            = "/v2/bot/group/%s/summary"
    61  	APIEndpointCreateRichMenu             = "/v2/bot/richmenu"
    62  	APIEndpointGetRichMenu                = "/v2/bot/richmenu/%s"
    63  	APIEndpointListRichMenu               = "/v2/bot/richmenu/list"
    64  	APIEndpointDeleteRichMenu             = "/v2/bot/richmenu/%s"
    65  	APIEndpointGetUserRichMenu            = "/v2/bot/user/%s/richmenu"
    66  	APIEndpointLinkUserRichMenu           = "/v2/bot/user/%s/richmenu/%s"
    67  	APIEndpointUnlinkUserRichMenu         = "/v2/bot/user/%s/richmenu"
    68  	APIEndpointSetDefaultRichMenu         = "/v2/bot/user/all/richmenu/%s"
    69  	APIEndpointDefaultRichMenu            = "/v2/bot/user/all/richmenu"   // Get: GET / Delete: DELETE
    70  	APIEndpointDownloadRichMenuImage      = "/v2/bot/richmenu/%s/content" // Download: GET / Upload: POST
    71  	APIEndpointUploadRichMenuImage        = "/v2/bot/richmenu/%s/content" // Download: GET / Upload: POST
    72  	APIEndpointBulkLinkRichMenu           = "/v2/bot/richmenu/bulk/link"
    73  	APIEndpointBulkUnlinkRichMenu         = "/v2/bot/richmenu/bulk/unlink"
    74  	APIEndpointValidateRichMenuObject     = "/v2/bot/richmenu/validate"
    75  
    76  	APIEndpointCreateRichMenuAlias = "/v2/bot/richmenu/alias"
    77  	APIEndpointGetRichMenuAlias    = "/v2/bot/richmenu/alias/%s"
    78  	APIEndpointUpdateRichMenuAlias = "/v2/bot/richmenu/alias/%s"
    79  	APIEndpointDeleteRichMenuAlias = "/v2/bot/richmenu/alias/%s"
    80  	APIEndpointListRichMenuAlias   = "/v2/bot/richmenu/alias/list"
    81  
    82  	APIEndpointGetAllLIFFApps = "/liff/v1/apps"
    83  	APIEndpointAddLIFFApp     = "/liff/v1/apps"
    84  	APIEndpointUpdateLIFFApp  = "/liff/v1/apps/%s/view"
    85  	APIEndpointDeleteLIFFApp  = "/liff/v1/apps/%s"
    86  
    87  	APIEndpointLinkToken = "/v2/bot/user/%s/linkToken"
    88  
    89  	APIEndpointGetMessageDelivery = "/v2/bot/message/delivery/%s"
    90  	APIEndpointGetMessageProgress = "/v2/bot/message/progress/%s"
    91  	APIEndpointInsight            = "/v2/bot/insight/%s"
    92  	APIEndpointGetBotInfo         = "/v2/bot/info"
    93  
    94  	APIEndpointIssueAccessToken  = "/v2/oauth/accessToken"
    95  	APIEndpointRevokeAccessToken = "/v2/oauth/revoke"
    96  	APIEndpointVerifyAccessToken = "/v2/oauth/verify"
    97  
    98  	APIEndpointIssueAccessTokenV2  = "/oauth2/v2.1/token"
    99  	APIEndpointGetAccessTokensV2   = "/oauth2/v2.1/tokens/kid"
   100  	APIEndpointRevokeAccessTokenV2 = "/oauth2/v2.1/revoke"
   101  
   102  	APIEndpointGetWebhookInfo     = "/v2/bot/channel/webhook/endpoint"
   103  	APIEndpointSetWebhookEndpoint = "/v2/bot/channel/webhook/endpoint"
   104  	APIEndpointTestWebhook        = "/v2/bot/channel/webhook/test"
   105  
   106  	APIAudienceGroupUpload            = "/v2/bot/audienceGroup/upload"
   107  	APIAudienceGroupUploadByFile      = "/v2/bot/audienceGroup/upload/byFile"
   108  	APIAudienceGroupClick             = "/v2/bot/audienceGroup/click"
   109  	APIAudienceGroupIMP               = "/v2/bot/audienceGroup/imp"
   110  	APIAudienceGroupUpdateDescription = "/v2/bot/audienceGroup/%d/updateDescription"
   111  	APIAudienceGroupActivate          = "/v2/bot/audienceGroup/%d/activate"
   112  	APIAudienceGroup                  = "/v2/bot/audienceGroup/%d"
   113  	APIAudienceGroupList              = "/v2/bot/audienceGroup/list"
   114  	APIAudienceGroupAuthorityLevel    = "/v2/bot/audienceGroup/authorityLevel"
   115  )
   116  
   117  // Client type
   118  type Client struct {
   119  	channelSecret    string
   120  	channelToken     string
   121  	endpointBase     *url.URL     // default APIEndpointBase
   122  	endpointBaseData *url.URL     // default APIEndpointBaseData
   123  	httpClient       *http.Client // default http.DefaultClient
   124  	retryKeyID       string       // X-Line-Retry-Key allows you to safely retry API requests without duplicating messages
   125  }
   126  
   127  // ClientOption type
   128  type ClientOption func(*Client) error
   129  
   130  // New returns a new bot client instance.
   131  func New(channelSecret, channelToken string, options ...ClientOption) (*Client, error) {
   132  	if channelSecret == "" {
   133  		return nil, errors.New("missing channel secret")
   134  	}
   135  	if channelToken == "" {
   136  		return nil, errors.New("missing channel access token")
   137  	}
   138  	c := &Client{
   139  		channelSecret: channelSecret,
   140  		channelToken:  channelToken,
   141  		httpClient:    http.DefaultClient,
   142  	}
   143  	for _, option := range options {
   144  		err := option(c)
   145  		if err != nil {
   146  			return nil, err
   147  		}
   148  	}
   149  	if c.endpointBase == nil {
   150  		u, err := url.ParseRequestURI(APIEndpointBase)
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  		c.endpointBase = u
   155  	}
   156  	if c.endpointBaseData == nil {
   157  		u, err := url.ParseRequestURI(APIEndpointBaseData)
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		c.endpointBaseData = u
   162  	}
   163  	return c, nil
   164  }
   165  
   166  // WithHTTPClient function
   167  func WithHTTPClient(c *http.Client) ClientOption {
   168  	return func(client *Client) error {
   169  		client.httpClient = c
   170  		return nil
   171  	}
   172  }
   173  
   174  // WithEndpointBase function
   175  func WithEndpointBase(endpointBase string) ClientOption {
   176  	return func(client *Client) error {
   177  		u, err := url.ParseRequestURI(endpointBase)
   178  		if err != nil {
   179  			return err
   180  		}
   181  		client.endpointBase = u
   182  		return nil
   183  	}
   184  }
   185  
   186  // WithEndpointBaseData function
   187  func WithEndpointBaseData(endpointBaseData string) ClientOption {
   188  	return func(client *Client) error {
   189  		u, err := url.ParseRequestURI(endpointBaseData)
   190  		if err != nil {
   191  			return err
   192  		}
   193  		client.endpointBaseData = u
   194  		return nil
   195  	}
   196  }
   197  
   198  func (client *Client) url(base *url.URL, endpoint string) string {
   199  	u := *base
   200  	u.Path = path.Join(u.Path, endpoint)
   201  	return u.String()
   202  }
   203  
   204  func (client *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) {
   205  	req.Header.Set("Authorization", "Bearer "+client.channelToken)
   206  	req.Header.Set("User-Agent", "LINE-BotSDK-Go/"+version)
   207  	if len(client.retryKeyID) > 0 {
   208  		req.Header.Set("X-Line-Retry-Key", client.retryKeyID)
   209  	}
   210  	if ctx != nil {
   211  		req = req.WithContext(ctx)
   212  	}
   213  	return client.httpClient.Do(req)
   214  }
   215  
   216  func (client *Client) get(ctx context.Context, base *url.URL, endpoint string, query url.Values) (*http.Response, error) {
   217  	req, err := http.NewRequest(http.MethodGet, client.url(base, endpoint), nil)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	if query != nil {
   222  		req.URL.RawQuery = query.Encode()
   223  	}
   224  	return client.do(ctx, req)
   225  }
   226  
   227  func (client *Client) post(ctx context.Context, endpoint string, body io.Reader) (*http.Response, error) {
   228  	req, err := http.NewRequest(http.MethodPost, client.url(client.endpointBase, endpoint), body)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	req.Header.Set("Content-Type", "application/json; charset=UTF-8")
   233  	return client.do(ctx, req)
   234  }
   235  
   236  func (client *Client) postForm(ctx context.Context, endpoint string, body io.Reader) (*http.Response, error) {
   237  	req, err := http.NewRequest("POST", client.url(client.endpointBase, endpoint), body)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   242  	return client.do(ctx, req)
   243  }
   244  
   245  func (client *Client) postFormFile(ctx context.Context, endpoint string, values map[string]io.Reader) (*http.Response, error) {
   246  	b, contentType, err := uploadFile(values)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	req, err := http.NewRequest(http.MethodPost, client.url(client.endpointBaseData, endpoint), &b)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  	req.Header.Set("Content-Type", contentType)
   255  	return client.do(ctx, req)
   256  }
   257  
   258  func (client *Client) put(ctx context.Context, endpoint string, body io.Reader) (*http.Response, error) {
   259  	req, err := http.NewRequest(http.MethodPut, client.url(client.endpointBase, endpoint), body)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	req.Header.Set("Content-Type", "application/json; charset=UTF-8")
   264  	return client.do(ctx, req)
   265  }
   266  
   267  func (client *Client) putFormFile(ctx context.Context, endpoint string, values map[string]io.Reader) (*http.Response, error) {
   268  	b, contentType, err := uploadFile(values)
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	req, err := http.NewRequest(http.MethodPut, client.url(client.endpointBaseData, endpoint), &b)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	req.Header.Set("Content-Type", contentType)
   277  	return client.do(ctx, req)
   278  }
   279  
   280  func (client *Client) delete(ctx context.Context, endpoint string) (*http.Response, error) {
   281  	req, err := http.NewRequest(http.MethodDelete, client.url(client.endpointBase, endpoint), nil)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	return client.do(ctx, req)
   286  }
   287  
   288  func (client *Client) setRetryKey(retryKey string) {
   289  	client.retryKeyID = retryKey
   290  }
   291  
   292  func closeResponse(res *http.Response) error {
   293  	defer res.Body.Close()
   294  	_, err := io.Copy(io.Discard, res.Body)
   295  	return err
   296  }
   297  
   298  func uploadFile(values map[string]io.Reader) (bytes.Buffer, string, error) {
   299  	var (
   300  		b   bytes.Buffer
   301  		err error
   302  	)
   303  	w := multipart.NewWriter(&b)
   304  	for key, r := range values {
   305  		var fw io.Writer
   306  		if x, ok := r.(io.Closer); ok {
   307  			defer x.Close()
   308  		}
   309  		if _, ok := r.(*bytes.Buffer); ok {
   310  			h := make(textproto.MIMEHeader)
   311  			h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s.txt"`, key, time.Now().Format("20060102150405")))
   312  			h.Set("Content-Type", "text/plain")
   313  			if fw, err = w.CreatePart(h); err != nil {
   314  				return b, "", err
   315  			}
   316  		} else {
   317  			if fw, err = w.CreateFormField(key); err != nil {
   318  				return b, "", err
   319  			}
   320  		}
   321  		if _, err := io.Copy(fw, r); err != nil {
   322  			return b, "", err
   323  		}
   324  	}
   325  	w.Close()
   326  	return b, w.FormDataContentType(), nil
   327  }