From fa0cfe1e33f9267531d75aa8757dbcbc9058a3da Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 4 Apr 2019 23:15:17 +0300 Subject: [PATCH 1/7] Add Status method for images client. Refs #4. --- .gitignore | 1 + images/client.go | 54 +++++++++++++++++++++++++++++++++++++++++++ images/client_test.go | 33 ++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 images/client.go create mode 100644 images/client_test.go diff --git a/.gitignore b/.gitignore index 70da7b7..5c1f39e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ cover.out vendor/ ngrok.yml +.envrc diff --git a/images/client.go b/images/client.go new file mode 100644 index 0000000..7372c00 --- /dev/null +++ b/images/client.go @@ -0,0 +1,54 @@ +package images + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type Client struct { + OAuthToken string + SkillID string + HTTPClient *http.Client +} + +func (c *Client) http() *http.Client { + if c.HTTPClient != nil { + return c.HTTPClient + } + return http.DefaultClient +} + +type StatusResponse struct { + Total int + Used int +} + +func (c *Client) Status() (*StatusResponse, error) { + req, err := http.NewRequest("GET", "https://dialogs.yandex.net/api/v1/status", nil) + if err != nil { + return nil, err + } + if c.OAuthToken != "" { + req.Header.Set("Authorization", "OAuth "+c.OAuthToken) + } + + resp, err := c.http().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("status code %d", resp.StatusCode) + } + + var res struct { + Images struct { + Quota StatusResponse + } + } + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil, err + } + return &res.Images.Quota, nil +} diff --git a/images/client_test.go b/images/client_test.go new file mode 100644 index 0000000..d431350 --- /dev/null +++ b/images/client_test.go @@ -0,0 +1,33 @@ +package images + +import ( + "os" + "testing" +) + +func TestClient(t *testing.T) { + if testing.Short() { + t.Skip("-short is passed, skipping integration test") + } + + token := os.Getenv("ALICE_TEST_OAUTH_TOKEN") + skill := os.Getenv("ALICE_TEST_SKILL_ID") + if token == "" || skill == "" { + t.Skip("`ALICE_TEST_OAUTH_TOKEN` or `ALICE_TEST_SKILL_ID` is not set, skipping integration test") + } + + c := Client{ + OAuthToken: token, + SkillID: skill, + } + + t.Run("Status", func(t *testing.T) { + status, err := c.Status() + if err != nil { + t.Fatal(err) + } + if status.Total != 104857600 { + t.Errorf("unexpected total %d", status.Total) + } + }) +} From d6271946fdcb1355d8575f3ff232710211012b26 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 4 Apr 2019 23:22:41 +0300 Subject: [PATCH 2/7] Run tests for all packages. --- .github/tests-go-1.12/entrypoint.sh | 2 +- .github/tests-go-master/entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/tests-go-1.12/entrypoint.sh b/.github/tests-go-1.12/entrypoint.sh index e50d7fa..84dd48c 100755 --- a/.github/tests-go-1.12/entrypoint.sh +++ b/.github/tests-go-1.12/entrypoint.sh @@ -2,6 +2,6 @@ set -eux -go test -v -coverprofile cover.out +go test -v -coverprofile cover.out ./... curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION diff --git a/.github/tests-go-master/entrypoint.sh b/.github/tests-go-master/entrypoint.sh index e50d7fa..84dd48c 100755 --- a/.github/tests-go-master/entrypoint.sh +++ b/.github/tests-go-master/entrypoint.sh @@ -2,6 +2,6 @@ set -eux -go test -v -coverprofile cover.out +go test -v -coverprofile cover.out ./... curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION From dba24dcad10a9a3885891160ffbf2dc228cc8aa3 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sun, 4 Aug 2019 13:40:31 +0300 Subject: [PATCH 3/7] Move client to resources package. --- .github/tests-go-1.12/entrypoint.sh | 2 +- .github/tests-go-master/entrypoint.sh | 2 +- .gitignore | 1 - {images => resources}/client.go | 4 ++-- {images => resources}/client_test.go | 19 +++++++++---------- 5 files changed, 13 insertions(+), 15 deletions(-) rename {images => resources}/client.go (98%) rename {images => resources}/client_test.go (60%) diff --git a/.github/tests-go-1.12/entrypoint.sh b/.github/tests-go-1.12/entrypoint.sh index 84dd48c..af30810 100755 --- a/.github/tests-go-1.12/entrypoint.sh +++ b/.github/tests-go-1.12/entrypoint.sh @@ -2,6 +2,6 @@ set -eux -go test -v -coverprofile cover.out ./... +go test -v -short -coverprofile cover.out ./... curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION diff --git a/.github/tests-go-master/entrypoint.sh b/.github/tests-go-master/entrypoint.sh index 84dd48c..af30810 100755 --- a/.github/tests-go-master/entrypoint.sh +++ b/.github/tests-go-master/entrypoint.sh @@ -2,6 +2,6 @@ set -eux -go test -v -coverprofile cover.out ./... +go test -v -short -coverprofile cover.out ./... curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION diff --git a/.gitignore b/.gitignore index 5c1f39e..70da7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ cover.out vendor/ ngrok.yml -.envrc diff --git a/images/client.go b/resources/client.go similarity index 98% rename from images/client.go rename to resources/client.go index 7372c00..982193d 100644 --- a/images/client.go +++ b/resources/client.go @@ -1,4 +1,4 @@ -package images +package resources import ( "encoding/json" @@ -7,8 +7,8 @@ import ( ) type Client struct { - OAuthToken string SkillID string + OAuthToken string HTTPClient *http.Client } diff --git a/images/client_test.go b/resources/client_test.go similarity index 60% rename from images/client_test.go rename to resources/client_test.go index d431350..0f6ba48 100644 --- a/images/client_test.go +++ b/resources/client_test.go @@ -1,8 +1,11 @@ -package images +package resources import ( "os" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestClient(t *testing.T) { @@ -10,10 +13,10 @@ func TestClient(t *testing.T) { t.Skip("-short is passed, skipping integration test") } - token := os.Getenv("ALICE_TEST_OAUTH_TOKEN") skill := os.Getenv("ALICE_TEST_SKILL_ID") - if token == "" || skill == "" { - t.Skip("`ALICE_TEST_OAUTH_TOKEN` or `ALICE_TEST_SKILL_ID` is not set, skipping integration test") + token := os.Getenv("ALICE_TEST_OAUTH_TOKEN") + if skill == "" || token == "" { + t.Skip("`ALICE_TEST_SKILL_ID` or `ALICE_TEST_OAUTH_TOKEN` is not set, skipping integration test") } c := Client{ @@ -23,11 +26,7 @@ func TestClient(t *testing.T) { t.Run("Status", func(t *testing.T) { status, err := c.Status() - if err != nil { - t.Fatal(err) - } - if status.Total != 104857600 { - t.Errorf("unexpected total %d", status.Total) - } + require.NoError(t, err) + assert.Equal(t, 104857600, status.Total) }) } From 144cb98eef33a5e9b607a17efdea264591bc184f Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sun, 4 Aug 2019 14:07:55 +0300 Subject: [PATCH 4/7] Expand README. --- .gitignore | 8 +++++++- README.md | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 70da7b7..c17f911 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -.envrc cover.out vendor/ + +.envrc ngrok.yml + +*.mp3 +*.ogg +*.opus +*.wav diff --git a/README.md b/README.md index d525671..90b8e30 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,30 @@ Package alice provides helpers for developing skills for Alice virtual assistant via Yandex.Dialogs platform. + +# Example + +```go +h := alice.NewHandler(func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) { + return &alice.ResponsePayload{ + Text: "Bye!", + EndSession: true, + }, nil +}) + +h.Errorf = log.Printf +http.Handle("/", h) + +const addr = "127.0.0.1:8080" +log.Printf("Listening on http://%s ...", addr) +log.Fatal(http.ListenAndServe(addr, nil)) +``` + +See [documentation](https://godoc.org/github.com/AlekSi/alice) and [examples](examples). + +# License + +Copyright (c) 2019 Alexey Palazhchenko. [MIT-style license](LICENSE). + +Tests use the following resources: +* https://freesound.org/people/prucanada/sounds/415341/ by prucanada. From 275761cb3818bbc9de047705aca2a18a39585c2c Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sun, 4 Aug 2019 14:10:27 +0300 Subject: [PATCH 5/7] Expand example. --- README.md | 5 +++-- doc_test.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 90b8e30..9bb07ce 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ via Yandex.Dialogs platform. # Example ```go -h := alice.NewHandler(func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) { +responder := func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) { return &alice.ResponsePayload{ Text: "Bye!", EndSession: true, }, nil -}) +} +h := alice.NewHandler(responder) h.Errorf = log.Printf http.Handle("/", h) diff --git a/doc_test.go b/doc_test.go index 16f0df5..7f9a8ac 100644 --- a/doc_test.go +++ b/doc_test.go @@ -9,13 +9,14 @@ import ( ) func Example() { - h := alice.NewHandler(func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) { + responder := func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) { return &alice.ResponsePayload{ Text: "Bye!", EndSession: true, }, nil - }) + } + h := alice.NewHandler(responder) h.Errorf = log.Printf http.Handle("/", h) From cfa7e79f8dd64b7b207e387d7fcb742008986594 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sun, 25 Aug 2019 14:11:35 +0300 Subject: [PATCH 6/7] WIP --- handler.go | 2 + resources/client.go | 184 +++++++++++++++++++++++++++++++++++---- resources/client_test.go | 40 +++++++-- 3 files changed, 203 insertions(+), 23 deletions(-) diff --git a/handler.go b/handler.go index f82fc5b..5fbe8c4 100644 --- a/handler.go +++ b/handler.go @@ -91,6 +91,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } req.Body = ioutil.NopCloser(&body) + req.ContentLength = int64(body.Len()) + req.TransferEncoding = nil } b, err := httputil.DumpRequest(req, true) diff --git a/resources/client.go b/resources/client.go index 982193d..38f18ca 100644 --- a/resources/client.go +++ b/resources/client.go @@ -1,27 +1,138 @@ package resources import ( + "bytes" "encoding/json" "fmt" + "io" + "io/ioutil" + "mime/multipart" "net/http" + "net/http/httputil" + "os" + "path/filepath" + "time" + + "github.com/AlekSi/alice" ) +type Quota struct { + Total int + Used int +} + +type Sound struct { + ID string + SkillID string + Size *int + OriginalName string + CreatedAt time.Time + IsProcessed bool + Error *string +} + type Client struct { SkillID string OAuthToken string HTTPClient *http.Client + + // debugging options + Debugf alice.Printf // debug logger + Indent bool // indent requests and responses + StrictDecoder bool // disallow unexpected fields in responses +} + +func (c *Client) do(req *http.Request, respBody interface{}) error { + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + var jsonRequst bool + if c.OAuthToken != "" { + req.Header.Set("Authorization", "OAuth "+c.OAuthToken) + } + if req.Body != nil && req.Header.Get("Content-Type") == "" { + jsonRequst = true + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + + if c.Debugf != nil { + if c.Indent && jsonRequst { + b, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + + var body bytes.Buffer + if err = json.Indent(&body, b, "", " "); err != nil { + return err + } + req.Body = ioutil.NopCloser(&body) + req.ContentLength = int64(body.Len()) + req.TransferEncoding = nil + } + + b, err := httputil.DumpRequestOut(req, jsonRequst) + if err != nil { + return err + } + c.debugf("Request:\n%s", b) + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() //nolint:errcheck + + if c.Debugf != nil { + if c.Indent { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + var body bytes.Buffer + if err = json.Indent(&body, b, "", " "); err != nil { + return err + } + resp.Body = ioutil.NopCloser(&body) + resp.ContentLength = int64(body.Len()) + resp.TransferEncoding = nil + } + + b, err := httputil.DumpResponse(resp, true) + if err != nil { + return err + } + c.debugf("Response:\n%s", b) + } + + if resp.StatusCode/100 != 2 { + return fmt.Errorf("status code %d", resp.StatusCode) + } + + d := json.NewDecoder(resp.Body) + if c.StrictDecoder { + d.DisallowUnknownFields() + } + return d.Decode(&respBody) } -func (c *Client) http() *http.Client { - if c.HTTPClient != nil { - return c.HTTPClient +func (c *Client) debugf(format string, a ...interface{}) { + if c.Debugf != nil { + c.Debugf(format, a...) } - return http.DefaultClient } type StatusResponse struct { - Total int - Used int + Images struct { + Quota Quota + } + Sounds struct { + Quota Quota + } } func (c *Client) Status() (*StatusResponse, error) { @@ -29,26 +140,65 @@ func (c *Client) Status() (*StatusResponse, error) { if err != nil { return nil, err } - if c.OAuthToken != "" { - req.Header.Set("Authorization", "OAuth "+c.OAuthToken) + + var res StatusResponse + if err = c.do(req, &res); err != nil { + return nil, err } + return &res, nil +} - resp, err := c.http().Do(req) +func (c *Client) UploadSound(name string, r io.Reader) (*Sound, error) { + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + fw, err := mw.CreateFormFile("file", name) if err != nil { return nil, err } - defer resp.Body.Close() - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("status code %d", resp.StatusCode) + if _, err = io.Copy(fw, r); err != nil { + return nil, err + } + if err = mw.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", "https://dialogs.yandex.net/api/v1/skills/"+c.SkillID+"/sounds", &buf) + if err != nil { + return nil, err } + req.Header.Add("Content-Type", mw.FormDataContentType()) var res struct { - Images struct { - Quota StatusResponse - } + Sound Sound + } + if err = c.do(req, &res); err != nil { + return nil, err + } + return &res.Sound, nil +} + +func (c *Client) UploadSoundFile(filename string) (*Sound, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() //nolint:errcheck + + return c.UploadSound(filepath.Base(filename), f) +} + +func (c *Client) ListSounds() ([]Sound, error) { + req, err := http.NewRequest("GET", "https://dialogs.yandex.net/api/v1/skills/"+c.SkillID+"/sounds", nil) + if err != nil { + return nil, err + } + + var res struct { + Sounds []Sound + Total int } - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + if err = c.do(req, &res); err != nil { return nil, err } - return &res.Images.Quota, nil + return res.Sounds, nil } diff --git a/resources/client_test.go b/resources/client_test.go index 0f6ba48..eb9e5a5 100644 --- a/resources/client_test.go +++ b/resources/client_test.go @@ -2,7 +2,9 @@ package resources import ( "os" + "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -13,20 +15,46 @@ func TestClient(t *testing.T) { t.Skip("-short is passed, skipping integration test") } - skill := os.Getenv("ALICE_TEST_SKILL_ID") - token := os.Getenv("ALICE_TEST_OAUTH_TOKEN") - if skill == "" || token == "" { + skillID := os.Getenv("ALICE_TEST_SKILL_ID") + oAuthToken := os.Getenv("ALICE_TEST_OAUTH_TOKEN") + if skillID == "" || oAuthToken == "" { t.Skip("`ALICE_TEST_SKILL_ID` or `ALICE_TEST_OAUTH_TOKEN` is not set, skipping integration test") } c := Client{ - OAuthToken: token, - SkillID: skill, + SkillID: skillID, + OAuthToken: oAuthToken, + Indent: true, + StrictDecoder: true, } t.Run("Status", func(t *testing.T) { + c.Debugf = t.Logf + status, err := c.Status() require.NoError(t, err) - assert.Equal(t, 104857600, status.Total) + assert.Equal(t, 104857600, status.Images.Quota.Total) + assert.Equal(t, 1073741824, status.Sounds.Quota.Total) + }) + + t.Run("Sound", func(t *testing.T) { + t.Run("UploadSoundFile", func(t *testing.T) { + c.Debugf = t.Logf + + sound, err := c.UploadSoundFile(filepath.Join("..", "testdata", "go.wav")) + require.NoError(t, err) + require.NotEmpty(t, sound) + assert.NotEmpty(t, skillID, sound.ID) + assert.Equal(t, skillID, sound.SkillID) + assert.Empty(t, sound.Size) + assert.Equal(t, "go.wav", sound.OriginalName) + assert.WithinDuration(t, time.Now(), sound.CreatedAt, 5*time.Second) + assert.False(t, sound.IsProcessed) + assert.Nil(t, sound.Error) + + sounds, err := c.ListSounds() + require.NoError(t, err) + assert.Empty(t, sounds) + }) }) } From acf749c96e476764e11243cf7f864c62d08f4f7d Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sun, 8 Dec 2019 14:01:29 +0300 Subject: [PATCH 7/7] Delete old workflows. --- .github/main.workflow | 29 --------------------------- .github/tests-go-1.12/Dockerfile | 8 -------- .github/tests-go-1.12/entrypoint.sh | 7 ------- .github/tests-go-master/Dockerfile | 8 -------- .github/tests-go-master/entrypoint.sh | 7 ------- 5 files changed, 59 deletions(-) delete mode 100644 .github/main.workflow delete mode 100644 .github/tests-go-1.12/Dockerfile delete mode 100755 .github/tests-go-1.12/entrypoint.sh delete mode 100644 .github/tests-go-master/Dockerfile delete mode 100755 .github/tests-go-master/entrypoint.sh diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index e946c38..0000000 --- a/.github/main.workflow +++ /dev/null @@ -1,29 +0,0 @@ -workflow "Push" { - on = "push" - resolves = ["Push: Go 1.12", "Push: Go master"] -} - -workflow "PR" { - on = "pull_request" - resolves = ["PR: Go 1.12", "PR: Go master"] -} - -action "Push: Go 1.12" { - uses = "./.github/tests-go-1.12" - secrets = ["CODECOV_TOKEN"] -} - -action "Push: Go master" { - uses = "./.github/tests-go-master" - secrets = ["CODECOV_TOKEN"] -} - -action "PR: Go 1.12" { - uses = "./.github/tests-go-1.12" - secrets = ["CODECOV_TOKEN"] -} - -action "PR: Go master" { - uses = "./.github/tests-go-master" - secrets = ["CODECOV_TOKEN"] -} diff --git a/.github/tests-go-1.12/Dockerfile b/.github/tests-go-1.12/Dockerfile deleted file mode 100644 index a7e3042..0000000 --- a/.github/tests-go-1.12/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM golang:1.12 - -LABEL "com.github.actions.name"="Test with Go 1.12" -LABEL "com.github.actions.icon"="radio" -LABEL "com.github.actions.color"="blue" - -ADD entrypoint.sh /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/tests-go-1.12/entrypoint.sh b/.github/tests-go-1.12/entrypoint.sh deleted file mode 100755 index af30810..0000000 --- a/.github/tests-go-1.12/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -eux - -go test -v -short -coverprofile cover.out ./... - -curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION diff --git a/.github/tests-go-master/Dockerfile b/.github/tests-go-master/Dockerfile deleted file mode 100644 index 7632c1e..0000000 --- a/.github/tests-go-master/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM captncraig/go-tip - -LABEL "com.github.actions.name"="Test with Go master" -LABEL "com.github.actions.icon"="radio" -LABEL "com.github.actions.color"="red" - -ADD entrypoint.sh /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/tests-go-master/entrypoint.sh b/.github/tests-go-master/entrypoint.sh deleted file mode 100755 index af30810..0000000 --- a/.github/tests-go-master/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -eux - -go test -v -short -coverprofile cover.out ./... - -curl -s https://codecov.io/bash | bash -s -- -X fix -e GOLANG_VERSION