ldf
3 years ago
11 changed files with 1216 additions and 0 deletions
-
5cmd/conf/cfg.toml
-
32cmd/conf/log.yml
-
0cmd/log/asana.error
-
480cmd/log/asana.log
-
5go.mod
-
58go.sum
-
202module/attach/attachments.go
-
127module/story/stories.go
-
107module/task/task.go
-
16util/constant.go
-
184util/http.go
@ -0,0 +1,5 @@ |
|||
|
|||
[logger] |
|||
conf = "./conf/log.yml" |
|||
path = "./log/" |
|||
app_name = "asana" |
@ -0,0 +1,32 @@ |
|||
|
|||
level: "info" |
|||
development: false |
|||
disableCaller: false |
|||
disableStacktrace: false |
|||
sampling: |
|||
encoding: "console" |
|||
|
|||
# encoder |
|||
encoderConfig: |
|||
messageKey: "message" |
|||
levelKey: "level" |
|||
timeKey: "time" |
|||
nameKey: "logger" |
|||
callerKey: "caller" |
|||
stacktraceKey: "stacktrace" |
|||
lineEnding: "" |
|||
# levelEncoder: "capitalColor" |
|||
timeEncoder: "iso8601" |
|||
durationEncoder: "seconds" |
|||
callerEncoder: "short" |
|||
nameEncoder: "" |
|||
|
|||
outputPaths: |
|||
# - "../logs/yuanex.log" |
|||
# - "./logs/yuanex.log" |
|||
- "stderr" |
|||
errorOutputPaths: |
|||
# - "../logs/yuanex.error" |
|||
# - "./logs/yuanex.error" |
|||
- "stderr" |
|||
initialFields: |
480
cmd/log/asana.log
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,5 @@ |
|||
module asana |
|||
|
|||
go 1.15 |
|||
|
|||
require git.drinkme.beer/yinghe/log v0.1.3 |
@ -0,0 +1,58 @@ |
|||
git.drinkme.beer/yinghe/log v0.1.3 h1:QwHVLEqPo5nF/uucrmJJPSl9OXgjrx4JUzKieRfsOes= |
|||
git.drinkme.beer/yinghe/log v0.1.3/go.mod h1:4PAIGcEDUqVkKYSFQEN65+R6QWSsjvbfkIoEhGThGH8= |
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= |
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= |
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= |
|||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= |
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= |
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= |
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
|||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= |
|||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= |
|||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= |
|||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= |
|||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= |
|||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= |
|||
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= |
|||
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= |
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
|||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= |
|||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= |
|||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= |
|||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
|||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= |
|||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= |
|||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
|||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= |
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= |
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= |
|||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= |
|||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= |
@ -0,0 +1,202 @@ |
|||
package attach |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/json" |
|||
"errors" |
|||
"fmt" |
|||
"io" |
|||
"mime/multipart" |
|||
"net/http" |
|||
"net/textproto" |
|||
"os" |
|||
"strings" |
|||
|
|||
"git.drinkme.beer/yinghe/log" |
|||
|
|||
"asana/util" |
|||
) |
|||
|
|||
const ( |
|||
URI = "/api/1.0/tasks/%s/attachments" |
|||
) |
|||
|
|||
type Request struct { |
|||
Body io.Reader |
|||
TaskID string |
|||
Name string |
|||
Path string |
|||
PAToken string |
|||
TicketID string |
|||
} |
|||
|
|||
type Data struct { |
|||
Response Response `json:"data"` |
|||
} |
|||
|
|||
type Response struct { |
|||
ID string `json:"gid"` |
|||
ResourceType string `json:"resource_type"` |
|||
Name string `json:"name"` |
|||
// ResourceSubtype - asana, dropbox, gdrive, onedrive, box, and external
|
|||
ResourceSubtype string `json:"resource_subtype"` |
|||
} |
|||
|
|||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") |
|||
|
|||
func escapeQuotes(s string) string { |
|||
return quoteEscaper.Replace(s) |
|||
} |
|||
|
|||
// Create - upload attachments
|
|||
func (r Request) Create() (resp *Response, err error) { |
|||
var ( |
|||
tempPath string |
|||
data struct { |
|||
Response Response `json:"data"` |
|||
} |
|||
) |
|||
tempPath = "./static/" + r.Name |
|||
|
|||
// TODO
|
|||
err = download(r.Path, tempPath) |
|||
if nil != err { |
|||
return nil, err |
|||
} |
|||
|
|||
attachments, _ := os.Open(tempPath) |
|||
defer func() { |
|||
err := attachments.Close() |
|||
if nil != err { |
|||
log.Errorf("failed to close temporary file: %s", err.Error()) |
|||
} |
|||
|
|||
err = os.Remove(tempPath) |
|||
if nil != err { |
|||
log.Errorf("failed to remove temporary file: %s", err.Error()) |
|||
} |
|||
}() |
|||
|
|||
// Step 1. Try to determine the contentType.
|
|||
contentType, body, err := fDetectContentType(attachments) |
|||
if nil != err { |
|||
log.Errorf("failed to detect content type: %s", err.Error()) |
|||
return |
|||
} |
|||
|
|||
if nil == body { |
|||
log.Errorf("empty attachment") |
|||
return nil, errors.New("generating attachment failed") |
|||
} |
|||
|
|||
// Step 2:
|
|||
// Initiate and then make the upload.
|
|||
prc, pwc := io.Pipe() |
|||
mpartW := multipart.NewWriter(pwc) |
|||
go func() { |
|||
defer func() { |
|||
_ = mpartW.Close() |
|||
_ = pwc.Close() |
|||
}() |
|||
h := make(textproto.MIMEHeader) |
|||
h.Set("Content-Disposition", |
|||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`, |
|||
escapeQuotes("file"), escapeQuotes(r.Name))) |
|||
//h.Set("Content-Type", "application/octet-stream")
|
|||
log.Infof("Content-Type: %s", contentType) |
|||
h.Set("Content-Type", contentType) |
|||
formFile, err := mpartW.CreatePart(h) |
|||
//formFile, err := mpartW.CreateFormFile("file", r.Name)
|
|||
if err != nil { |
|||
return |
|||
} |
|||
_, _ = io.Copy(formFile, body) |
|||
//writeStringField(mpartW, "Content-Type", contentType)
|
|||
//writeStringField(mpartW, "resource_subtype", "asana")
|
|||
//writeStringField(mpartW, "name", "20210811065901")
|
|||
}() |
|||
|
|||
fullURL := fmt.Sprintf("%s/api/1.0/tasks/%s/attachments", util.AsanaHost, r.TaskID) |
|||
|
|||
req, err := http.NewRequest("POST", fullURL, prc) |
|||
if err != nil { |
|||
log.Errorf("failed to build attachment request: %s", err.Error()) |
|||
return nil, err |
|||
} |
|||
req.Header.Set("Content-Type", mpartW.FormDataContentType()) |
|||
req.Header.Set("Authorization", r.PAToken) |
|||
|
|||
buf, err := util.Request(req) |
|||
if nil != err { |
|||
return nil, err |
|||
} |
|||
err = json.Unmarshal(buf, &data) |
|||
if nil != err { |
|||
log.Errorf("[%s][%s]failed to upload attachments: %s", r.TicketID, r.TaskID, err.Error()) |
|||
return nil, err |
|||
} |
|||
return &data.Response, err |
|||
} |
|||
|
|||
func fDetectContentType(r io.Reader) (string, io.Reader, error) { |
|||
if r == nil { |
|||
return "", nil, errors.New("empty attachments") |
|||
} |
|||
|
|||
seeker, seekable := r.(io.Seeker) |
|||
sniffBuf := make([]byte, 512) |
|||
n, err := io.ReadAtLeast(r, sniffBuf, 1) |
|||
if err != nil { |
|||
log.Errorf(err.Error()) |
|||
return "", nil, err |
|||
} |
|||
|
|||
contentType := http.DetectContentType(sniffBuf) |
|||
needsRepad := !seekable |
|||
if seekable { |
|||
if _, err = seeker.Seek(int64(-n), io.SeekCurrent); err != nil { |
|||
// Since we failed to rewind it, mark it as needing repad
|
|||
needsRepad = true |
|||
} |
|||
} |
|||
|
|||
if needsRepad { |
|||
r = io.MultiReader(bytes.NewReader(sniffBuf), r) |
|||
} |
|||
|
|||
return contentType, r, nil |
|||
} |
|||
|
|||
func writeStringField(w *multipart.Writer, key, value string) { |
|||
fw, err := w.CreateFormField(key) |
|||
if err == nil { |
|||
_, _ = io.WriteString(fw, value) |
|||
} |
|||
} |
|||
|
|||
var ( |
|||
errValidation = errors.New("invalid request") |
|||
) |
|||
|
|||
func download(src, filename string) error { |
|||
if "" == src || "" == filename { |
|||
return errValidation |
|||
} |
|||
f, err := os.Create(filename) |
|||
if nil != err { |
|||
log.Errorf("failed to create attachments") |
|||
return errors.New("failed to create file") |
|||
} |
|||
defer f.Close() |
|||
//defer os.Remove(filename)
|
|||
|
|||
resp, err := http.Get(src) |
|||
if nil != err { |
|||
log.Infof("download report failed: %s", err.Error()) |
|||
return err |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
_, err = io.Copy(f, resp.Body) |
|||
return err |
|||
} |
@ -0,0 +1,127 @@ |
|||
package story |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"errors" |
|||
"fmt" |
|||
"net/http" |
|||
|
|||
"git.drinkme.beer/yinghe/log" |
|||
|
|||
"asana/util" |
|||
) |
|||
|
|||
const ( |
|||
URI = "/api/1.0/tasks/%s/stories" |
|||
) |
|||
|
|||
type Data struct { |
|||
Request Request `json:"data"` |
|||
} |
|||
|
|||
type Request struct { |
|||
HtmlText string `json:"html_text,omitempty"` |
|||
Text string `json:"text"` |
|||
StickerName string `json:"sticker_name,omitempty"` |
|||
IsPinned bool `json:"is_pinned"` |
|||
|
|||
taskID string |
|||
ticketID string |
|||
paToken string |
|||
} |
|||
|
|||
func (r *Request) GetTaskID() string { |
|||
return r.taskID |
|||
} |
|||
|
|||
func (r *Request) SetTaskID(id string) { |
|||
r.taskID = id |
|||
} |
|||
|
|||
func (r *Request) GetTicketID() string { |
|||
return r.ticketID |
|||
} |
|||
|
|||
func (r *Request) SetTicketID(id string) { |
|||
r.ticketID = id |
|||
} |
|||
|
|||
func (r *Request) GetPAToken() string { |
|||
return r.paToken |
|||
} |
|||
|
|||
func (r *Request) SetPAToken(token string) { |
|||
r.paToken = token |
|||
} |
|||
|
|||
type Response struct { |
|||
ID string `json:"gid"` |
|||
ResourceType string `json:"resource_type"` |
|||
Type string `json:"type"` |
|||
Text string `json:"text"` |
|||
// IsPinned Conditional
|
|||
// Whether the story should be pinned on the resource.
|
|||
IsPinned bool `json:"is_pinned"` |
|||
StickerName string `json:"sticker_name,omitempty"` |
|||
} |
|||
|
|||
func (r Request) validate() error { |
|||
if "" == r.taskID { |
|||
log.Errorf("task id is empty") |
|||
return errors.New("task id is empty") |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// Create comments on task
|
|||
func (r Request) Create() (resp *Response, err error) { |
|||
var ( |
|||
reqData Data |
|||
respData struct { |
|||
Response Response `json:"data"` |
|||
} |
|||
) |
|||
if err = r.validate(); nil != err { |
|||
return |
|||
} |
|||
|
|||
resp = new(Response) |
|||
|
|||
headers := make(map[string]string) |
|||
headers["Authorization"] = r.paToken |
|||
headers["Content-Type"] = util.ContentType |
|||
|
|||
reqData.Request = r |
|||
|
|||
buf, err := json.Marshal(&reqData) |
|||
if nil != err { |
|||
log.Errorf("failed to generate comment request: %s", err.Error()) |
|||
return |
|||
} |
|||
|
|||
log.Infof("request: %s", string(buf)) |
|||
|
|||
client := util.NewHttpClient(util.AsanaHost, fmt.Sprintf(URI, r.taskID), util.HttpPostMethod, buf) |
|||
client.Headers = headers |
|||
|
|||
err = client.Request() |
|||
if nil != err { |
|||
log.Errorf("failed to create new task comment: %s", err.Error()) |
|||
return |
|||
} |
|||
log.Infof("response status: %d", client.HTTPStatus) |
|||
if http.StatusCreated != client.HTTPStatus { |
|||
log.Errorf("unexpected response") |
|||
err = errors.New("unexpected response") |
|||
return |
|||
} |
|||
|
|||
err = json.Unmarshal(client.Body, &respData) |
|||
if nil != err { |
|||
log.Errorf("illegal comment result: %s", err.Error()) |
|||
return |
|||
} |
|||
resp = &respData.Response |
|||
log.Infof("[%s]new comment ID:%s", r.taskID, resp.ID) |
|||
return |
|||
} |
@ -0,0 +1,107 @@ |
|||
package task |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"errors" |
|||
"net/http" |
|||
|
|||
"git.drinkme.beer/yinghe/log" |
|||
|
|||
"asana/util" |
|||
) |
|||
|
|||
type Data struct { |
|||
Request Request `json:"data"` |
|||
} |
|||
|
|||
type Request struct { |
|||
ResourceSubtype string `json:"resource_subtype"` |
|||
Assignee string `json:"assignee"` |
|||
Name string `json:"name"` |
|||
Completed bool `json:"completed"` |
|||
DueOn string `json:"due_on"` |
|||
Liked bool `json:"linked"` |
|||
// Notes refer to the content of each Ticket
|
|||
Notes string `json:"notes"` |
|||
StartOn string `json:"start_on"` |
|||
CustomFields map[string]string `json:"custom_fields"` |
|||
Projects []string `json:"projects"` |
|||
Workspace string `json:"workspace"` |
|||
|
|||
TicketID string `json:"-"` |
|||
TicketType string `json:"-"` |
|||
paToken string |
|||
} |
|||
|
|||
func (r *Request) GetPAToken() string { |
|||
return r.paToken |
|||
} |
|||
|
|||
func (r *Request) SetPAToken(token string) { |
|||
r.paToken = token |
|||
} |
|||
|
|||
type Response struct { |
|||
ID string `json:"gid"` |
|||
Name string `json:"name"` |
|||
ResourceType string `json:"resource_type"` |
|||
AssigneeStatus string `json:"assignee_status"` |
|||
} |
|||
|
|||
const ( |
|||
URI = "/api/1.0/tasks" |
|||
ResourceSubtype = "default_task" |
|||
) |
|||
|
|||
func (r Request) Create() (resp *Response, err error) { |
|||
var ( |
|||
reqData Data |
|||
respData struct { |
|||
Response Response `json:"data"` |
|||
} |
|||
) |
|||
|
|||
resp = new(Response) |
|||
|
|||
headers := make(map[string]string) |
|||
headers["Authorization"] = r.paToken |
|||
headers["Content-Type"] = util.ContentType |
|||
|
|||
if "" == r.ResourceSubtype { |
|||
r.ResourceSubtype = ResourceSubtype |
|||
} |
|||
|
|||
reqData.Request = r |
|||
|
|||
buf, err := json.Marshal(&reqData) |
|||
if nil != err { |
|||
log.Errorf("failed to generate task request: %s", err.Error()) |
|||
return |
|||
} |
|||
|
|||
log.Infof("request: %s", string(buf)) |
|||
|
|||
client := util.NewHttpClient(util.AsanaHost, URI, util.HttpPostMethod, buf) |
|||
client.Headers = headers |
|||
|
|||
err = client.Request() |
|||
if nil != err { |
|||
log.Errorf("failed to create new Asana task: %s", err.Error()) |
|||
return |
|||
} |
|||
log.Infof("response status: %d", client.HTTPStatus) |
|||
if http.StatusCreated != client.HTTPStatus { |
|||
log.Errorf("unexpected response") |
|||
err = errors.New("unexpected response") |
|||
return |
|||
} |
|||
|
|||
err = json.Unmarshal(client.Body, &respData) |
|||
if nil != err { |
|||
log.Errorf("illegal task result: %s", err.Error()) |
|||
return |
|||
} |
|||
resp = &respData.Response |
|||
log.Infof("[%s]new task ID:%s", r.TicketID, resp.ID) |
|||
return |
|||
} |
@ -0,0 +1,16 @@ |
|||
package util |
|||
|
|||
const ( |
|||
AsanaHost = "https://app.asana.com" |
|||
|
|||
ContentType = "application/json" |
|||
|
|||
HttpGetMethod = "GET" |
|||
HttpPostMethod = "POST" |
|||
|
|||
AsanaDateFormat = "2006-01-02" |
|||
) |
|||
|
|||
var ( |
|||
AsanaHeaders = make(map[string]string) |
|||
) |
@ -0,0 +1,184 @@ |
|||
package util |
|||
|
|||
import ( |
|||
"bufio" |
|||
"bytes" |
|||
"context" |
|||
"errors" |
|||
//"fmt"
|
|||
"io/ioutil" |
|||
"net" |
|||
"net/http" |
|||
"time" |
|||
|
|||
"git.drinkme.beer/yinghe/log" |
|||
) |
|||
|
|||
// Network contains common configuration.
|
|||
type Network struct { |
|||
Env string `toml:"env"` |
|||
Host []string `toml:"gateway"` |
|||
GatewayURL string |
|||
RequestTimeout time.Duration `toml:"request_timeout"` |
|||
ConnectTimeout time.Duration `toml:"connect_timeout"` |
|||
SocketTimeout time.Duration `toml:"socket_timeout"` |
|||
} |
|||
|
|||
var network = Network{ |
|||
RequestTimeout: 45 * time.Second, |
|||
ConnectTimeout: 45 * time.Second, |
|||
SocketTimeout: 55 * time.Second, |
|||
} |
|||
|
|||
var client = &http.Client{ |
|||
Transport: &http.Transport{ |
|||
DialContext: func(ctx context.Context, n, addr string) (net.Conn, error) { |
|||
conn, err := net.DialTimeout(n, addr, |
|||
time.Second*network.RequestTimeout, |
|||
) |
|||
if err != nil { |
|||
return conn, err |
|||
} |
|||
conn.SetDeadline(time.Now(). |
|||
Add(time.Second * network.ConnectTimeout)) |
|||
return conn, err |
|||
}, |
|||
ResponseHeaderTimeout: time.Second * network.SocketTimeout, |
|||
MaxConnsPerHost: 20, |
|||
IdleConnTimeout: 3 * time.Minute, |
|||
MaxIdleConns: 10, |
|||
//Proxy: func(_ *http.Request) (*url.URL, error) {
|
|||
// return url.Parse("http://192.168.0.104:1087")
|
|||
//},
|
|||
}, |
|||
} |
|||
|
|||
func SetNetworkCfg(cfg Network) { |
|||
if 0 < cfg.RequestTimeout { |
|||
network.RequestTimeout = cfg.RequestTimeout |
|||
} |
|||
|
|||
if 0 < cfg.ConnectTimeout { |
|||
network.ConnectTimeout = cfg.ConnectTimeout |
|||
} |
|||
|
|||
if 0 < cfg.SocketTimeout { |
|||
network.SocketTimeout = cfg.SocketTimeout |
|||
} |
|||
} |
|||
|
|||
type Body interface { |
|||
BuildRequest() []byte |
|||
} |
|||
|
|||
type Client struct { |
|||
GatewayURL string |
|||
URI string |
|||
Headers map[string]string |
|||
Authorization string |
|||
Method string |
|||
HTTPStatus int |
|||
Body []byte // Indicates both Request Body & Response Body
|
|||
TraceId interface{} |
|||
Buffer *bufio.Reader |
|||
} |
|||
|
|||
func (c *Client) AddHeader(k, v string) { |
|||
c.Headers[k] = v |
|||
} |
|||
|
|||
type Bytes []byte |
|||
|
|||
func (self Bytes) BuildRequest() []byte { |
|||
return self |
|||
} |
|||
|
|||
/* |
|||
url, uri, method, body |
|||
*/ |
|||
func NewHttpClient(url, uri, m string, b []byte) *Client { |
|||
return &Client{ |
|||
GatewayURL: url, |
|||
URI: uri, |
|||
Method: m, |
|||
Headers: make(map[string]string), |
|||
Body: b, |
|||
} |
|||
} |
|||
|
|||
func (c *Client) BuildRequest(body Body) { |
|||
if nil == c.Body { |
|||
c.Body = make([]byte, 0, 1024) |
|||
} |
|||
c.Body = append(c.Body, body.BuildRequest()...) |
|||
} |
|||
|
|||
func (c *Client) Request() (err error) { |
|||
log.Infof("request url: %s", c.GatewayURL+c.URI) |
|||
request, err := http.NewRequest(c.Method, |
|||
c.GatewayURL+c.URI, |
|||
bytes.NewReader(c.Body), |
|||
) |
|||
if err != nil { |
|||
log.Info("Post err occurs:", err.Error()) |
|||
return |
|||
} |
|||
|
|||
for k, v := range c.Headers { |
|||
request.Header.Set(k, v) |
|||
} |
|||
|
|||
resp, err := client.Do(request) |
|||
if nil == resp { |
|||
log.Info("none response received") |
|||
err = errors.New("none response received") |
|||
return |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
c.HTTPStatus = resp.StatusCode |
|||
c.Body, err = ioutil.ReadAll(resp.Body) |
|||
|
|||
log.Infof("response: %s", string(c.Body)) |
|||
return |
|||
} |
|||
|
|||
func Request(req *http.Request) ([]byte, error) { |
|||
var ( |
|||
err error |
|||
body []byte |
|||
) |
|||
|
|||
if nil == req { |
|||
log.Errorf("illegal request") |
|||
return nil, errors.New("illegal request") |
|||
} |
|||
log.Infof("%v", req) |
|||
resp, err := client.Do(req) |
|||
if nil == resp { |
|||
log.Info("none response received") |
|||
err = errors.New("none response received") |
|||
return nil, err |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
if http.StatusOK != resp.StatusCode { |
|||
if nil != resp.Body { |
|||
|
|||
body, _ = ioutil.ReadAll(resp.Body) |
|||
log.Infof("response: %s", string(body)) |
|||
} |
|||
log.Errorf("unexpected status: %d", resp.StatusCode) |
|||
return nil, errors.New("unexpected status") |
|||
} |
|||
|
|||
body, err = ioutil.ReadAll(resp.Body) |
|||
if nil != err { |
|||
log.Errorf("invalid response body: %s", err.Error()) |
|||
return nil, err |
|||
} |
|||
|
|||
log.Infof("response: %s", string(body)) |
|||
|
|||
return body, nil |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue