diff options
author | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2020-02-21 01:38:47 +0300 |
---|---|---|
committer | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2020-02-21 01:38:47 +0300 |
commit | 757b2766cd4491ceb7eed5075c1477ba91880fbf (patch) | |
tree | fd7a44b2ba65810c5a837d9bc293f7b5a0d87525 |
first commit
-rw-r--r-- | LICENSE | 19 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | constants.go | 43 | ||||
-rw-r--r-- | example/main.go | 37 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | marusia.go | 114 | ||||
-rw-r--r-- | types.go | 115 |
7 files changed, 338 insertions, 0 deletions
@@ -0,0 +1,19 @@ +Copyright (c) 2020 Alexander Kiryukhin <a.kiryukhin@mail.ru> + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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.
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..80a5d4e --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Marusia API [![GoDoc](https://godoc.org/github.com/neonxp/marusia?status.svg)](https://godoc.org/github.com/neonxp/marusia) + +Skills SDK for [Marusia](http://marusia.mail.ru/) voice assistant. + +Documentation: [http://godoc.org/github.com/neonxp/marusia](http://godoc.org/github.com/neonxp/marusia) + +Example: [/example/main.go](/example/main.go)
\ No newline at end of file diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..6566ce9 --- /dev/null +++ b/constants.go @@ -0,0 +1,43 @@ +// Copyright (c) 2020 Alexander Kiryukhin <a.kiryukhin@mail.ru> + +// 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: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// 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. + +package marusia + +//CtxSessionID context key for session_id +//CtxMessageID context key for message_id +//CtxUserID context key for user_id +//CtxSkillID context key for skill_id +//CtxNew context key for is new session flag +//CtxClientID context key for client_id +//CtxLocale context key for POSIX locale +//CtxTimezone context key for timezone +//CtxInterfaces context key for interface +const ( + version = "1.0" + CtxSessionID = "session_id" + CtxMessageID = "message_id" + CtxUserID = "user_id" + CtxSkillID = "skill_id" + CtxNew = "new" + CtxClientID = "client_id" + CtxLocale = "locale" + CtxTimezone = "timezone" + CtxInterfaces = "interfaces" +) diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..404c970 --- /dev/null +++ b/example/main.go @@ -0,0 +1,37 @@ +// +build example + +package main + +import ( + "context" + "log" + "net/http" + + "github.com/neonxp/marusia" +) + +func main() { + m := marusia.NewMarusia(messageHandler) + server := http.Server{ + Addr: ":8080", + Handler: m.Handler(), + } + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatal(err) + } +} + +func messageHandler(ctx context.Context, req *marusia.Request) (*marusia.Response, error) { + log.Printf( + "Session id: %s\nUser id: %s\nMessage id: %s\nIncomming message: %s\nButton payload: %+v", + ctx.Value(marusia.CtxSessionID), + ctx.Value(marusia.CtxUserID), + ctx.Value(marusia.CtxMessageID), + req.Command, + req.Payload, + ) + resp := marusia.NewResponse("Это ответ на запрос!"). + AddButton("Адрес библиотеки", nil, "https://github.com/neonxp/marusia"). + AddButton("Произвольный payload", map[string]interface{}{"Hello": "world"}, "") + return resp, nil +} @@ -0,0 +1,3 @@ +module github.com/neonxp/marusia + +go 1.13 diff --git a/marusia.go b/marusia.go new file mode 100644 index 0000000..5356720 --- /dev/null +++ b/marusia.go @@ -0,0 +1,114 @@ +// Copyright (c) 2020 Alexander Kiryukhin <a.kiryukhin@mail.ru> + +// 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: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// 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. + +package marusia + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + "strings" +) + +// Marusia is main API object +type Marusia struct { + handler MessageHandler + ctx context.Context + errorLogger func(error) +} + +// SetCtx sets optional parent context +func (m *Marusia) SetCtx(ctx context.Context) *Marusia { + m.ctx = ctx + return m +} + +// SetErrorLogger sets optional error logger +func (m *Marusia) SetErrorLogger(errorLogger func(error)) *Marusia { + m.errorLogger = errorLogger + return m +} + +// NewMarusia is API constructor +func NewMarusia(handler MessageHandler) *Marusia { + return &Marusia{handler: handler, ctx: context.Background()} +} + +// MessageHandler is http.MessageHandler that proceed requests from Marusia and sends responses to Marusia +func (m *Marusia) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + reqEnvelope := new(requestEnvelope) + if err := json.NewDecoder(r.Body).Decode(reqEnvelope); err != nil { + w.WriteHeader(http.StatusBadRequest) + if m.errorLogger != nil { + m.errorLogger(err) + } + return + } + ctx := getContext(m.ctx, reqEnvelope) + resp, err := m.handler(ctx, reqEnvelope.Request) + if err != nil { + if m.errorLogger != nil { + m.errorLogger(err) + } + w.WriteHeader(http.StatusInternalServerError) + return + } + respEnvelope := &responseEnvelope{ + Response: resp, + Session: reqEnvelope.Session, + Version: version, + } + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(respEnvelope); err != nil { + if m.errorLogger != nil { + m.errorLogger(err) + } + w.WriteHeader(http.StatusInternalServerError) + } + }) +} + +func getContext(parent context.Context, req *requestEnvelope) context.Context { + data := map[string]string{ + CtxSessionID: req.Session.SessionID, + CtxUserID: req.Session.UserID, + CtxSkillID: req.Session.SkillID, + CtxMessageID: strconv.Itoa(req.Session.MessageID), + CtxClientID: req.Meta.ClientID, + CtxLocale: req.Meta.Locale, + CtxTimezone: req.Meta.Timezone, + } + var interfaces []string + for iface := range req.Meta.Interfaces { + interfaces = append(interfaces, iface) + } + data[CtxInterfaces] = strings.Join(interfaces, ",") + if req.Session.New { + data[CtxNew] = "true" + } + ctx := parent + for k, v := range data { + ctx = context.WithValue(ctx, k, v) + } + return ctx +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..f63fc86 --- /dev/null +++ b/types.go @@ -0,0 +1,115 @@ +// Copyright (c) 2020 Alexander Kiryukhin <a.kiryukhin@mail.ru> + +// 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: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// 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. + +package marusia + +import "context" + +type requestEnvelope struct { + Meta meta `json:"meta"` + Request *Request `json:"request"` + Session *session `json:"session"` + Version string `json:"version"` +} + +type responseEnvelope struct { + Response *Response `json:"response"` + Session *session `json:"session"` + Version string `json:"version"` +} + +// Text sets text to response +func (r *Response) SetText(text string) *Response { + r.Text = text + return r +} + +// TTS sets text with pronounce +func (r *Response) SetTTS(tts string) *Response { + r.TTS = tts + return r +} + +// EndSession if set dialog will be completed +func (r *Response) SetEndSession(endSession bool) *Response { + r.EndSession = endSession + return r +} + +// AddButton to dialog +func (r *Response) AddButton(title string, payload Payload, URL string) *Response { + r.Buttons = append(r.Buttons, newButton(title, payload, URL)) + return r +} + +// Request represents incoming message from Marusia to skill +type Request struct { + Command string `json:"command"` + OriginalUtterance string `json:"original_utterance"` + Type string `json:"type"` + Payload Payload `json:"payload"` + Nlu struct { + Tokens []string `json:"tokens"` + Entities []interface{} `json:"entities"` + } `json:"nlu"` +} + +// Response represents outgoing message from skill to Marusia +type Response struct { + Text string `json:"text"` + TTS string `json:"tts"` + Buttons []*button `json:"buttons"` + EndSession bool `json:"end_session"` +} + +func NewResponse(text string) *Response { + return &Response{Text: text} +} + +type meta struct { + ClientID string `json:"client_id"` + Locale string `json:"locale"` + Timezone string `json:"timezone"` + Interfaces map[string]interface{} `json:"interfaces"` +} + +type session struct { + SessionID string `json:"session_id"` + MessageID int `json:"message_id"` + UserID string `json:"user_id"` + SkillID string `json:"skill_id,omitempty"` + New bool `json:"new,omitempty"` +} + +type button struct { + Title string `json:"title"` + Payload Payload `json:"payload"` + URL string `json:"url"` +} + +func newButton(title string, payload Payload, URL string) *button { + return &button{Title: title, Payload: payload, URL: URL} +} + +// Payload represents payload that sends by pressing interface button +type Payload map[string]interface{} + +// MessageHandler represents handler for incoming requests +type MessageHandler func(context.Context, *Request) (*Response, error) |