aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2020-02-21 01:38:47 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2020-02-21 01:38:47 +0300
commit757b2766cd4491ceb7eed5075c1477ba91880fbf (patch)
treefd7a44b2ba65810c5a837d9bc293f7b5a0d87525
first commit
-rw-r--r--LICENSE19
-rw-r--r--README.md7
-rw-r--r--constants.go43
-rw-r--r--example/main.go37
-rw-r--r--go.mod3
-rw-r--r--marusia.go114
-rw-r--r--types.go115
7 files changed, 338 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1d14cfe
--- /dev/null
+++ b/LICENSE
@@ -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
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..01cb22a
--- /dev/null
+++ b/go.mod
@@ -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)