gitee.com/larksuite/oapi-sdk-go/v3@v3.0.3/core/reqtranslator.go (about)

     1  /*
     2   * MIT License
     3   *
     4   * Copyright (c) 2022 Lark Technologies Pte. Ltd.
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
     7   *
     8   * The above copyright notice and this permission notice, shall be included in all copies or substantial portions of the Software.
     9   *
    10   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    11   */
    12  
    13  package larkcore
    14  
    15  import (
    16  	"bytes"
    17  	"context"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"mime/multipart"
    23  	"net/http"
    24  	"net/url"
    25  	"reflect"
    26  	"strings"
    27  )
    28  
    29  type ReqTranslator struct {
    30  }
    31  
    32  func (translator *ReqTranslator) translate(ctx context.Context, req *ApiReq, accessTokenType AccessTokenType, config *Config, option *RequestOption) (*http.Request, error) {
    33  	body := req.Body
    34  	if _, ok := body.(*Formdata); !ok {
    35  		if option.FileUpload {
    36  			body = toFormdata(body)
    37  		}
    38  	} else {
    39  		option.FileUpload = true
    40  	}
    41  
    42  	contentType, rawBody, err := translator.payload(body)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	// path
    48  	var pathSegs []string
    49  	for _, p := range strings.Split(req.ApiPath, "/") {
    50  		if strings.Index(p, ":") == 0 {
    51  			varName := p[1:]
    52  			v, ok := req.PathParams[varName]
    53  			if !ok {
    54  				return nil, fmt.Errorf("http path:%s, name: %s, not found value", req.ApiPath, varName)
    55  			}
    56  			val := fmt.Sprint(v)
    57  			if val == "" {
    58  				return nil, fmt.Errorf("http path:%s, name: %s, value is empty", req.ApiPath, varName)
    59  			}
    60  			val = url.PathEscape(val)
    61  			pathSegs = append(pathSegs, val)
    62  			continue
    63  		}
    64  		pathSegs = append(pathSegs, p)
    65  	}
    66  	newPath := strings.Join(pathSegs, "/")
    67  	if strings.Index(newPath, "http") != 0 {
    68  		newPath = fmt.Sprintf("%s%s", config.BaseUrl, newPath)
    69  	}
    70  
    71  	queryPath := req.QueryParams.Encode()
    72  	if queryPath != "" {
    73  		newPath = fmt.Sprintf("%s?%s", newPath, queryPath)
    74  	}
    75  
    76  	req1, err := translator.newHTTPRequest(ctx, req.HttpMethod, newPath, contentType, rawBody, accessTokenType, option, config)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	return req1, nil
    81  }
    82  
    83  func (translator *ReqTranslator) translateOld(ctx context.Context, input interface{}, tokenType AccessTokenType, config *Config, httpMethod, httpPath string, option *RequestOption) (*http.Request, error) {
    84  	paths, queries, body := translator.parseInput(input, option)
    85  	if _, ok := body.(*Formdata); ok {
    86  		option.FileUpload = true
    87  	}
    88  
    89  	contentType, rawBody, err := translator.payload(body)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	fullURL, err := translator.getFullReqUrl(config.BaseUrl, httpPath, paths, queries)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	req, err := translator.newHTTPRequest(ctx, httpMethod, fullURL, contentType, rawBody, tokenType, option, config)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	return req, nil
   104  }
   105  
   106  func authorizationToHeader(req *http.Request, token string) {
   107  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
   108  }
   109  
   110  func (translator *ReqTranslator) newHTTPRequest(ctx context.Context,
   111  	httpMethod, url, contentType string, body []byte,
   112  	accessTokenType AccessTokenType, option *RequestOption, config *Config) (*http.Request, error) {
   113  	httpRequest, err := http.NewRequestWithContext(ctx, httpMethod, url, bytes.NewBuffer(body))
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	if option.RequestId != "" {
   118  		httpRequest.Header.Add(customRequestId, option.RequestId)
   119  	}
   120  	for k, vs := range option.Header {
   121  		for _, v := range vs {
   122  			httpRequest.Header.Add(k, v)
   123  		}
   124  	}
   125  	httpRequest.Header.Set(userAgentHeader, userAgent())
   126  	if contentType != "" {
   127  		httpRequest.Header.Set(contentTypeHeader, contentType)
   128  	}
   129  	switch accessTokenType {
   130  	case AccessTokenTypeApp:
   131  		appAccessToken := option.AppAccessToken
   132  		if config.EnableTokenCache && appAccessToken == "" {
   133  			appAccessToken, err = tokenManager.getAppAccessToken(ctx, config, option.AppTicket)
   134  			if err != nil {
   135  				return nil, err
   136  			}
   137  		}
   138  		authorizationToHeader(httpRequest, appAccessToken)
   139  
   140  	case AccessTokenTypeTenant:
   141  		tenantAccessToken := option.TenantAccessToken
   142  		if config.EnableTokenCache {
   143  			tenantAccessToken, err = tokenManager.getTenantAccessToken(ctx, config, option.TenantKey, option.AppTicket)
   144  			if err != nil {
   145  				return nil, err
   146  			}
   147  		}
   148  		authorizationToHeader(httpRequest, tenantAccessToken)
   149  
   150  	case AccessTokenTypeUser:
   151  		authorizationToHeader(httpRequest, option.UserAccessToken)
   152  	}
   153  
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	err = translator.signHelpdeskAuthToken(httpRequest, option.NeedHelpDeskAuth, config.HelpdeskAuthToken)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	return httpRequest, nil
   163  }
   164  
   165  func (translator *ReqTranslator) signHelpdeskAuthToken(rawRequest *http.Request, needHelpDeskAuth bool, authToken string) error {
   166  	if needHelpDeskAuth {
   167  		if authToken == "" {
   168  			return errors.New("help desk API, please set the helpdesk information of lark.App")
   169  		}
   170  		rawRequest.Header.Set("X-Lark-Helpdesk-Authorization", authToken)
   171  	}
   172  	return nil
   173  }
   174  
   175  func (translator *ReqTranslator) getFullReqUrl(domain string, httpPath string, pathVars, queries map[string]interface{}) (string, error) {
   176  	// path
   177  	var pathSegs []string
   178  	for _, p := range strings.Split(httpPath, "/") {
   179  		if strings.Index(p, ":") == 0 {
   180  			varName := p[1:]
   181  			v, ok := pathVars[varName]
   182  			if !ok {
   183  				return "", fmt.Errorf("http path:%s, name: %s, not found value", httpPath, varName)
   184  			}
   185  			val := fmt.Sprint(v)
   186  			if val == "" {
   187  				return "", fmt.Errorf("http path:%s, name: %s, value is empty", httpPath, varName)
   188  			}
   189  			val = url.PathEscape(val)
   190  			pathSegs = append(pathSegs, val)
   191  			continue
   192  		}
   193  		pathSegs = append(pathSegs, p)
   194  	}
   195  	newPath := strings.Join(pathSegs, "/")
   196  	if strings.Index(newPath, "http") != 0 {
   197  		newPath = fmt.Sprintf("%s%s", domain, newPath)
   198  	}
   199  	// query
   200  	query := make(url.Values)
   201  	for k, v := range queries {
   202  		sv := reflect.ValueOf(v)
   203  		if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
   204  			for i := 0; i < sv.Len(); i++ {
   205  				query.Add(k, fmt.Sprint(sv.Index(i)))
   206  			}
   207  		} else {
   208  			query.Set(k, fmt.Sprint(v))
   209  		}
   210  	}
   211  	if len(query) > 0 {
   212  		newPath = fmt.Sprintf("%s?%s", newPath, query.Encode())
   213  	}
   214  	return newPath, nil
   215  }
   216  
   217  func (translator *ReqTranslator) payload(body interface{}) (string, []byte, error) {
   218  	if fd, ok := body.(*Formdata); ok {
   219  		return fd.content()
   220  	}
   221  	contentType := defaultContentType
   222  	if body == nil {
   223  		return contentType, nil, nil
   224  	}
   225  	bs, err := json.Marshal(body)
   226  	return contentType, bs, err
   227  }
   228  
   229  func NewFormdata() *Formdata {
   230  	return &Formdata{}
   231  }
   232  
   233  func (fd *Formdata) AddField(field string, val interface{}) *Formdata {
   234  	if fd.fields == nil {
   235  		fd.fields = map[string]interface{}{}
   236  	}
   237  	fd.fields[field] = val
   238  	return fd
   239  }
   240  
   241  func (fd *Formdata) AddFile(field string, r io.Reader) *Formdata {
   242  	return fd.AddField(field, r)
   243  }
   244  
   245  func (fd *Formdata) content() (string, []byte, error) {
   246  	if fd.data != nil {
   247  		return fd.data.contentType, fd.data.content, nil
   248  	}
   249  	buf := &bytes.Buffer{}
   250  	writer := multipart.NewWriter(buf)
   251  	for key, val := range fd.fields {
   252  		if r, ok := val.(io.Reader); ok {
   253  			part, err := writer.CreateFormFile(key, "unknown-file")
   254  			if err != nil {
   255  				return "", nil, err
   256  			}
   257  			_, err = io.Copy(part, r)
   258  			if err != nil {
   259  				return "", nil, err
   260  			}
   261  			continue
   262  		}
   263  		err := writer.WriteField(key, fmt.Sprint(val))
   264  		if err != nil {
   265  			return "", nil, err
   266  		}
   267  	}
   268  	contentType := writer.FormDataContentType()
   269  	err := writer.Close()
   270  	if err != nil {
   271  		return "", nil, err
   272  	}
   273  	fd.data = &struct {
   274  		content     []byte
   275  		contentType string
   276  	}{content: buf.Bytes(), contentType: contentType}
   277  	return fd.data.contentType, fd.data.content, nil
   278  }
   279  
   280  type Formdata struct {
   281  	fields map[string]interface{}
   282  	data   *struct {
   283  		content     []byte
   284  		contentType string
   285  	}
   286  }
   287  
   288  func (translator *ReqTranslator) parseInput(input interface{}, option *RequestOption) (map[string]interface{}, map[string]interface{}, interface{}) {
   289  	if input == nil {
   290  		return nil, nil, nil
   291  	}
   292  	if _, ok := input.(*Formdata); ok {
   293  		return nil, nil, input
   294  	}
   295  	var hasHTTPTag bool
   296  	paths, queries := map[string]interface{}{}, map[string]interface{}{}
   297  	vv := reflect.ValueOf(input)
   298  	vt := reflect.TypeOf(input)
   299  	if vt.Kind() == reflect.Ptr {
   300  		vv = vv.Elem()
   301  		vt = vt.Elem()
   302  	}
   303  	if vt.Kind() != reflect.Struct {
   304  		return nil, nil, input
   305  	}
   306  	var body interface{}
   307  	for i := 0; i < vt.NumField(); i++ {
   308  		fieldValue := vv.Field(i)
   309  		fieldType := vt.Field(i)
   310  		if path, ok := fieldType.Tag.Lookup("path"); ok {
   311  			hasHTTPTag = true
   312  			if path != "" && !isEmptyValue(fieldValue) {
   313  				paths[path] = reflect.Indirect(fieldValue).Interface()
   314  			}
   315  			continue
   316  		}
   317  		if query, ok := fieldType.Tag.Lookup("query"); ok {
   318  			hasHTTPTag = true
   319  			if query != "" && !isEmptyValue(fieldValue) {
   320  				queries[query] = reflect.Indirect(fieldValue).Interface()
   321  			}
   322  			continue
   323  		}
   324  		if _, ok := fieldType.Tag.Lookup("body"); ok {
   325  			hasHTTPTag = true
   326  			body = fieldValue.Interface()
   327  		}
   328  	}
   329  	if !hasHTTPTag {
   330  		body = input
   331  		if option.FileUpload {
   332  			body = toFormdata(input)
   333  		}
   334  		return nil, nil, body
   335  	}
   336  	if body != nil {
   337  		if option.FileUpload {
   338  			body = toFormdata(body)
   339  		}
   340  	}
   341  	return paths, queries, body
   342  }
   343  
   344  func toFormdata(body interface{}) *Formdata {
   345  	formdata := &Formdata{}
   346  	v := reflect.ValueOf(body)
   347  	t := reflect.TypeOf(body)
   348  	if t.Kind() == reflect.Ptr {
   349  		v = v.Elem()
   350  		t = t.Elem()
   351  	}
   352  	for i := 0; i < t.NumField(); i++ {
   353  		fieldValue := v.Field(i)
   354  		fieldType := t.Field(i)
   355  		if isEmptyValue(fieldValue) {
   356  			continue
   357  		}
   358  		if fieldName := fieldType.Tag.Get("json"); fieldName != "" {
   359  			fieldName = strings.TrimSuffix(fieldName, ",omitempty")
   360  			formdata.AddField(fieldName, reflect.Indirect(fieldValue).Interface())
   361  		}
   362  	}
   363  	return formdata
   364  }