├── .gitignore ├── LICENSE ├── README.md ├── alice.go ├── alice_test.go ├── doc.go ├── doc_test.go ├── effects └── effects.go ├── entity.go ├── entity_test.go ├── example_test.go ├── go.mod ├── go.sum ├── image.go ├── image_test.go ├── images ├── health.png └── zhdun.jpeg ├── manual └── README.md ├── request.go ├── request_test.go ├── response.go ├── response_test.go └── sounds └── sounds.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Artem Zakharov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alice [![GoDoc](https://godoc.org/github.com/azzzak/alice?status.svg)](https://godoc.org/github.com/azzzak/alice) [![Go Report Card](https://goreportcard.com/badge/github.com/azzzak/alice)](https://goreportcard.com/report/github.com/azzzak/alice) 2 | 3 | Библиотека для создания навыков, расширяющих функциональность голосового помощника [Алиса](https://alice.yandex.ru). Упрощает разработку навыков, оставляя возможность тонкой настройки. Преимущества библиотеки: 4 | 5 | - поддержка [связки аккаунтов](https://yandex.ru/dev/dialogs/alice/doc/auth/about-account-linking-docpage/) 6 | - объединение методов в цепочки при конструировании ответа 7 | - вспомогательные методы для оживления диалога 8 | - автоответ на служебные ping-пакеты 9 | 10 | ## Установка или обновление 11 | 12 | `go get -u github.com/azzzak/alice` 13 | 14 | ## Пример 15 | 16 | Простейший навык — говорит "привет", после чего повторяет каждую реплику пользователя. 17 | 18 | ```Go 19 | package main 20 | 21 | import ( 22 | "net/http" 23 | 24 | "github.com/azzzak/alice" 25 | ) 26 | 27 | func main() { 28 | updates := alice.ListenForWebhook("/hook") 29 | go http.ListenAndServe(":3000", nil) 30 | 31 | updates.Loop(func(k alice.Kit) *alice.Response { 32 | req, resp := k.Init() 33 | if req.IsNewSession() { 34 | return resp.Text("привет") 35 | } 36 | return resp.Text(req.OriginalUtterance()) 37 | }) 38 | } 39 | ``` 40 | 41 | Детальный разбор примера смотрите в [руководстве](manual/README.md). 42 | 43 | ## Цепочки методов 44 | 45 | Позволяют конструировать ответ со всеми возможностями, которые предлагает Алиса: кнопки, картинки и звуки. 46 | 47 | **Пример.** Ответ с текстом и TTS с паузой и звуком из библиотеки Алисы: 48 | 49 | ```Go 50 | resp.Text("творог"). 51 | TTS("твор+ог"). 52 | Pause(3). 53 | Sound(sounds.Harp1) 54 | ``` 55 | 56 | **Пример.** Ответ со случайно выбранной строкой и двумя кнопками: 57 | 58 | ```Go 59 | resp.RandomText("привет", "алоха"). 60 | Button("хай", "", false). 61 | Button("отстань", "", false) 62 | ``` 63 | 64 | **Пример.** При любом _num_ ответ остается согласованным: 65 | 66 | ```Go 67 | resp.Text(fmt.Sprintf("%d %s пива %s на столе", num, 68 | alice.Plural(num, "бутылка", "бутылки", "бутылок"), 69 | alice.Plural(num, "стояла", "стояли", "стояло")). 70 | Sound(sounds.ThingsGlass1) 71 | ``` 72 | 73 | ## Навыки на базе библиотеки 74 | 75 | [![Дневник здоровья](images/health.png)](https://dialogs.yandex.ru/store/skills/dd5bb5ec-dnevnik-zdorov-ya) \ 76 | [**Дневник здоровья**](https://dialogs.yandex.ru/store/skills/dd5bb5ec-dnevnik-zdorov-ya) 77 | 78 | [![Полезный Ждун](images/zhdun.jpeg)](https://dialogs.yandex.ru/store/skills/16ff4b52-poleznyj-zhdu) \ 79 | [**Полезный Ждун**](https://dialogs.yandex.ru/store/skills/16ff4b52-poleznyj-zhdu) 80 | 81 | Присылайте свои навыки на azzzak@yandex.ru 82 | -------------------------------------------------------------------------------- /alice.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "net/http/httputil" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | func init() { 18 | rand.Seed(time.Now().UnixNano()) 19 | } 20 | 21 | // Kit структура для передачи данных в главный цикл. 22 | type Kit struct { 23 | Req *Request 24 | Resp *Response 25 | // Ctx позволяет получить сигнал об истечении периода ожидания ответа. 26 | Ctx context.Context 27 | 28 | c chan<- *Response 29 | } 30 | 31 | // Init получает входящий пакет и заготовку исходящего из данных запроса. 32 | func (k Kit) Init() (*Request, *Response) { 33 | return k.Req, k.Resp 34 | } 35 | 36 | // Options структура с настройками. 37 | type Options struct { 38 | AutoPong bool 39 | Timeout time.Duration 40 | Debug bool 41 | } 42 | 43 | // AutoPong автоматический ответ на технические сообщения ping, проверяющие работоспособность навыка. По умолчанию включено. 44 | func AutoPong(b bool) func(*Options) { 45 | return func(opts *Options) { 46 | opts.AutoPong = b 47 | } 48 | } 49 | 50 | // Timeout таймаут обработки запроса в миллисекундах. По истечении запрос перестает обрабатываться и навык отправляет ошибку. Значение по умолчанию 3000 мс — официальное время ожидания ответа навыка. 51 | func Timeout(t int) func(*Options) { 52 | return func(opts *Options) { 53 | if t < 1 { 54 | log.Fatalln("Timeout must be positive integer") 55 | } 56 | opts.Timeout = time.Duration(t) 57 | } 58 | } 59 | 60 | // Debug показывает в консоле содержимое входящих и исходящих пакетов. По умолчанию отключено. 61 | func Debug(b bool) func(*Options) { 62 | return func(opts *Options) { 63 | opts.Debug = b 64 | } 65 | } 66 | 67 | // Stream канал, передающий данные в основной цикл. 68 | type Stream <-chan Kit 69 | 70 | // Handler сигнатура функции, передаваемой методу Loop(). 71 | type Handler func(k Kit) *Response 72 | 73 | // Loop отвечает за работу главного цикла. 74 | func (updates Stream) Loop(f Handler) { 75 | for kit := range updates { 76 | go func(k Kit) { 77 | k.c <- f(k) 78 | close(k.c) 79 | }(kit) 80 | } 81 | } 82 | 83 | // ListenForWebhook регистрирует обработчик входящих пакетов. 84 | func ListenForWebhook(hook string, opts ...func(*Options)) Stream { 85 | conf := Options{ 86 | AutoPong: true, 87 | Timeout: 3000, 88 | Debug: false, 89 | } 90 | for _, opt := range opts { 91 | opt(&conf) 92 | } 93 | 94 | stream := make(chan Kit, 1) 95 | http.HandleFunc(hook, webhook(conf, stream)) 96 | 97 | return stream 98 | } 99 | 100 | func webhook(conf Options, stream chan<- Kit) http.HandlerFunc { 101 | reqPool := sync.Pool{ 102 | New: func() interface{} { 103 | return new(Request) 104 | }, 105 | } 106 | 107 | respPool := sync.Pool{ 108 | New: func() interface{} { 109 | return new(Response) 110 | }, 111 | } 112 | 113 | return func(w http.ResponseWriter, r *http.Request) { 114 | defer r.Body.Close() 115 | 116 | ctx, cancel := context.WithTimeout(r.Context(), conf.Timeout*time.Millisecond) 117 | defer cancel() 118 | 119 | if conf.Debug { 120 | requestDump, err := httputil.DumpRequest(r, true) 121 | if err != nil { 122 | log.Println(err) 123 | } 124 | fmt.Println(string(requestDump)) 125 | } 126 | 127 | req := reqPool.Get().(*Request) 128 | defer reqPool.Put(req) 129 | 130 | decoder := json.NewDecoder(r.Body) 131 | if err := decoder.Decode(req.clean()); err != nil { 132 | log.Println(err) 133 | w.WriteHeader(http.StatusBadRequest) 134 | return 135 | } 136 | 137 | resp := respPool.Get().(*Response) 138 | resp.clean().prepareResponse(req) 139 | defer respPool.Put(resp) 140 | 141 | if conf.AutoPong { 142 | if req.Type() == SimpleUtterance && req.Text() == "ping" { 143 | if md, err := json.Marshal(resp.Text("pong")); err == nil { 144 | w.Header().Set("Content-Type", "application/json") 145 | w.Write(md) 146 | return 147 | } 148 | w.WriteHeader(http.StatusInternalServerError) 149 | return 150 | } 151 | } 152 | 153 | req.Bearer = r.Header.Get("Authorization") 154 | 155 | back := make(chan *Response) 156 | stream <- Kit{ 157 | Req: req, 158 | Resp: resp, 159 | Ctx: ctx, 160 | 161 | c: back, 162 | } 163 | 164 | var response *Response 165 | select { 166 | case <-ctx.Done(): 167 | log.Println(ctx.Err()) 168 | w.WriteHeader(http.StatusInternalServerError) 169 | return 170 | case response = <-back: 171 | } 172 | 173 | writer := io.Writer(w) 174 | 175 | if conf.Debug { 176 | var buf bytes.Buffer 177 | writer = io.MultiWriter(w, &buf) 178 | defer func() { 179 | fmt.Printf("\n%s\n\n", buf.String()) 180 | }() 181 | } 182 | 183 | w.Header().Set("Content-Type", "application/json") 184 | encoder := json.NewEncoder(writer) 185 | if err := encoder.Encode(&response); err != nil { 186 | log.Println(err) 187 | w.WriteHeader(http.StatusInternalServerError) 188 | } 189 | } 190 | } 191 | 192 | // Plural помогает согласовать слово с числительным. 193 | func Plural(n int, singular, plural1, plural2 string) string { 194 | switch n % 100 { 195 | case 11, 12, 13, 14: 196 | return plural2 197 | } 198 | switch n % 10 { 199 | case 0, 5, 6, 7, 8, 9: 200 | return plural2 201 | case 1: 202 | return singular 203 | default: 204 | return plural1 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /alice_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/azzzak/alice" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAutoPong(t *testing.T) { 12 | type args struct { 13 | b bool 14 | } 15 | tests := []struct { 16 | name string 17 | args bool 18 | want alice.Options 19 | }{ 20 | { 21 | name: "", 22 | args: true, 23 | want: alice.Options{ 24 | AutoPong: true, 25 | }, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | conf := alice.Options{} 31 | opt := alice.AutoPong(tt.args) 32 | opt(&conf) 33 | assert.Equal(t, tt.want, conf) 34 | }) 35 | } 36 | } 37 | 38 | func TestTimeout(t *testing.T) { 39 | tests := []struct { 40 | name string 41 | args int 42 | want alice.Options 43 | }{ 44 | { 45 | name: "", 46 | args: 2500, 47 | want: alice.Options{ 48 | Timeout: 2500, 49 | }, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | conf := alice.Options{} 55 | opt := alice.Timeout(tt.args) 56 | opt(&conf) 57 | assert.Equal(t, tt.want, conf) 58 | }) 59 | } 60 | } 61 | 62 | func TestDebug(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | args bool 66 | want alice.Options 67 | }{ 68 | { 69 | name: "", 70 | args: true, 71 | want: alice.Options{ 72 | Debug: true, 73 | }, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | conf := alice.Options{} 79 | opt := alice.Debug(tt.args) 80 | opt(&conf) 81 | assert.Equal(t, tt.want, conf) 82 | }) 83 | } 84 | } 85 | 86 | func TestKit_Init(t *testing.T) { 87 | type fields struct { 88 | Req *alice.Request 89 | Resp *alice.Response 90 | Ctx context.Context 91 | } 92 | tests := []struct { 93 | name string 94 | fields fields 95 | want *alice.Request 96 | want1 *alice.Response 97 | }{ 98 | { 99 | name: "", 100 | fields: fields{ 101 | Req: getReq(0), 102 | Resp: getResp(0), 103 | }, 104 | want: getReq(0), 105 | want1: getResp(0), 106 | }, 107 | } 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | k := alice.Kit{ 111 | Req: tt.fields.Req, 112 | Resp: tt.fields.Resp, 113 | } 114 | got, got1 := k.Init() 115 | assert.Equal(t, tt.want, got) 116 | assert.Equal(t, tt.want1, got1) 117 | }) 118 | } 119 | } 120 | 121 | func TestPlural(t *testing.T) { 122 | type args struct { 123 | n int 124 | singular string 125 | plural1 string 126 | plural2 string 127 | } 128 | tests := []struct { 129 | name string 130 | args args 131 | want string 132 | }{ 133 | { 134 | name: "", 135 | args: args{ 136 | n: 0, 137 | singular: "бутылка", 138 | plural1: "бутылки", 139 | plural2: "бутылок", 140 | }, 141 | want: "бутылок", 142 | }, { 143 | name: "", 144 | args: args{ 145 | n: 1, 146 | singular: "бутылка", 147 | plural1: "бутылки", 148 | plural2: "бутылок", 149 | }, 150 | want: "бутылка", 151 | }, { 152 | name: "", 153 | args: args{ 154 | n: 2, 155 | singular: "бутылка", 156 | plural1: "бутылки", 157 | plural2: "бутылок", 158 | }, 159 | want: "бутылки", 160 | }, { 161 | name: "", 162 | args: args{ 163 | n: 5, 164 | singular: "бутылка", 165 | plural1: "бутылки", 166 | plural2: "бутылок", 167 | }, 168 | want: "бутылок", 169 | }, { 170 | name: "", 171 | args: args{ 172 | n: 15, 173 | singular: "бутылка", 174 | plural1: "бутылки", 175 | plural2: "бутылок", 176 | }, 177 | want: "бутылок", 178 | }, { 179 | name: "", 180 | args: args{ 181 | n: 21, 182 | singular: "бутылка", 183 | plural1: "бутылки", 184 | plural2: "бутылок", 185 | }, 186 | want: "бутылка", 187 | }, { 188 | name: "", 189 | args: args{ 190 | n: 105, 191 | singular: "бутылка", 192 | plural1: "бутылки", 193 | plural2: "бутылок", 194 | }, 195 | want: "бутылок", 196 | }, 197 | } 198 | for _, tt := range tests { 199 | t.Run(tt.name, func(t *testing.T) { 200 | if got := alice.Plural(tt.args.n, tt.args.singular, tt.args.plural1, tt.args.plural2); got != tt.want { 201 | t.Errorf("PluralForm() = %v, want %v", got, tt.want) 202 | } 203 | }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package alice реализует библиотеку для создания навыков, расширяющих функциональность голосового помощника Алиса от Яндекса. 2 | package alice 3 | -------------------------------------------------------------------------------- /doc_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/azzzak/alice" 7 | ) 8 | 9 | func Example() { 10 | updates := alice.ListenForWebhook("/hook") 11 | go http.ListenAndServe(":3000", nil) 12 | 13 | updates.Loop(func(k alice.Kit) *alice.Response { 14 | req, resp := k.Init() 15 | if req.IsNewSession() { 16 | return resp.Text("привет") 17 | } 18 | return resp.Text(req.OriginalUtterance()) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /effects/effects.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | const ( 4 | // NoEffect Отключает эффекты. 5 | NoEffect = "-" 6 | // BehindTheWall Голос из-за стены. 7 | BehindTheWall = "behind_the_wall" 8 | // Hamster Голос хомяка. 9 | Hamster = "hamster" 10 | // Megaphone Голос через мегафон. 11 | Megaphone = "megaphone" 12 | // PitchDown Низкий голос. 13 | PitchDown = "pitch_down" 14 | // Psychodelic Психоделический голос. 15 | Psychodelic = "psychodelic" 16 | // Pulse Голос с прерываниями. 17 | Pulse = "pulse" 18 | // TrainAnnounce Громкоговоритель на вокзале. 19 | TrainAnnounce = "train_announce" 20 | ) 21 | -------------------------------------------------------------------------------- /entity.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | const ( 8 | // NENameType именованная сущность с ФИО. 9 | NENameType = "YANDEX.FIO" 10 | 11 | // NELocationType именованная сущность с топонимом. 12 | NELocationType = "YANDEX.GEO" 13 | 14 | // NEDataTimeType именованная сущность с датой и временем. 15 | NEDataTimeType = "YANDEX.DATETIME" 16 | 17 | // NENumberType именованная сущность с числом. 18 | NENumberType = "YANDEX.NUMBER" 19 | ) 20 | 21 | //Entity структура прототипа именованной сущности в запросе. 22 | type Entity struct { 23 | Tokens struct { 24 | Start int `json:"start"` 25 | End int `json:"end"` 26 | } `json:"tokens"` 27 | Type string `json:"type"` 28 | Value *json.RawMessage `json:"value"` 29 | } 30 | 31 | // NEName структура типа NENameType. 32 | type NEName struct { 33 | Start, End int 34 | FirstName string `json:"first_name,omitempty"` 35 | PatronymicName string `json:"patronymic_name,omitempty"` 36 | LastName string `json:"last_name,omitempty"` 37 | } 38 | 39 | // NELocation структура типа NELocationType. 40 | type NELocation struct { 41 | Start, End int 42 | Country string `json:"country,omitempty"` 43 | City string `json:"city,omitempty"` 44 | Street string `json:"street,omitempty"` 45 | HouseNumber string `json:"house_number,omitempty"` 46 | Airport string `json:"airport,omitempty"` 47 | } 48 | 49 | // NEDateTime структура типа NEDataTimeType. 50 | type NEDateTime struct { 51 | Start, End int 52 | Year int `json:"year,omitempty"` 53 | YearIsRelative bool `json:"year_is_relative,omitempty"` 54 | Month int `json:"month,omitempty"` 55 | MonthIsRelative bool `json:"month_is_relative,omitempty"` 56 | Day int `json:"day,omitempty"` 57 | DayIsRelative bool `json:"day_is_relative,omitempty"` 58 | Hour int `json:"hour,omitempty"` 59 | HourIsRelative bool `json:"hour_is_relative,omitempty"` 60 | Minute int `json:"minute,omitempty"` 61 | MinuterIsRelative bool `json:"minute_is_relative,omitempty"` 62 | } 63 | 64 | // NENumber структура типа NENumberType. 65 | type NENumber float32 66 | 67 | func (NEName) netype() {} 68 | func (NELocation) netype() {} 69 | func (NEDateTime) netype() {} 70 | func (NENumber) netype() {} 71 | 72 | type netype interface { 73 | netype() 74 | } 75 | 76 | // Entities возвращает необработанные именованные сущности из запроса. 77 | func (req *Request) Entities() (Entities, error) { 78 | var entities Entities 79 | entities, err := unmarshalEntities(req.Request.NLU.Entities) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return entities, nil 84 | } 85 | 86 | // Names возвращает готовый к использованию массив именованных сущностей с ФИО. 87 | func (e Entities) Names() []NEName { 88 | var a []NEName 89 | for _, v := range e[NENameType] { 90 | d := v.Value.(*NEName) 91 | d.Start, d.End = v.Start, v.End 92 | a = append(a, *d) 93 | } 94 | return a 95 | } 96 | 97 | // Locations возвращает готовый к использованию массив именованных сущностей с топонимами. 98 | func (e Entities) Locations() []NELocation { 99 | var a []NELocation 100 | for _, v := range e[NELocationType] { 101 | d := v.Value.(*NELocation) 102 | d.Start, d.End = v.Start, v.End 103 | a = append(a, *d) 104 | } 105 | return a 106 | } 107 | 108 | // DatesTimes возвращает готовый к использованию массив именованных сущностей с датами и временем. 109 | func (e Entities) DatesTimes() []NEDateTime { 110 | var a []NEDateTime 111 | for _, v := range e[NEDataTimeType] { 112 | d := v.Value.(*NEDateTime) 113 | d.Start, d.End = v.Start, v.End 114 | a = append(a, *d) 115 | } 116 | return a 117 | } 118 | 119 | // NumberWrapper обертка для чисел из именованных сущностей. 120 | type NumberWrapper struct { 121 | Start, End int 122 | Value float32 123 | } 124 | 125 | // Numbers возвращает готовый к использованию массив именованных сущностей с числами. 126 | func (e Entities) Numbers() []NumberWrapper { 127 | var a []NumberWrapper 128 | for _, v := range e[NENumberType] { 129 | n := v.Value.(*NENumber) 130 | d := NumberWrapper{ 131 | Start: v.Start, 132 | End: v.End, 133 | Value: float32(*n), 134 | } 135 | a = append(a, d) 136 | } 137 | return a 138 | } 139 | 140 | type newrapper struct { 141 | Start, End int 142 | Value netype 143 | } 144 | 145 | // Entities контейнер для передачи необработанных именованных сущностей. 146 | type Entities map[string][]newrapper 147 | 148 | func unmarshalEntities(e []Entity) (Entities, error) { 149 | m := make(Entities) 150 | for _, v := range e { 151 | h := holder(v.Type) 152 | if err := json.Unmarshal(*v.Value, h); err != nil { 153 | return nil, err 154 | } 155 | wrapper := newrapper{ 156 | Start: v.Tokens.Start, 157 | End: v.Tokens.End, 158 | Value: h, 159 | } 160 | m[v.Type] = append(m[v.Type], wrapper) 161 | } 162 | return m, nil 163 | } 164 | 165 | func holder(t string) netype { 166 | switch t { 167 | case NENameType: 168 | return new(NEName) 169 | case NELocationType: 170 | return new(NELocation) 171 | case NEDataTimeType: 172 | return new(NEDateTime) 173 | case NENumberType: 174 | return new(NENumber) 175 | } 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /entity_test.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRequest_Entities(t *testing.T) { 11 | test0 := map[string][]newrapper{ 12 | "YANDEX.DATETIME": { 13 | { 14 | Start: 4, 15 | End: 5, 16 | Value: &NEDateTime{ 17 | Start: 0, 18 | End: 0, 19 | Year: 0, 20 | YearIsRelative: false, 21 | MonthIsRelative: false, 22 | Month: 0, 23 | Day: -1, 24 | DayIsRelative: true, 25 | Hour: 0, 26 | HourIsRelative: false, 27 | Minute: 0, 28 | MinuterIsRelative: false, 29 | }, 30 | }, 31 | }, 32 | "YANDEX.FIO": []newrapper{ 33 | { 34 | Start: 2, 35 | End: 5, 36 | Value: &NEName{ 37 | Start: 0, 38 | End: 0, 39 | FirstName: "валентин", 40 | PatronymicName: "петрович", 41 | LastName: "вчера", 42 | }, 43 | }, { 44 | Start: 5, 45 | End: 7, 46 | Value: &NEName{ 47 | Start: 0, 48 | End: 0, 49 | FirstName: "сергей", 50 | PatronymicName: "", 51 | LastName: "иванов", 52 | }, 53 | }, 54 | }, 55 | "YANDEX.GEO": []newrapper{ 56 | { 57 | Start: 8, 58 | End: 10, 59 | Value: &NELocation{ 60 | Start: 0, 61 | End: 0, 62 | Country: "", 63 | City: "саранск", 64 | Street: "", 65 | HouseNumber: "", 66 | Airport: "", 67 | }, 68 | }, 69 | }, 70 | "YANDEX.NUMBER": []newrapper{ 71 | { 72 | Start: 10, 73 | End: 11, 74 | Value: func(i NENumber) *NENumber { return &i }(NENumber(5)), 75 | }, 76 | }, 77 | } 78 | 79 | tests := []struct { 80 | name string 81 | request *Request 82 | want Entities 83 | wantErr bool 84 | }{ 85 | { 86 | name: "", 87 | request: getReq(0), 88 | want: Entities(test0), 89 | wantErr: false, 90 | }, 91 | } 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | req := tt.request 95 | got, err := req.Entities() 96 | if (err != nil) != tt.wantErr { 97 | t.Errorf("Request.Entities() error = %v, wantErr %v", err, tt.wantErr) 98 | return 99 | } 100 | assert.Equal(t, tt.want, got) 101 | }) 102 | } 103 | } 104 | 105 | func TestEntities_Names(t *testing.T) { 106 | req := getReq(0) 107 | entities, _ := req.Entities() 108 | 109 | names := []NEName{ 110 | { 111 | Start: 2, 112 | End: 5, 113 | FirstName: "валентин", 114 | PatronymicName: "петрович", 115 | LastName: "вчера", 116 | }, { 117 | Start: 5, 118 | End: 7, 119 | FirstName: "сергей", 120 | LastName: "иванов", 121 | }, 122 | } 123 | 124 | tests := []struct { 125 | name string 126 | e Entities 127 | want []NEName 128 | }{ 129 | { 130 | name: "", 131 | e: entities, 132 | want: names, 133 | }, 134 | } 135 | for _, tt := range tests { 136 | t.Run(tt.name, func(t *testing.T) { 137 | got := tt.e.Names() 138 | assert.Equal(t, tt.want, got) 139 | }) 140 | } 141 | } 142 | 143 | func TestEntities_Locations(t *testing.T) { 144 | req := getReq(0) 145 | entities, _ := req.Entities() 146 | 147 | locations := []NELocation{ 148 | { 149 | Start: 8, 150 | End: 10, 151 | Country: "", 152 | City: "саранск", 153 | }, 154 | } 155 | 156 | tests := []struct { 157 | name string 158 | e Entities 159 | want []NELocation 160 | }{ 161 | { 162 | name: "", 163 | e: entities, 164 | want: locations, 165 | }, 166 | } 167 | for _, tt := range tests { 168 | t.Run(tt.name, func(t *testing.T) { 169 | got := tt.e.Locations() 170 | assert.Equal(t, tt.want, got) 171 | }) 172 | } 173 | } 174 | 175 | func TestEntities_DatesTimes(t *testing.T) { 176 | req := getReq(0) 177 | entities, _ := req.Entities() 178 | 179 | dt := []NEDateTime{ 180 | { 181 | Start: 4, 182 | End: 5, 183 | Day: -1, 184 | DayIsRelative: true, 185 | }, 186 | } 187 | 188 | tests := []struct { 189 | name string 190 | e Entities 191 | want []NEDateTime 192 | }{ 193 | { 194 | name: "", 195 | e: entities, 196 | want: dt, 197 | }, 198 | } 199 | for _, tt := range tests { 200 | t.Run(tt.name, func(t *testing.T) { 201 | got := tt.e.DatesTimes() 202 | assert.Equal(t, tt.want, got) 203 | }) 204 | } 205 | } 206 | 207 | func TestEntities_Numbers(t *testing.T) { 208 | req := getReq(0) 209 | entities, _ := req.Entities() 210 | 211 | num := []NumberWrapper{ 212 | { 213 | Start: 10, 214 | End: 11, 215 | Value: 5, 216 | }, 217 | } 218 | 219 | tests := []struct { 220 | name string 221 | e Entities 222 | want []NumberWrapper 223 | }{ 224 | { 225 | name: "", 226 | e: entities, 227 | want: num, 228 | }, 229 | } 230 | for _, tt := range tests { 231 | t.Run(tt.name, func(t *testing.T) { 232 | got := tt.e.Numbers() 233 | assert.Equal(t, tt.want, got) 234 | }) 235 | } 236 | } 237 | 238 | func getReq(n int) *Request { 239 | source := []string{`{"meta":{"client_id":"ru.yandex.searchplugin/7.16 (none none; android 4.4.2)","interfaces":{"account_linking":{},"payments":{},"screen":{}},"locale":"ru-RU","timezone":"UTC"},"request":{"command":"по словам валентина петровича, вчера сергей иванов отвез в саранск пять мешков дерьма","nlu":{"entities":[{"tokens":{"end":5,"start":2},"type":"YANDEX.FIO","value":{"first_name":"валентин","last_name":"вчера","patronymic_name":"петрович"}},{"tokens":{"end":5,"start":4},"type":"YANDEX.DATETIME","value":{"day":-1,"day_is_relative":true}},{"tokens":{"end":7,"start":5},"type":"YANDEX.FIO","value":{"first_name":"сергей","last_name":"иванов"}},{"tokens":{"end":10,"start":8},"type":"YANDEX.GEO","value":{"city":"саранск"}},{"tokens":{"end":11,"start":10},"type":"YANDEX.NUMBER","value":5}],"tokens":["по","словам","валентина","петровича","вчера","сергей","иванов","отвез","в","саранск","5","мешков","д"]},"original_utterance":"по словам валентина петровича, вчера сергей иванов отвез в саранск пять мешков дерьма","type":"SimpleUtterance"},"session":{"message_id":7,"new":false,"session_id":"ed0cba5b-e68516f8-b73d36cc-ce696176","skill_id":"e03f8d5b-35ef-4d57-9450-b721ca17a6c3","user_id":"03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0"},"version":"1.0"}`} 240 | 241 | var req = new(Request) 242 | json.Unmarshal([]byte(source[n]), req) 243 | return req 244 | } 245 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/azzzak/alice" 8 | ) 9 | 10 | func ExampleListenForWebhook() { 11 | updates := alice.ListenForWebhook("/hook", alice.Debug(true)) 12 | go http.ListenAndServe(":3000", nil) 13 | 14 | updates.Loop(func(k alice.Kit) *alice.Response { 15 | _, resp := k.Init() 16 | return resp.Text("ok") 17 | }) 18 | } 19 | 20 | func ExampleRequest_Entities() { 21 | updates := alice.ListenForWebhook("/hook") 22 | go http.ListenAndServe(":3000", nil) 23 | 24 | updates.Loop(func(k alice.Kit) *alice.Response { 25 | req, resp := k.Init() 26 | 27 | entities, _ := req.Entities() 28 | for _, v := range entities.Names() { 29 | fmt.Printf("%+v\n", v) 30 | } 31 | for _, v := range entities.Locations() { 32 | fmt.Printf("%+v\n", v) 33 | } 34 | for _, v := range entities.DatesTimes() { 35 | fmt.Printf("%+v\n", v) 36 | } 37 | for _, v := range entities.Numbers() { 38 | fmt.Printf("%+v\n", v) 39 | } 40 | 41 | return resp.Text("ok") 42 | }) 43 | } 44 | 45 | func ExampleRequest_Payload() { 46 | updates := alice.ListenForWebhook("/hook") 47 | go http.ListenAndServe(":3000", nil) 48 | 49 | updates.Loop(func(k alice.Kit) *alice.Response { 50 | req, resp := k.Init() 51 | 52 | // payload вида {"msg":"ok","n":42} 53 | payload, err := req.Payload() 54 | if err == nil { 55 | fmt.Println(payload["msg"].(string)) 56 | fmt.Println(payload["n"].(int)) 57 | } 58 | 59 | return resp.Text("ok") 60 | }) 61 | } 62 | 63 | func ExampleRequest_PayloadString() { 64 | updates := alice.ListenForWebhook("/hook") 65 | go http.ListenAndServe(":3000", nil) 66 | 67 | updates.Loop(func(k alice.Kit) *alice.Response { 68 | req, resp := k.Init() 69 | 70 | payload, err := req.PayloadString() 71 | if err == nil { 72 | fmt.Println(payload) 73 | } 74 | 75 | return resp.Text("ok") 76 | }) 77 | } 78 | 79 | func ExampleResponse_BigImage() { 80 | updates := alice.ListenForWebhook("/hook") 81 | go http.ListenAndServe(":3000", nil) 82 | 83 | updates.Loop(func(k alice.Kit) *alice.Response { 84 | _, resp := k.Init() 85 | 86 | return resp.BigImage("111111/00000000000000000000", "image", "desc") 87 | }) 88 | } 89 | 90 | func ExampleResponse_BigImage_button() { 91 | updates := alice.ListenForWebhook("/hook") 92 | go http.ListenAndServe(":3000", nil) 93 | 94 | updates.Loop(func(k alice.Kit) *alice.Response { 95 | _, resp := k.Init() 96 | 97 | b := alice.NewImageButton("кнопка", "https://yandex.ru") 98 | return resp.BigImage("111111/00000000000000000000", "image", "desc", b) 99 | }) 100 | } 101 | 102 | func ExampleResponse_List() { 103 | updates := alice.ListenForWebhook("/hook") 104 | go http.ListenAndServe(":3000", nil) 105 | 106 | updates.Loop(func(k alice.Kit) *alice.Response { 107 | _, resp := k.Init() 108 | 109 | var list alice.List 110 | list.Add("111111/00000000000000000000", "image1", "desc1") 111 | list.Add("222222/00000000000000000000", "image2", "desc2") 112 | list.Add("333333/00000000000000000000", "image3", "desc3") 113 | 114 | return resp.Text("список").List("список", "подвал", list) 115 | }) 116 | } 117 | 118 | func ExamplePlural() { 119 | num := 5 120 | fmt.Printf("%d %s пива %s на столе", num, 121 | alice.Plural(num, "бутылка", "бутылки", "бутылок"), 122 | alice.Plural(num, "стояла", "стояли", "стояло")) 123 | // Output: 5 бутылок пива стояло на столе 124 | } 125 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/azzzak/alice 2 | 3 | go 1.12 4 | 5 | require github.com/stretchr/testify v1.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 8 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 9 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | const ( 4 | // BigImageType одна большая картинка. 5 | BigImageType = "BigImage" 6 | 7 | // ItemsListType список с картинками. 8 | ItemsListType = "ItemsList" 9 | ) 10 | 11 | // Image структура картинки. 12 | type Image struct { 13 | ImageID string `json:"image_id"` 14 | Title string `json:"title,omitempty"` 15 | Description string `json:"description,omitempty"` 16 | Button *ImageButton `json:"button,omitempty"` 17 | } 18 | 19 | // ImageButton структура кнопки для добавления интерактивности картинке. 20 | type ImageButton struct { 21 | Title string `json:"title,omitempty"` 22 | URL string `json:"url,omitempty"` 23 | Payload interface{} `json:"payload,omitempty"` 24 | } 25 | 26 | // NewImageButton создает кнопку для добавления интерактивности картинке. Payload может быть проигнорирован. Если задано больше одного payload используется только первый. 27 | func NewImageButton(title, url string, payload ...interface{}) ImageButton { 28 | var p interface{} 29 | if len(payload) > 0 { 30 | p = payload[0] 31 | } 32 | 33 | return ImageButton{ 34 | Title: title, 35 | URL: url, 36 | Payload: p, 37 | } 38 | } 39 | 40 | // Card структура для добавления в ответ картинок. 41 | type Card struct { 42 | ImageID string `json:"image_id,omitempty"` 43 | Title string `json:"title,omitempty"` 44 | Description string `json:"description,omitempty"` 45 | Button *ImageButton `json:"button,omitempty"` 46 | 47 | Type string `json:"type"` 48 | Header *struct { 49 | Text string `json:"text,omitempty"` 50 | } `json:"header,omitempty"` 51 | Items []Image `json:"items,omitempty"` 52 | Footer *struct { 53 | Text string `json:"text,omitempty"` 54 | Button *ImageButton `json:"button,omitempty"` 55 | } `json:"footer,omitempty"` 56 | } 57 | 58 | // BigImage добавляет к ответу одну большую картинку. Кнопка может быть проигнорирована. Если задано больше одной кнопки используется только первая. Требуемые размеры изображения: 1x — 388x172, 1.5x — 582x258, 2x — 776x344, 3x — 1164x516, 3.5x — 1358x602, 4x — 1552x688. 59 | func (resp *Response) BigImage(id, title, desc string, button ...ImageButton) *Response { 60 | var b *ImageButton 61 | if len(button) > 0 { 62 | b = &button[0] 63 | } 64 | 65 | image := &Card{ 66 | Type: BigImageType, 67 | ImageID: id, 68 | Title: title, 69 | Description: desc, 70 | Button: b, 71 | } 72 | 73 | resp.Response.Card = image 74 | return resp 75 | } 76 | 77 | // List хранилище картинок для создания списка. 78 | type List struct { 79 | Images []Image 80 | } 81 | 82 | // Add добавляет картинку в заготовку для списка. Кнопка может быть проигнорирована. Если задано больше одной кнопки используется только первая. 83 | func (l *List) Add(id, title, desc string, button ...ImageButton) *List { 84 | var b *ImageButton 85 | if len(button) > 0 { 86 | b = &button[0] 87 | } 88 | 89 | image := Image{ 90 | ImageID: id, 91 | Title: title, 92 | Description: desc, 93 | Button: b, 94 | } 95 | 96 | return l.AddImages(image) 97 | } 98 | 99 | // AddImages добавляет одну или несколько картинок в заготовку для списка. 100 | func (l *List) AddImages(images ...Image) *List { 101 | for _, v := range images { 102 | l.Images = append(l.Images, v) 103 | } 104 | return l 105 | } 106 | 107 | // List добавляет к ответу список с картинками. Кнопка может быть проигнорирована. Если задано больше одной кнопки используется только первая. 108 | func (resp *Response) List(header, footer string, l List, button ...ImageButton) *Response { 109 | var b *ImageButton 110 | if len(button) > 0 { 111 | b = &button[0] 112 | } 113 | 114 | if len(l.Images) > 5 { 115 | l.Images = l.Images[:5] 116 | } 117 | 118 | list := &Card{ 119 | Type: ItemsListType, 120 | Header: &struct { 121 | Text string `json:"text,omitempty"` 122 | }{ 123 | Text: header, 124 | }, 125 | Items: l.Images, 126 | Footer: &struct { 127 | Text string `json:"text,omitempty"` 128 | Button *ImageButton `json:"button,omitempty"` 129 | }{ 130 | Text: footer, 131 | Button: b, 132 | }, 133 | } 134 | 135 | resp.Response.Card = list 136 | return resp 137 | } 138 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/azzzak/alice" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewImageButton(t *testing.T) { 12 | test0 := alice.ImageButton{ 13 | Title: "кнопка", 14 | URL: "https://google.com", 15 | Payload: nil, 16 | } 17 | 18 | test1 := alice.ImageButton{ 19 | Title: "кнопка2", 20 | URL: "https://google.com", 21 | Payload: `{"msg":"ok"}`, 22 | } 23 | 24 | type args struct { 25 | title string 26 | url string 27 | payload []interface{} 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want alice.ImageButton 33 | }{ 34 | { 35 | name: "", 36 | args: args{ 37 | title: "кнопка", 38 | url: "https://google.com", 39 | payload: nil, 40 | }, 41 | want: test0, 42 | }, { 43 | name: "", 44 | args: args{ 45 | title: "кнопка2", 46 | url: "https://google.com", 47 | payload: []interface{}{`{"msg":"ok"}`}, 48 | }, 49 | want: test1, 50 | }, { 51 | name: "", 52 | args: args{ 53 | title: "кнопка2", 54 | url: "https://google.com", 55 | payload: []interface{}{`{"msg":"ok"}`, `{"msg":"nope"}`}, 56 | }, 57 | want: test1, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | assert.Equal(t, tt.want, alice.NewImageButton(tt.args.title, tt.args.url, tt.args.payload...)) 63 | }) 64 | } 65 | } 66 | 67 | func TestResponse_BigImage(t *testing.T) { 68 | source_0 := `{"response":{"text":"","card":{"image_id":"123","title":"название","description":"описание","type":"BigImage"},"end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}` 69 | var test0 = new(alice.Response) 70 | json.Unmarshal([]byte(source_0), test0) 71 | 72 | source_1 := `{"response":{"text":"","card":{"image_id":"456","title":"название2","description":"описание2","button":{"url":"https://google.com"},"type":"BigImage"},"end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}` 73 | var test1 = new(alice.Response) 74 | json.Unmarshal([]byte(source_1), test1) 75 | 76 | type args struct { 77 | id string 78 | title string 79 | desc string 80 | button []alice.ImageButton 81 | } 82 | tests := []struct { 83 | name string 84 | response *alice.Response 85 | args args 86 | want *alice.Response 87 | }{ 88 | { 89 | name: "", 90 | response: getResp(0), 91 | args: args{ 92 | id: "123", 93 | title: "название", 94 | desc: "описание", 95 | button: nil, 96 | }, 97 | want: test0, 98 | }, { 99 | name: "", 100 | response: getResp(0), 101 | args: args{ 102 | id: "456", 103 | title: "название2", 104 | desc: "описание2", 105 | button: []alice.ImageButton{ 106 | { 107 | Title: "", 108 | URL: "https://google.com", 109 | }, 110 | }, 111 | }, 112 | want: test1, 113 | }, 114 | } 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | resp := tt.response 118 | got := resp.BigImage(tt.args.id, tt.args.title, tt.args.desc, tt.args.button...) 119 | assert.Equal(t, tt.want, got) 120 | }) 121 | } 122 | } 123 | 124 | func TestGallery_Add(t *testing.T) { 125 | test0 := &alice.List{ 126 | Images: []alice.Image{ 127 | { 128 | ImageID: "123", 129 | Title: "название", 130 | Description: "описание", 131 | Button: nil, 132 | }, 133 | }, 134 | } 135 | 136 | test1 := &alice.List{ 137 | Images: []alice.Image{ 138 | { 139 | ImageID: "456", 140 | Title: "название2", 141 | Description: "описание2", 142 | Button: &alice.ImageButton{ 143 | Title: "кнопка", 144 | URL: "https://google.com", 145 | Payload: "data", 146 | }, 147 | }, 148 | }, 149 | } 150 | 151 | type args struct { 152 | id string 153 | title string 154 | desc string 155 | button []alice.ImageButton 156 | } 157 | tests := []struct { 158 | name string 159 | args args 160 | want *alice.List 161 | }{ 162 | { 163 | name: "", 164 | args: args{ 165 | id: "123", 166 | title: "название", 167 | desc: "описание", 168 | button: nil, 169 | }, 170 | want: test0, 171 | }, { 172 | name: "", 173 | args: args{ 174 | id: "456", 175 | title: "название2", 176 | desc: "описание2", 177 | button: []alice.ImageButton{ 178 | { 179 | Title: "кнопка", 180 | URL: "https://google.com", 181 | Payload: "data", 182 | }, 183 | }, 184 | }, 185 | want: test1, 186 | }, 187 | } 188 | for _, tt := range tests { 189 | t.Run(tt.name, func(t *testing.T) { 190 | g := &alice.List{} 191 | got := g.Add(tt.args.id, tt.args.title, tt.args.desc, tt.args.button...) 192 | assert.Equal(t, tt.want, got) 193 | }) 194 | } 195 | } 196 | 197 | func TestGallery_AddImages(t *testing.T) { 198 | test0 := &alice.List{ 199 | Images: []alice.Image{ 200 | { 201 | ImageID: "123", 202 | Title: "название", 203 | Description: "описание", 204 | Button: nil, 205 | }, { 206 | ImageID: "456", 207 | Title: "название2", 208 | Description: "описание2", 209 | Button: &alice.ImageButton{ 210 | Title: "кнопка", 211 | URL: "https://google.com", 212 | Payload: "data", 213 | }, 214 | }, 215 | }, 216 | } 217 | 218 | type args struct { 219 | images []alice.Image 220 | } 221 | tests := []struct { 222 | name string 223 | args args 224 | want *alice.List 225 | }{ 226 | { 227 | name: "", 228 | args: args{ 229 | images: []alice.Image{{ 230 | ImageID: "123", 231 | Title: "название", 232 | Description: "описание", 233 | Button: nil, 234 | }, { 235 | ImageID: "456", 236 | Title: "название2", 237 | Description: "описание2", 238 | Button: &alice.ImageButton{ 239 | Title: "кнопка", 240 | URL: "https://google.com", 241 | Payload: "data", 242 | }, 243 | }}, 244 | }, 245 | want: test0, 246 | }, 247 | } 248 | for _, tt := range tests { 249 | t.Run(tt.name, func(t *testing.T) { 250 | g := &alice.List{} 251 | got := g.AddImages(tt.args.images...) 252 | assert.Equal(t, tt.want, got) 253 | }) 254 | } 255 | } 256 | 257 | func TestResponse_List(t *testing.T) { 258 | source := `{"response":{"text":"","card":{"type":"ItemsList","header":{"text":"заголовок"},"items":[{"image_id":"123","title":"название","description":"описание"},{"image_id":"456","title":"название2","description":"описание2","button":{"title":"кнопка","url":"https://google.com","payload":"data"}}],"footer":{"text":"подвал","button":{"title":"кнопка2","url":"https://ya.ru","payload":"msg"}}},"end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}` 259 | var test0 = new(alice.Response) 260 | json.Unmarshal([]byte(source), test0) 261 | 262 | source1 := `{"response":{"text":"","card":{"type":"ItemsList","header":{"text":"заголовок"},"items":[{"image_id":"123"},{"image_id":"123"},{"image_id":"123"},{"image_id":"123"},{"image_id":"123"}],"footer":{"text":"подвал"}},"end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}` 263 | var test1 = new(alice.Response) 264 | json.Unmarshal([]byte(source1), test1) 265 | 266 | type args struct { 267 | header string 268 | footer string 269 | g alice.List 270 | button []alice.ImageButton 271 | } 272 | tests := []struct { 273 | name string 274 | response *alice.Response 275 | args args 276 | want *alice.Response 277 | }{ 278 | { 279 | name: "", 280 | response: getResp(0), 281 | args: args{ 282 | header: "заголовок", 283 | footer: "подвал", 284 | g: alice.List{ 285 | Images: []alice.Image{{ 286 | ImageID: "123", 287 | Title: "название", 288 | Description: "описание", 289 | Button: nil, 290 | }, { 291 | ImageID: "456", 292 | Title: "название2", 293 | Description: "описание2", 294 | Button: &alice.ImageButton{ 295 | Title: "кнопка", 296 | URL: "https://google.com", 297 | Payload: "data", 298 | }, 299 | }}, 300 | }, 301 | button: []alice.ImageButton{ 302 | { 303 | Title: "кнопка2", 304 | URL: "https://ya.ru", 305 | Payload: "msg", 306 | }, 307 | }, 308 | }, 309 | want: test0, 310 | }, { 311 | name: "", 312 | response: getResp(0), 313 | args: args{ 314 | header: "заголовок", 315 | footer: "подвал", 316 | g: alice.List{ 317 | Images: []alice.Image{{ 318 | ImageID: "123", 319 | Title: "", 320 | Description: "", 321 | }, { 322 | ImageID: "123", 323 | Title: "", 324 | Description: "", 325 | }, { 326 | ImageID: "123", 327 | Title: "", 328 | Description: "", 329 | }, { 330 | ImageID: "123", 331 | Title: "", 332 | Description: "", 333 | }, { 334 | ImageID: "123", 335 | Title: "", 336 | Description: "", 337 | }, { 338 | ImageID: "123", 339 | Title: "", 340 | Description: "", 341 | }, 342 | }, 343 | }, 344 | }, 345 | want: test1, 346 | }, 347 | } 348 | for _, tt := range tests { 349 | t.Run(tt.name, func(t *testing.T) { 350 | resp := tt.response 351 | got := resp.List(tt.args.header, tt.args.footer, tt.args.g, tt.args.button...) 352 | assert.Equal(t, tt.want, got) 353 | }) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /images/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azzzak/alice/b2abde44a05897a7e92480b1844f57abe4b5d19b/images/health.png -------------------------------------------------------------------------------- /images/zhdun.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azzzak/alice/b2abde44a05897a7e92480b1844f57abe4b5d19b/images/zhdun.jpeg -------------------------------------------------------------------------------- /manual/README.md: -------------------------------------------------------------------------------- 1 | # Руководство 2 | 3 | Разберем простейший пример: 4 | 5 | ```Go 6 | package main 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/azzzak/alice" 12 | ) 13 | 14 | func main() { 15 | updates := alice.ListenForWebhook("/hook") 16 | go http.ListenAndServe(":3000", nil) 17 | 18 | updates.Loop(func(k alice.Kit) *alice.Response { 19 | req, resp := k.Init() 20 | if req.IsNewSession() { 21 | return resp.Text("привет") 22 | } 23 | return resp.Text(req.OriginalUtterance()) 24 | }) 25 | } 26 | ``` 27 | 28 | Функция `ListenForWebhook` регистрирует обработчик, который принимает входящие пакеты от Алисы. Функции передается путь и настройки. 29 | 30 | В простейшем случае путь будет `/`, но выбор осмысленного имени позволит запускать на одном домене несколько навыков. 31 | 32 | ```Go 33 | alice.ListenForWebhook("/hook") 34 | ``` 35 | 36 | Настройки задаются с помощью соответствующих функций. Например, чтобы изменить время ожидания ответа (по умолчанию это 3000 мс) есть функция `alice.Timeout`. 37 | 38 | ```Go 39 | alice.ListenForWebhook("/hook", alice.Timeout(2500)) 40 | ``` 41 | 42 | Следующая строка запускает горутину в которой работает сервер. 43 | 44 | ```Go 45 | go http.ListenAndServe(":3000", nil) 46 | ``` 47 | 48 | Вместо http-сервера можно запустить https-сервер. 49 | 50 | ```Go 51 | go http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil) 52 | ``` 53 | 54 | Однако разумно «спрятать» приложение за веб-сервером (вроде nginx), оставив последнему работу с сертификатами. 55 | 56 | Функция `ListenForWebhook` возвращает канал, который получает входящие пакеты. За манипуляции с ними отвечает метод `Loop`, принимающий функцию-обработчик. 57 | 58 | ```Go 59 | func(k alice.Kit) *alice.Response{...} 60 | ``` 61 | 62 | Разработчик наполняет тело функции кодом, который будет выполняться для каждой новой порции данных. Результат работы функции станет ответом Алисе. 63 | 64 | Метод `Init()` получает входящий пакет и заготовку исходящего из данных запроса. 65 | 66 | ```Go 67 | req, resp := k.Init() 68 | ``` 69 | 70 | С помощью различных методов из структуры запроса можно получить текст реплики пользователя, узнать даннные, извлеченные из запроса и пр. В примере используется два таких метода: один помогает выяснить было ли сообщение первым в сессии, другой — получает реплику пользователя. 71 | 72 | ```Go 73 | req.IsNewSession() 74 | ... 75 | req.OriginalUtterance() 76 | ``` 77 | 78 | Структура ответа уже готова к использованию. Для конструирования ответа доступны методы, которые можно объединять в цепочки. 79 | 80 | Полный список методов для работы с запросом и ответом есть в [документации](https://godoc.org/github.com/azzzak/alice). 81 | 82 | После того, как ответ подготовлен, его нужно отправить Алисе. В примере мы видим два варианта. Если это первое сообщение в сессии пользователь получит приветствие. 83 | 84 | ```Go 85 | return resp.Text("привет") 86 | ``` 87 | 88 | Во всех остальных случаях это будет повторение его собственной реплики. 89 | 90 | ```Go 91 | return resp.Text(req.OriginalUtterance()) 92 | ``` 93 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | // SimpleUtterance простая реплика. 11 | SimpleUtterance = "SimpleUtterance" 12 | 13 | // ButtonPressed нажатие кнопки. 14 | ButtonPressed = "ButtonPressed" 15 | ) 16 | 17 | // Request структура входящего сообщения. 18 | type Request struct { 19 | Meta struct { 20 | Locale string `json:"locale"` 21 | Timezone string `json:"timezone"` 22 | ClientID string `json:"client_id"` 23 | Interfaces struct { 24 | AccountLinking *struct{} `json:"account_linking"` 25 | Screen *struct{} `json:"screen"` 26 | } `json:"interfaces"` 27 | } `json:"meta"` 28 | 29 | LinkingComplete *struct{} `json:"account_linking_complete_event,omitenpty"` 30 | 31 | Request struct { 32 | Command string `json:"command"` 33 | OriginalUtterance string `json:"original_utterance"` 34 | Type string `json:"type"` 35 | Markup struct { 36 | DangerousContext *bool `json:"dangerous_context,omitempty"` 37 | } `json:"markup,omitempty"` 38 | Payload interface{} `json:"payload,omitempty"` 39 | NLU struct { 40 | Tokens []string `json:"tokens"` 41 | Entities []Entity `json:"entities,omitempty"` 42 | } `json:"nlu"` 43 | } `json:"request"` 44 | 45 | Session struct { 46 | New bool `json:"new"` 47 | MessageID int `json:"message_id"` 48 | SessionID string `json:"session_id"` 49 | SkillID string `json:"skill_id"` 50 | UserID string `json:"user_id"` 51 | } `json:"session"` 52 | 53 | State struct { 54 | Session interface{} `json:"session,omitempty"` 55 | } `json:"state,omitempty"` 56 | 57 | Version string `json:"version"` 58 | Bearer string 59 | } 60 | 61 | func (req *Request) clean() *Request { 62 | req.Meta.Interfaces = struct { 63 | AccountLinking *struct{} `json:"account_linking"` 64 | Screen *struct{} `json:"screen"` 65 | }{ 66 | nil, 67 | nil, 68 | } 69 | req.LinkingComplete = nil 70 | req.Request.Command = "" 71 | req.Request.OriginalUtterance = "" 72 | req.Request.Payload = nil 73 | req.Request.Markup = struct { 74 | DangerousContext *bool `json:"dangerous_context,omitempty"` 75 | }{ 76 | nil, 77 | } 78 | req.Request.NLU = struct { 79 | Tokens []string `json:"tokens"` 80 | Entities []Entity `json:"entities,omitempty"` 81 | }{ 82 | []string{}, 83 | []Entity{}, 84 | } 85 | req.Bearer = "" 86 | return req 87 | } 88 | 89 | // Locale язык в формате POSIX. 90 | func (req *Request) Locale() string { 91 | return req.Meta.Locale 92 | } 93 | 94 | // Timezone название часового пояса. 95 | func (req *Request) Timezone() string { 96 | return req.Meta.Timezone 97 | } 98 | 99 | // ClientID идентификатор клиентского устройства или приложения. Не рекомендуется использовать. 100 | func (req *Request) ClientID() string { 101 | return req.Meta.ClientID 102 | } 103 | 104 | // CanAccountLinking поддерживает ли устройство пользователя создание связки аккаунтов. 105 | func (req *Request) CanAccountLinking() bool { 106 | return req.Meta.Interfaces.AccountLinking != nil 107 | } 108 | 109 | // HasScreen имеет ли устройство пользователя экран. 110 | func (req *Request) HasScreen() bool { 111 | return req.Meta.Interfaces.Screen != nil 112 | } 113 | 114 | // IsLinkingComplete связка аккаунтов создана. 115 | func (req *Request) IsLinkingComplete() bool { 116 | return req.LinkingComplete != nil 117 | } 118 | 119 | // Command реплика пользователя, преобразованная Алисой. В частности, текст очищается от знаков препинания, а числительные преобразуются в числа. 120 | func (req *Request) Command() string { 121 | return req.Request.Command 122 | } 123 | 124 | // OriginalUtterance реплика пользователя без изменений. 125 | func (req *Request) OriginalUtterance() string { 126 | return req.Request.OriginalUtterance 127 | } 128 | 129 | // Text синоним метода OriginalUtterance(). 130 | func (req *Request) Text() string { 131 | return req.OriginalUtterance() 132 | } 133 | 134 | // Type тип запроса (реплика или нажатие кнопки). 135 | func (req *Request) Type() string { 136 | return req.Request.Type 137 | } 138 | 139 | // DangerousContext флаг опасной реплики. 140 | func (req *Request) DangerousContext() bool { 141 | if req.Request.Markup.DangerousContext != nil { 142 | return *req.Request.Markup.DangerousContext 143 | } 144 | return false 145 | } 146 | 147 | // Payload возвращает map[string]interface{} с данными, которые были переданы в Payload кнопки. Подходит для Payload, оформленного в виде json-объекта. Если Payload простая строка следует использовать метод PayloadString(). Если в запросе нет Payload возвращается ошибка. 148 | func (req *Request) Payload() (map[string]interface{}, error) { 149 | if req.Request.Payload != nil { 150 | return req.Request.Payload.(map[string]interface{}), nil 151 | } 152 | return nil, errors.New("Payload is nil") 153 | } 154 | 155 | // PayloadString возвращает строку, которая была передана в Payload кнопки. Для сложных объектов следует использовать метод Payload(). Если в запросе нет Payload возвращается ошибка. 156 | func (req *Request) PayloadString() (string, error) { 157 | if req.Request.Payload != nil { 158 | return req.Request.Payload.(string), nil 159 | } 160 | return "", errors.New("Payload is nil") 161 | } 162 | 163 | // Tokens массив слов из реплики. 164 | func (req *Request) Tokens() []string { 165 | return req.Request.NLU.Tokens 166 | } 167 | 168 | // IsNewSession отправлена реплика в рамках нового разговора или уже начатого. 169 | func (req *Request) IsNewSession() bool { 170 | return req.Session.New 171 | } 172 | 173 | // MessageID счетчик сообщений в рамках сессии. 174 | func (req *Request) MessageID() int { 175 | return req.Session.MessageID 176 | } 177 | 178 | // SessionID идентификатор сессии. 179 | func (req *Request) SessionID() string { 180 | return req.Session.SessionID 181 | } 182 | 183 | // SkillID идентификатор навыка. 184 | func (req *Request) SkillID() string { 185 | return req.Session.SkillID 186 | } 187 | 188 | // UserID идентификатор пользователя. 189 | func (req *Request) UserID() string { 190 | return req.Session.UserID 191 | } 192 | 193 | // Ver версия протокола. 194 | func (req *Request) Ver() string { 195 | return req.Version 196 | } 197 | 198 | // State.Session Состояние сессии. 199 | func (req *Request) StateSession(key string) interface{} { 200 | if req.State.Session == nil { 201 | return nil 202 | } 203 | session := req.State.Session.(map[string]interface{}) 204 | 205 | return session[key] 206 | } 207 | 208 | // State.Session Состояние сессии json строкой 209 | func (req *Request) StateSessionAsJson() (string, error) { 210 | data, err := json.Marshal(req.State.Session) 211 | 212 | return string(data), err 213 | } 214 | 215 | 216 | // AuthToken токен, полученный при связке аккаунтов. 217 | func (req *Request) AuthToken() string { 218 | if req.Bearer != "" { 219 | return strings.TrimPrefix(req.Bearer, "Bearer ") 220 | } 221 | return "" 222 | } 223 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/azzzak/alice" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRequest_Locale(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | request *alice.Request 15 | want string 16 | }{ 17 | { 18 | name: "", 19 | request: getReq(0), 20 | want: "ru-RU", 21 | }, { 22 | name: "", 23 | request: getReq(1), 24 | want: "ru-RU", 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | req := tt.request 30 | if got := req.Locale(); got != tt.want { 31 | t.Errorf("Request.Locale() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestRequest_Timezone(t *testing.T) { 38 | tests := []struct { 39 | name string 40 | request *alice.Request 41 | want string 42 | }{ 43 | { 44 | name: "", 45 | request: getReq(0), 46 | want: "UTC", 47 | }, { 48 | name: "", 49 | request: getReq(1), 50 | want: "UTC", 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | req := tt.request 56 | if got := req.Timezone(); got != tt.want { 57 | t.Errorf("Request.Timezone() = %v, want %v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestRequest_ClientID(t *testing.T) { 64 | tests := []struct { 65 | name string 66 | request *alice.Request 67 | want string 68 | }{ 69 | { 70 | name: "", 71 | request: getReq(0), 72 | want: "ru.yandex.searchplugin/7.16 (none none; android 4.4.2)", 73 | }, { 74 | name: "", 75 | request: getReq(1), 76 | want: "ru.yandex.searchplugin/7.16 (none none; android 4.4.2)", 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | req := tt.request 82 | if got := req.ClientID(); got != tt.want { 83 | t.Errorf("Request.ClientID() = %v, want %v", got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestRequest_HasScreen(t *testing.T) { 90 | tests := []struct { 91 | name string 92 | request *alice.Request 93 | want bool 94 | }{ 95 | { 96 | name: "", 97 | request: getReq(0), 98 | want: true, 99 | }, { 100 | name: "", 101 | request: getReq(1), 102 | want: false, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | req := tt.request 108 | if got := req.HasScreen(); got != tt.want { 109 | t.Errorf("Request.HasScreen() = %v, want %v", got, tt.want) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestRequest_Command(t *testing.T) { 116 | tests := []struct { 117 | name string 118 | request *alice.Request 119 | want string 120 | }{ 121 | { 122 | name: "", 123 | request: getReq(0), 124 | want: "съешь еще этих мягких французских булок", 125 | }, { 126 | name: "", 127 | request: getReq(1), 128 | want: "", 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | req := tt.request 134 | if got := req.Command(); got != tt.want { 135 | t.Errorf("Request.Command() = %v, want %v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestRequest_OriginalUtterance(t *testing.T) { 142 | tests := []struct { 143 | name string 144 | request *alice.Request 145 | want string 146 | }{ 147 | { 148 | name: "", 149 | request: getReq(0), 150 | want: "съешь еще этих мягких французских булок", 151 | }, { 152 | name: "", 153 | request: getReq(1), 154 | want: "", 155 | }, 156 | } 157 | for _, tt := range tests { 158 | t.Run(tt.name, func(t *testing.T) { 159 | req := tt.request 160 | if got := req.OriginalUtterance(); got != tt.want { 161 | t.Errorf("Request.OriginalUtterance() = %v, want %v", got, tt.want) 162 | } 163 | }) 164 | } 165 | } 166 | 167 | func TestRequest_Text(t *testing.T) { 168 | tests := []struct { 169 | name string 170 | request *alice.Request 171 | want string 172 | }{ 173 | { 174 | name: "", 175 | request: getReq(0), 176 | want: "съешь еще этих мягких французских булок", 177 | }, { 178 | name: "", 179 | request: getReq(1), 180 | want: "", 181 | }, 182 | } 183 | for _, tt := range tests { 184 | t.Run(tt.name, func(t *testing.T) { 185 | req := tt.request 186 | if got := req.Text(); got != tt.want { 187 | t.Errorf("Request.Text() = %v, want %v", got, tt.want) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func TestRequest_Type(t *testing.T) { 194 | tests := []struct { 195 | name string 196 | request *alice.Request 197 | want string 198 | }{ 199 | { 200 | name: "", 201 | request: getReq(0), 202 | want: "SimpleUtterance", 203 | }, { 204 | name: "", 205 | request: getReq(1), 206 | want: "ButtonPressed", 207 | }, 208 | } 209 | for _, tt := range tests { 210 | t.Run(tt.name, func(t *testing.T) { 211 | req := tt.request 212 | if got := req.Type(); got != tt.want { 213 | t.Errorf("Request.Type() = %v, want %v", got, tt.want) 214 | } 215 | }) 216 | } 217 | } 218 | 219 | func TestRequest_DangerousContext(t *testing.T) { 220 | tests := []struct { 221 | name string 222 | request *alice.Request 223 | want bool 224 | }{ 225 | { 226 | name: "", 227 | request: getReq(0), 228 | want: false, 229 | }, { 230 | name: "", 231 | request: getReq(1), 232 | want: false, 233 | }, 234 | } 235 | for _, tt := range tests { 236 | t.Run(tt.name, func(t *testing.T) { 237 | req := tt.request 238 | if got := req.DangerousContext(); got != tt.want { 239 | t.Errorf("Request.DangerousContext() = %v, want %v", got, tt.want) 240 | } 241 | }) 242 | } 243 | } 244 | 245 | func TestRequest_Payload(t *testing.T) { 246 | tests := []struct { 247 | name string 248 | request *alice.Request 249 | want map[string]interface{} 250 | wantErr bool 251 | }{ 252 | { 253 | name: "", 254 | request: getReq(0), 255 | want: nil, 256 | wantErr: true, 257 | }, { 258 | name: "", 259 | request: getReq(1), 260 | want: map[string]interface{}{"msg": "ok"}, 261 | wantErr: false, 262 | }, 263 | } 264 | for _, tt := range tests { 265 | t.Run(tt.name, func(t *testing.T) { 266 | req := tt.request 267 | got, err := req.Payload() 268 | if (err != nil) != tt.wantErr { 269 | t.Errorf("Request.Payload() error = %v, wantErr %v", err, tt.wantErr) 270 | return 271 | } 272 | assert.Equal(t, tt.want, got) 273 | }) 274 | } 275 | } 276 | 277 | func TestRequest_PayloadString(t *testing.T) { 278 | tests := []struct { 279 | name string 280 | request *alice.Request 281 | want string 282 | wantErr bool 283 | }{ 284 | { 285 | name: "", 286 | request: getReq(0), 287 | want: "", 288 | wantErr: true, 289 | }, { 290 | name: "", 291 | request: getReq(2), 292 | want: "msg", 293 | wantErr: false, 294 | }, 295 | } 296 | for _, tt := range tests { 297 | t.Run(tt.name, func(t *testing.T) { 298 | req := tt.request 299 | got, err := req.PayloadString() 300 | if (err != nil) != tt.wantErr { 301 | t.Errorf("Request.PayloadString() error = %v, wantErr %v", err, tt.wantErr) 302 | return 303 | } 304 | assert.Equal(t, tt.want, got) 305 | }) 306 | } 307 | } 308 | 309 | func TestRequest_Tokens(t *testing.T) { 310 | tests := []struct { 311 | name string 312 | request *alice.Request 313 | want []string 314 | }{ 315 | { 316 | name: "", 317 | request: getReq(0), 318 | want: []string{"съешь", "еще", "этих", "мягких", "французских", "булок"}, 319 | }, { 320 | name: "", 321 | request: getReq(1), 322 | want: []string{}, 323 | }, 324 | } 325 | for _, tt := range tests { 326 | t.Run(tt.name, func(t *testing.T) { 327 | req := tt.request 328 | got := req.Tokens() 329 | assert.Equal(t, tt.want, got) 330 | }) 331 | } 332 | } 333 | 334 | func TestRequest_IsNewSession(t *testing.T) { 335 | tests := []struct { 336 | name string 337 | request *alice.Request 338 | want bool 339 | }{ 340 | { 341 | name: "", 342 | request: getReq(0), 343 | want: true, 344 | }, { 345 | name: "", 346 | request: getReq(1), 347 | want: false, 348 | }, 349 | } 350 | for _, tt := range tests { 351 | t.Run(tt.name, func(t *testing.T) { 352 | req := tt.request 353 | if got := req.IsNewSession(); got != tt.want { 354 | t.Errorf("Request.IsNewSession() = %v, want %v", got, tt.want) 355 | } 356 | }) 357 | } 358 | } 359 | 360 | func TestRequest_MessageID(t *testing.T) { 361 | tests := []struct { 362 | name string 363 | request *alice.Request 364 | want int 365 | }{ 366 | { 367 | name: "", 368 | request: getReq(0), 369 | want: 0, 370 | }, { 371 | name: "", 372 | request: getReq(1), 373 | want: 1, 374 | }, 375 | } 376 | for _, tt := range tests { 377 | t.Run(tt.name, func(t *testing.T) { 378 | req := tt.request 379 | if got := req.MessageID(); got != tt.want { 380 | t.Errorf("Request.MessageID() = %v, want %v", got, tt.want) 381 | } 382 | }) 383 | } 384 | } 385 | 386 | func TestRequest_SessionID(t *testing.T) { 387 | tests := []struct { 388 | name string 389 | request *alice.Request 390 | want string 391 | }{ 392 | { 393 | name: "", 394 | request: getReq(0), 395 | want: "e19e8eee-ae065e8-36e3f907-567a814b", 396 | }, { 397 | name: "", 398 | request: getReq(1), 399 | want: "eeb9fa7f-940e2502-1fbf9dfb-9448a1a9", 400 | }, 401 | } 402 | for _, tt := range tests { 403 | t.Run(tt.name, func(t *testing.T) { 404 | req := tt.request 405 | if got := req.SessionID(); got != tt.want { 406 | t.Errorf("Request.SessionID() = %v, want %v", got, tt.want) 407 | } 408 | }) 409 | } 410 | } 411 | 412 | func TestRequest_SkillID(t *testing.T) { 413 | tests := []struct { 414 | name string 415 | request *alice.Request 416 | want string 417 | }{ 418 | { 419 | name: "", 420 | request: getReq(0), 421 | want: "e03f8d5b-35ef-4d57-9450-b721ca17a6c3", 422 | }, { 423 | name: "", 424 | request: getReq(1), 425 | want: "e03f8d5b-35ef-4d57-9450-b721ca17a6c3", 426 | }, 427 | } 428 | for _, tt := range tests { 429 | t.Run(tt.name, func(t *testing.T) { 430 | req := tt.request 431 | if got := req.SkillID(); got != tt.want { 432 | t.Errorf("Request.SkillID() = %v, want %v", got, tt.want) 433 | } 434 | }) 435 | } 436 | } 437 | 438 | func TestRequest_UserID(t *testing.T) { 439 | tests := []struct { 440 | name string 441 | request *alice.Request 442 | want string 443 | }{ 444 | { 445 | name: "", 446 | request: getReq(0), 447 | want: "03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0", 448 | }, { 449 | name: "", 450 | request: getReq(1), 451 | want: "03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0", 452 | }, 453 | } 454 | for _, tt := range tests { 455 | t.Run(tt.name, func(t *testing.T) { 456 | req := tt.request 457 | if got := req.UserID(); got != tt.want { 458 | t.Errorf("Request.UserID() = %v, want %v", got, tt.want) 459 | } 460 | }) 461 | } 462 | } 463 | 464 | func TestRequest_Ver(t *testing.T) { 465 | tests := []struct { 466 | name string 467 | request *alice.Request 468 | want string 469 | }{ 470 | { 471 | name: "", 472 | request: getReq(0), 473 | want: "1.0", 474 | }, { 475 | name: "", 476 | request: getReq(1), 477 | want: "1.0", 478 | }, 479 | } 480 | for _, tt := range tests { 481 | t.Run(tt.name, func(t *testing.T) { 482 | req := tt.request 483 | if got := req.Ver(); got != tt.want { 484 | t.Errorf("Request.Ver() = %v, want %v", got, tt.want) 485 | } 486 | }) 487 | } 488 | } 489 | 490 | func TestRequest_StateSession(t *testing.T) { 491 | tests := map[string]struct { 492 | request *alice.Request 493 | want interface{} 494 | }{ 495 | "when state is empty 0": { 496 | request: getReq(0), 497 | want: nil, 498 | }, 499 | "when state is empty 1":{ 500 | request: getReq(1), 501 | want: nil, 502 | }, 503 | "when state is empty 2":{ 504 | request: getReq(2), 505 | want: nil, 506 | }, 507 | } 508 | for name, tt := range tests { 509 | t.Run(name, func(t *testing.T) { 510 | req := tt.request 511 | got := req.StateSession(""); 512 | if !assert.Equal(t, tt.want, got) { 513 | t.Errorf("Request.StateSession() = %v, want %v", got, tt.want) 514 | } 515 | 516 | }) 517 | } 518 | t.Run("when state is struct", func(t *testing.T) { 519 | req := getReq(3) 520 | want := make(map[string]interface{}) 521 | want["int_value"] = 42 522 | 523 | assert.Equal(t, 42.0, req.StateSession("int_value")) 524 | assert.Equal(t, "exampleString", req.StateSession("string_value")) 525 | assert.Equal(t, []interface{}{1.0,2.0,3.0,4.0}, req.StateSession("array_value")) 526 | assert.Equal(t, map[string]interface{}{"one":"one"}, req.StateSession("struct_value")) 527 | stateJson, err := req.StateSessionAsJson() 528 | if assert.NoError(t, err) { 529 | assert.Equal(t, `{"array_value":[1,2,3,4],"int_value":42,"string_value":"exampleString","struct_value":{"one":"one"}}`, stateJson) 530 | } 531 | }) 532 | } 533 | 534 | func getReq(n int) *alice.Request { 535 | source := []string{`{"meta":{"client_id":"ru.yandex.searchplugin/7.16 (none none; android 4.4.2)","interfaces":{"account_linking":{},"payments":{},"screen":{}},"locale":"ru-RU","timezone":"UTC"},"request":{"command":"съешь еще этих мягких французских булок","nlu":{"entities":[],"tokens":["съешь","еще","этих","мягких","французских","булок"]},"original_utterance":"съешь еще этих мягких французских булок","type":"SimpleUtterance"},"session":{"message_id":0,"new":true,"session_id":"e19e8eee-ae065e8-36e3f907-567a814b","skill_id":"e03f8d5b-35ef-4d57-9450-b721ca17a6c3","user_id":"03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0"},"version":"1.0"}`, 536 | 537 | `{"meta":{"client_id":"ru.yandex.searchplugin/7.16 (none none; android 4.4.2)","interfaces":{"account_linking":{},"payments":{}},"locale":"ru-RU","timezone":"UTC"},"request":{"nlu":{"entities":[],"tokens":[]},"payload":{"msg":"ok"},"type":"ButtonPressed"},"session":{"message_id":1,"new":false,"session_id":"eeb9fa7f-940e2502-1fbf9dfb-9448a1a9","skill_id":"e03f8d5b-35ef-4d57-9450-b721ca17a6c3","user_id":"03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0"},"version":"1.0"}`, 538 | 539 | `{"meta":{"client_id":"ru.yandex.searchplugin/7.16 (none none; android 4.4.2)","interfaces":{"account_linking":{},"payments":{}},"locale":"ru-RU","timezone":"UTC"},"request":{"nlu":{"entities":[],"tokens":[]},"payload":"msg","type":"ButtonPressed"},"session":{"message_id":1,"new":false,"session_id":"eeb9fa7f-940e2502-1fbf9dfb-9448a1a9","skill_id":"e03f8d5b-35ef-4d57-9450-b721ca17a6c3","user_id":"03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0"},"version":"1.0"}`, 540 | 541 | `{"meta":{"client_id":"ru.yandex.searchplugin/7.16 (none none; android 4.4.2)","interfaces":{"account_linking":{},"payments":{}},"locale":"ru-RU","timezone":"UTC"},"request":{"nlu":{"entities":[],"tokens":[]},"payload":"msg","type":"ButtonPressed"},"session":{"message_id":1,"new":false,"session_id":"eeb9fa7f-940e2502-1fbf9dfb-9448a1a9","skill_id":"e03f8d5b-35ef-4d57-9450-b721ca17a6c3","user_id":"03B1D487CAA1C7EBF80A195491B78ACA0AC9934CDFB12A29D063A8329BC42BF0"},"state":{"session":{"array_value":[1,2,3,4],"int_value":42,"string_value":"exampleString","struct_value":{"one":"one"}}},"version":"1.0"}`, 542 | } 543 | 544 | var req = new(alice.Request) 545 | json.Unmarshal([]byte(source[n]), req) 546 | return req 547 | } 548 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package alice 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | 9 | "github.com/azzzak/alice/effects" 10 | ) 11 | 12 | // Response структура исходящего сообщения. 13 | type Response struct { 14 | Response *struct { 15 | Text string `json:"text"` 16 | TTS string `json:"tts,omitempty"` 17 | Card *Card `json:"card,omitempty"` 18 | Buttons []Button `json:"buttons,omitempty"` 19 | EndSession bool `json:"end_session"` 20 | } `json:"response,omitempty"` 21 | 22 | StartAccountLinking *struct{} `json:"start_account_linking,omitempty"` 23 | 24 | Session struct { 25 | MessageID int `json:"message_id"` 26 | SessionID string `json:"session_id"` 27 | UserID string `json:"user_id"` 28 | } `json:"session"` 29 | 30 | SessionState interface{} `json:"session_state,omitempty"` 31 | 32 | Version string `json:"version"` 33 | } 34 | 35 | func (resp *Response) clean() *Response { 36 | resp.Response = &struct { 37 | Text string `json:"text"` 38 | TTS string `json:"tts,omitempty"` 39 | Card *Card `json:"card,omitempty"` 40 | Buttons []Button `json:"buttons,omitempty"` 41 | EndSession bool `json:"end_session"` 42 | }{} 43 | resp.StartAccountLinking = nil 44 | resp.SessionState = nil 45 | return resp 46 | } 47 | 48 | func (resp *Response) prepareResponse(req *Request) *Response { 49 | resp.Session.MessageID = req.Session.MessageID 50 | resp.Session.SessionID = req.Session.SessionID 51 | resp.Session.UserID = req.Session.UserID 52 | resp.Version = "1.0" 53 | return resp 54 | } 55 | 56 | // StartAuthorization начать создание связки аккаунтов и показать пользователю карточку авторизации. 57 | func (resp *Response) StartAuthorization() *Response { 58 | // resp.Response = Response{} 59 | resp.StartAccountLinking = &struct{}{} 60 | resp.Response = nil 61 | return resp 62 | } 63 | 64 | // Text добавляет строку к текстовому ответу. Если передано несколько строк, они будут разделены пробелом. 65 | func (resp *Response) Text(s ...string) *Response { 66 | resp.Response.Text += strings.Join(s, " ") 67 | return resp 68 | } 69 | 70 | // RandomText добавляет к текстовому ответу случайную строку из числа предложенных. 71 | func (resp *Response) RandomText(s ...string) *Response { 72 | ix := rand.Intn(len(s)) 73 | resp.Response.Text += s[ix] 74 | return resp 75 | } 76 | 77 | // Space добавляет пробел к текстовому ответу. 78 | func (resp *Response) Space() *Response { 79 | resp.Response.Text += " " 80 | return resp 81 | } 82 | 83 | // S синоним метода Space(). 84 | func (resp *Response) S() *Response { 85 | return resp.Space() 86 | } 87 | 88 | // ResetText обнуляет текстовый ответ. 89 | func (resp *Response) ResetText() *Response { 90 | resp.Response.Text = "" 91 | return resp 92 | } 93 | 94 | // TTS добавляет строку к TTS. Если передано несколько строк, они будут разделены пробелом. 95 | func (resp *Response) TTS(tts ...string) *Response { 96 | resp.Response.TTS += strings.Join(tts, " ") 97 | return resp 98 | } 99 | 100 | // TextWithTTS добавляет строки к текстовому ответу и к TTS. 101 | func (resp *Response) TextWithTTS(s, tts string) *Response { 102 | return resp.Text(s).TTS(tts) 103 | } 104 | 105 | // Pause добавляет паузу к TTS (знак "-"). 106 | func (resp *Response) Pause(n int) *Response { 107 | if n < 1 { 108 | return resp 109 | } 110 | 111 | p := strings.Repeat("- ", n) 112 | resp.Response.TTS += " " + p 113 | return resp 114 | } 115 | 116 | // Effect накладывает звуковой эффект на TTS. 117 | func (resp *Response) Effect(effect string) *Response { 118 | e := fmt.Sprintf("", effect) 119 | resp.Response.TTS += e 120 | return resp 121 | } 122 | 123 | // NoEffect отключает наложенный на TTS эффект. 124 | func (resp *Response) NoEffect() *Response { 125 | return resp.Effect(effects.NoEffect) 126 | } 127 | 128 | // Sound добавляет к TTS звук из библиотеки Алисы. 129 | func (resp *Response) Sound(sound string) *Response { 130 | sound = strings.TrimSuffix(sound, ".opus") 131 | s := fmt.Sprintf("", sound) 132 | resp.Response.TTS += s 133 | return resp 134 | } 135 | 136 | // CustomSound добавляет к TTS собственный звук. 137 | func (resp *Response) CustomSound(skill, sound string) *Response { 138 | sound = strings.TrimSuffix(sound, ".opus") 139 | s := fmt.Sprintf("", skill, sound) 140 | resp.Response.TTS += s 141 | return resp 142 | } 143 | 144 | // ResetTTS обнуляет TTS. 145 | func (resp *Response) ResetTTS() *Response { 146 | resp.Response.TTS = "" 147 | return resp 148 | } 149 | 150 | // Button структура кнопки. 151 | type Button struct { 152 | Title string `json:"title"` 153 | URL string `json:"url,omitempty"` 154 | Payload interface{} `json:"payload,omitempty"` 155 | Hide bool `json:"hide,omitempty"` 156 | } 157 | 158 | // NewButton создает кнопку. Payload может быть проигнорирован. Если задано больше одного payload используется только первый. 159 | func NewButton(title, url string, hide bool, payload ...interface{}) Button { 160 | var p interface{} 161 | if len(payload) > 0 { 162 | p = payload[0] 163 | } 164 | 165 | return Button{ 166 | Title: title, 167 | URL: url, 168 | Hide: hide, 169 | Payload: p, 170 | } 171 | } 172 | 173 | // Button создает кнопку и добавляет ее в ответ. Payload может быть проигнорирован. Если задано больше одного payload используется только первый. 174 | func (resp *Response) Button(title, url string, hide bool, payload ...interface{}) *Response { 175 | var p interface{} 176 | if len(payload) > 0 { 177 | p = payload[0] 178 | } 179 | 180 | resp.Buttons(NewButton(title, url, hide, p)) 181 | return resp 182 | } 183 | 184 | // Buttons добавляет одну или несколько кнопок в ответ. 185 | func (resp *Response) Buttons(buttons ...Button) *Response { 186 | resp.Response.Buttons = append(resp.Response.Buttons, buttons...) 187 | return resp 188 | } 189 | 190 | // EndSession флаг о закрытии сессии. 191 | func (resp *Response) EndSession() *Response { 192 | resp.Response.EndSession = true 193 | return resp 194 | } 195 | 196 | // Phrase структура фразы. 197 | type Phrase struct { 198 | Text string 199 | TTS string 200 | } 201 | 202 | // NewPhrase создает фразу с текстом и TTS. 203 | func NewPhrase(text, tts string) Phrase { 204 | return Phrase{ 205 | Text: text, 206 | TTS: tts, 207 | } 208 | } 209 | 210 | // Phrase добавляет к тексту и TTS ответа данные фразы. 211 | func (resp *Response) Phrase(p ...Phrase) *Response { 212 | ix := rand.Intn(len(p)) 213 | resp.Response.Text += p[ix].Text 214 | resp.Response.TTS += p[ix].TTS 215 | return resp 216 | } 217 | 218 | // RandomPhrase добавляет к тексту и TTS ответа данные случайной фразы из числа предложенных. 219 | func (resp *Response) RandomPhrase(p ...Phrase) *Response { 220 | ix := rand.Intn(len(p)) 221 | resp.Response.Text += p[ix].Text 222 | resp.Response.TTS += p[ix].TTS 223 | return resp 224 | } 225 | 226 | // AddSessionState добавляет па 227 | func (resp *Response) AddSessionState(key string, data interface{}) error { 228 | if resp.SessionState == nil { 229 | resp.SessionState = make(map[string]interface{}) 230 | } 231 | 232 | if stateBag, ok := resp.SessionState.(map[string]interface{}); ok { 233 | stateBag[key] = data 234 | resp.SessionState = stateBag 235 | } else { 236 | return errors.New(fmt.Sprintf("session_state: can't set data because session_state have type %T", 237 | resp.SessionState)) 238 | } 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package alice_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/azzzak/alice" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestResponse_Text(t *testing.T) { 12 | test0 := getResp(0) 13 | test0.Response.Text = "тест" 14 | 15 | test1 := getResp(0) 16 | test1.Response.Text = "раз два три" 17 | 18 | test2 := getResp(0) 19 | test2.Response.Text = "" 20 | 21 | tests := []struct { 22 | name string 23 | response *alice.Response 24 | args []string 25 | want *alice.Response 26 | }{ 27 | { 28 | name: "", 29 | response: getResp(0), 30 | args: []string{"тест"}, 31 | want: test0, 32 | }, 33 | { 34 | name: "", 35 | response: getResp(0), 36 | args: []string{"раз", "два", "три"}, 37 | want: test1, 38 | }, { 39 | name: "", 40 | response: getResp(0), 41 | args: []string{""}, 42 | want: test2, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | resp := tt.response 48 | assert.Equal(t, tt.want, resp.Text(tt.args...)) 49 | }) 50 | } 51 | } 52 | 53 | func TestResponse_RandomText(t *testing.T) { 54 | test0 := getResp(0) 55 | test0.Response.Text = "тест" 56 | 57 | test1 := getResp(0) 58 | test1.Response.Text = "" 59 | 60 | tests := []struct { 61 | name string 62 | response *alice.Response 63 | args []string 64 | want *alice.Response 65 | }{ 66 | { 67 | name: "", 68 | response: getResp(0), 69 | args: []string{"тест"}, 70 | want: test0, 71 | }, { 72 | name: "", 73 | response: getResp(0), 74 | args: []string{""}, 75 | want: test1, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | resp := tt.response 81 | assert.Equal(t, tt.want, resp.RandomText(tt.args...)) 82 | }) 83 | } 84 | } 85 | 86 | func TestResponse_Space(t *testing.T) { 87 | test0 := getResp(0) 88 | test0.Response.Text = " " 89 | 90 | tests := []struct { 91 | name string 92 | response *alice.Response 93 | want *alice.Response 94 | }{ 95 | { 96 | name: "", 97 | response: getResp(0), 98 | want: test0, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | resp := tt.response 104 | assert.Equal(t, tt.want, resp.Space()) 105 | }) 106 | } 107 | } 108 | 109 | func TestResponse_S(t *testing.T) { 110 | test0 := getResp(0) 111 | test0.Response.Text = " " 112 | 113 | tests := []struct { 114 | name string 115 | response *alice.Response 116 | want *alice.Response 117 | }{ 118 | { 119 | name: "", 120 | response: getResp(0), 121 | want: test0, 122 | }, 123 | } 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | resp := tt.response 127 | assert.Equal(t, tt.want, resp.S()) 128 | }) 129 | } 130 | } 131 | 132 | func TestResponse_ResetText(t *testing.T) { 133 | test0 := getResp(0) 134 | test0.Response.Text = "тест" 135 | 136 | tests := []struct { 137 | name string 138 | response *alice.Response 139 | want *alice.Response 140 | }{ 141 | { 142 | name: "", 143 | response: test0, 144 | want: getResp(0), 145 | }, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | resp := tt.response 150 | assert.Equal(t, tt.want, resp.ResetText()) 151 | }) 152 | } 153 | } 154 | 155 | func TestResponse_TTS(t *testing.T) { 156 | test0 := getResp(0) 157 | test0.Response.TTS = "тест" 158 | 159 | test1 := getResp(0) 160 | test1.Response.TTS = "раз два три" 161 | 162 | test2 := getResp(0) 163 | test2.Response.TTS = "" 164 | 165 | tests := []struct { 166 | name string 167 | response *alice.Response 168 | args []string 169 | want *alice.Response 170 | }{ 171 | { 172 | name: "", 173 | response: getResp(0), 174 | args: []string{"тест"}, 175 | want: test0, 176 | }, 177 | { 178 | name: "", 179 | response: getResp(0), 180 | args: []string{"раз", "два", "три"}, 181 | want: test1, 182 | }, { 183 | name: "", 184 | response: getResp(0), 185 | args: []string{""}, 186 | want: test2, 187 | }, 188 | } 189 | for _, tt := range tests { 190 | t.Run(tt.name, func(t *testing.T) { 191 | resp := tt.response 192 | assert.Equal(t, tt.want, resp.TTS(tt.args...)) 193 | }) 194 | } 195 | } 196 | 197 | func TestResponse_TextWithTTS(t *testing.T) { 198 | test0 := getResp(0) 199 | test0.Response.Text = "тест" 200 | test0.Response.TTS = "ттс" 201 | 202 | type args struct { 203 | s string 204 | tts string 205 | } 206 | tests := []struct { 207 | name string 208 | response *alice.Response 209 | args args 210 | want *alice.Response 211 | }{ 212 | { 213 | name: "", 214 | response: getResp(0), 215 | args: args{"тест", "ттс"}, 216 | want: test0, 217 | }, 218 | } 219 | for _, tt := range tests { 220 | t.Run(tt.name, func(t *testing.T) { 221 | resp := tt.response 222 | assert.Equal(t, tt.want, resp.TextWithTTS(tt.args.s, tt.args.tts)) 223 | }) 224 | } 225 | } 226 | 227 | func TestResponse_Pause(t *testing.T) { 228 | test0 := getResp(0) 229 | test0.Response.TTS = " - - - - - " 230 | 231 | tests := []struct { 232 | name string 233 | response *alice.Response 234 | args int 235 | want *alice.Response 236 | }{ 237 | { 238 | name: "", 239 | response: getResp(0), 240 | args: 5, 241 | want: test0, 242 | }, 243 | { 244 | name: "", 245 | response: getResp(0), 246 | args: 0, 247 | want: getResp(0), 248 | }, 249 | } 250 | for _, tt := range tests { 251 | t.Run(tt.name, func(t *testing.T) { 252 | resp := tt.response 253 | assert.Equal(t, tt.want, resp.Pause(tt.args)) 254 | }) 255 | } 256 | } 257 | 258 | func TestResponse_Effect(t *testing.T) { 259 | test0 := getResp(0) 260 | test0.Response.TTS = `` 261 | 262 | tests := []struct { 263 | name string 264 | response *alice.Response 265 | args string 266 | want *alice.Response 267 | }{ 268 | { 269 | name: "", 270 | response: getResp(0), 271 | args: "pulse", 272 | want: test0, 273 | }, 274 | } 275 | for _, tt := range tests { 276 | t.Run(tt.name, func(t *testing.T) { 277 | resp := tt.response 278 | assert.Equal(t, tt.want, resp.Effect(tt.args)) 279 | }) 280 | } 281 | } 282 | 283 | func TestResponse_NoEffect(t *testing.T) { 284 | test0 := getResp(0) 285 | test0.Response.TTS = `` 286 | 287 | tests := []struct { 288 | name string 289 | response *alice.Response 290 | want *alice.Response 291 | }{ 292 | { 293 | name: "", 294 | response: getResp(0), 295 | want: test0, 296 | }, 297 | } 298 | for _, tt := range tests { 299 | t.Run(tt.name, func(t *testing.T) { 300 | resp := tt.response 301 | assert.Equal(t, tt.want, resp.NoEffect()) 302 | }) 303 | } 304 | } 305 | 306 | func TestResponse_Sound(t *testing.T) { 307 | test0 := getResp(0) 308 | test0.Response.TTS = `` 309 | 310 | tests := []struct { 311 | name string 312 | response *alice.Response 313 | args string 314 | want *alice.Response 315 | }{ 316 | { 317 | name: "", 318 | response: getResp(0), 319 | args: "alice-sounds-things-chainsaw-1.opus", 320 | want: test0, 321 | }, { 322 | name: "", 323 | response: getResp(0), 324 | args: "alice-sounds-things-chainsaw-1", 325 | want: test0, 326 | }, 327 | } 328 | for _, tt := range tests { 329 | t.Run(tt.name, func(t *testing.T) { 330 | resp := tt.response 331 | assert.Equal(t, tt.want, resp.Sound(tt.args)) 332 | }) 333 | } 334 | } 335 | 336 | func TestResponse_CustomSound(t *testing.T) { 337 | test0 := getResp(0) 338 | test0.Response.TTS = `` 339 | 340 | type args struct { 341 | skill string 342 | sound string 343 | } 344 | tests := []struct { 345 | name string 346 | response *alice.Response 347 | args args 348 | want *alice.Response 349 | }{ 350 | { 351 | name: "", 352 | response: getResp(0), 353 | args: args{"e03f8d5b-35ef-4d57-9450-b721ca17a6c3", "sound.opus"}, 354 | want: test0, 355 | }, { 356 | name: "", 357 | response: getResp(0), 358 | args: args{"e03f8d5b-35ef-4d57-9450-b721ca17a6c3", "sound"}, 359 | want: test0, 360 | }, 361 | } 362 | for _, tt := range tests { 363 | t.Run(tt.name, func(t *testing.T) { 364 | resp := tt.response 365 | assert.Equal(t, tt.want, resp.CustomSound(tt.args.skill, tt.args.sound)) 366 | }) 367 | } 368 | } 369 | 370 | func TestResponse_ResetTTS(t *testing.T) { 371 | test0 := getResp(0) 372 | test0.Response.TTS = "ттс" 373 | 374 | tests := []struct { 375 | name string 376 | response *alice.Response 377 | want *alice.Response 378 | }{ 379 | { 380 | name: "", 381 | response: test0, 382 | want: getResp(0), 383 | }, 384 | } 385 | for _, tt := range tests { 386 | t.Run(tt.name, func(t *testing.T) { 387 | resp := tt.response 388 | assert.Equal(t, tt.want, resp.ResetTTS()) 389 | }) 390 | } 391 | } 392 | 393 | func TestNewButton(t *testing.T) { 394 | test0 := alice.Button{ 395 | Title: "кнопка", 396 | URL: "https://google.com", 397 | Hide: false, 398 | Payload: nil, 399 | } 400 | 401 | test1 := alice.Button{ 402 | Title: "кнопка2", 403 | URL: "https://google.com", 404 | Hide: true, 405 | Payload: `{"msg":"ok"}`, 406 | } 407 | 408 | type args struct { 409 | title string 410 | url string 411 | hide bool 412 | payload []interface{} 413 | } 414 | tests := []struct { 415 | name string 416 | args args 417 | want alice.Button 418 | }{ 419 | { 420 | name: "", 421 | args: args{ 422 | title: "кнопка", 423 | url: "https://google.com", 424 | hide: false, 425 | payload: nil, 426 | }, 427 | want: test0, 428 | }, { 429 | name: "", 430 | args: args{ 431 | title: "кнопка2", 432 | url: "https://google.com", 433 | hide: true, 434 | payload: []interface{}{`{"msg":"ok"}`}, 435 | }, 436 | want: test1, 437 | }, { 438 | name: "", 439 | args: args{ 440 | title: "кнопка2", 441 | url: "https://google.com", 442 | hide: true, 443 | payload: []interface{}{`{"msg":"ok"}`, `{"msg":"nope"}`}, 444 | }, 445 | want: test1, 446 | }, 447 | } 448 | for _, tt := range tests { 449 | t.Run(tt.name, func(t *testing.T) { 450 | assert.Equal(t, tt.want, alice.NewButton(tt.args.title, tt.args.url, tt.args.hide, tt.args.payload...)) 451 | }) 452 | } 453 | } 454 | 455 | func TestResponse_Button(t *testing.T) { 456 | test0 := getResp(0) 457 | test0.Response.Text = "кнопка" 458 | test0.Response.EndSession = false 459 | 460 | test1 := getResp(0) 461 | test1.Response.Text = "кнопка2" 462 | 463 | type args struct { 464 | title string 465 | url string 466 | hide bool 467 | payload []interface{} 468 | } 469 | tests := []struct { 470 | name string 471 | response *alice.Response 472 | args args 473 | want *alice.Response 474 | }{ 475 | { 476 | name: "", 477 | response: test0, 478 | args: args{ 479 | title: "кнопка", 480 | url: "https://google.com", 481 | hide: false, 482 | payload: nil, 483 | }, 484 | want: getResp(1), 485 | }, 486 | { 487 | name: "", 488 | response: test1, 489 | args: args{ 490 | title: "кнопка2", 491 | url: "", 492 | hide: true, 493 | payload: []interface{}{"msg", "nope"}, 494 | }, 495 | want: getResp(2), 496 | }, 497 | } 498 | for _, tt := range tests { 499 | t.Run(tt.name, func(t *testing.T) { 500 | resp := tt.response 501 | assert.Equal(t, tt.want, resp.Button(tt.args.title, tt.args.url, tt.args.hide, tt.args.payload...)) 502 | }) 503 | } 504 | } 505 | 506 | func TestResponse_Buttons(t *testing.T) { 507 | b0 := alice.Button{ 508 | Title: "кнопка", 509 | URL: "https://google.com", 510 | Hide: false, 511 | Payload: nil, 512 | } 513 | b1 := alice.Button{ 514 | Title: "кнопка2", 515 | Hide: true, 516 | Payload: nil, 517 | } 518 | b2 := alice.Button{ 519 | Title: "кнопка2", 520 | Hide: true, 521 | Payload: nil, 522 | } 523 | test0 := getResp(0) 524 | test0.Response.Buttons = []alice.Button{b0, b1, b2} 525 | 526 | b3, b4 := b0, b1 527 | b3.Payload = "msg" 528 | b4.Payload = "ok" 529 | test1 := getResp(0) 530 | test1.Response.Buttons = []alice.Button{b3, b4} 531 | 532 | type args struct { 533 | buttons []alice.Button 534 | } 535 | tests := []struct { 536 | name string 537 | response *alice.Response 538 | args args 539 | want *alice.Response 540 | }{ 541 | { 542 | name: "", 543 | response: getResp(0), 544 | args: args{ 545 | buttons: []alice.Button{b0, b1, b2}, 546 | }, 547 | want: test0, 548 | }, { 549 | name: "", 550 | response: getResp(0), 551 | args: args{ 552 | buttons: []alice.Button{b3, b4}, 553 | }, 554 | want: test1, 555 | }, 556 | } 557 | for _, tt := range tests { 558 | t.Run(tt.name, func(t *testing.T) { 559 | resp := tt.response 560 | assert.Equal(t, tt.want, resp.Buttons(tt.args.buttons...)) 561 | }) 562 | } 563 | } 564 | 565 | func TestResponse_EndSession(t *testing.T) { 566 | test0 := getResp(0) 567 | test0.Response.EndSession = true 568 | 569 | tests := []struct { 570 | name string 571 | response *alice.Response 572 | want *alice.Response 573 | }{ 574 | { 575 | name: "", 576 | response: getResp(0), 577 | want: test0, 578 | }, 579 | } 580 | for _, tt := range tests { 581 | t.Run(tt.name, func(t *testing.T) { 582 | resp := tt.response 583 | assert.Equal(t, tt.want, resp.EndSession()) 584 | }) 585 | } 586 | } 587 | 588 | func TestNewPhrase(t *testing.T) { 589 | type args struct { 590 | text string 591 | tts string 592 | } 593 | tests := []struct { 594 | name string 595 | args args 596 | want alice.Phrase 597 | }{ 598 | { 599 | name: "", 600 | args: args{ 601 | text: "текст", 602 | tts: "ттс", 603 | }, 604 | want: alice.Phrase{ 605 | Text: "текст", 606 | TTS: "ттс", 607 | }, 608 | }, 609 | { 610 | name: "", 611 | args: args{ 612 | text: "", 613 | tts: "", 614 | }, 615 | want: alice.Phrase{}, 616 | }, 617 | } 618 | for _, tt := range tests { 619 | t.Run(tt.name, func(t *testing.T) { 620 | got := alice.NewPhrase(tt.args.text, tt.args.tts) 621 | assert.Equal(t, tt.want, got) 622 | }) 623 | } 624 | } 625 | 626 | func TestResponse_Phrase(t *testing.T) { 627 | test0 := getResp(0) 628 | test0.Response.Text = "тест" 629 | test0.Response.TTS = "ттс" 630 | 631 | test1 := getResp(0) 632 | test1.Response.Text = "" 633 | test1.Response.TTS = "" 634 | 635 | tests := []struct { 636 | name string 637 | response *alice.Response 638 | args alice.Phrase 639 | want *alice.Response 640 | }{ 641 | { 642 | name: "", 643 | response: getResp(0), 644 | args: alice.Phrase{ 645 | Text: "тест", 646 | TTS: "ттс", 647 | }, 648 | want: test0, 649 | }, { 650 | name: "", 651 | response: getResp(0), 652 | args: alice.Phrase{}, 653 | want: test1, 654 | }, 655 | } 656 | for _, tt := range tests { 657 | t.Run(tt.name, func(t *testing.T) { 658 | resp := tt.response 659 | assert.Equal(t, tt.want, resp.Phrase(tt.args)) 660 | }) 661 | } 662 | } 663 | 664 | func TestResponse_RandomPhrase(t *testing.T) { 665 | test0 := getResp(0) 666 | test0.Response.Text = "тест" 667 | test0.Response.TTS = "ттс" 668 | 669 | test1 := getResp(0) 670 | test1.Response.Text = "" 671 | test1.Response.TTS = "" 672 | 673 | tests := []struct { 674 | name string 675 | response *alice.Response 676 | args []alice.Phrase 677 | want *alice.Response 678 | }{ 679 | { 680 | name: "", 681 | response: getResp(0), 682 | args: []alice.Phrase{ 683 | { 684 | Text: "тест", 685 | TTS: "ттс", 686 | }, 687 | }, 688 | want: test0, 689 | }, { 690 | name: "", 691 | response: getResp(0), 692 | args: []alice.Phrase{ 693 | {}, 694 | }, 695 | want: test1, 696 | }, 697 | } 698 | for _, tt := range tests { 699 | t.Run(tt.name, func(t *testing.T) { 700 | resp := tt.response 701 | assert.Equal(t, tt.want, resp.RandomPhrase(tt.args...)) 702 | }) 703 | } 704 | } 705 | 706 | func TestResponse_AddSessionState(t *testing.T) { 707 | wantResp := getResp(0) 708 | state := make(map[string]interface{}) 709 | state["one"] = struct{}{} 710 | state["two"] = "twotwo" 711 | wantResp.SessionState = state 712 | 713 | resp := getResp(0) 714 | err := resp.AddSessionState("one", struct{}{}) 715 | assert.NoError(t, err) 716 | err = resp.AddSessionState("two", "twotwo") 717 | if assert.NoError(t, err) { 718 | assert.Equal(t, wantResp, resp) 719 | } 720 | } 721 | 722 | func getResp(n int) *alice.Response { 723 | source := []string{`{"response":{"text":"","end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}`, 724 | 725 | `{"response":{"text":"кнопка","buttons":[{"title":"кнопка","url":"https://google.com"}],"end_session":false},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}`, 726 | 727 | `{"response":{"text":"кнопка2","buttons":[{"title":"кнопка2","hide":true,"payload":"msg"}],"end_session":true},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}`, 728 | 729 | `{"response":{"text":"тест","buttons":[{"title":"кнопка","url":"https://google.com"},{"title":"кнопка2","hide":true},{"title":"кнопка3","hide":true}],"end_session":false},"session":{"message_id":1,"session_id":"","user_id":""},"version":"1.0"}`, 730 | 731 | `{"response":{"text":"тест","buttons":[{"title":"кнопка","url":"https://google.com","payload":"msg"},{"title":"кнопка2","payload":"ok","hide":true}],"end_session":false},"session":{"message_id":0,"session_id":"","user_id":""},"version":"1.0"}`} 732 | 733 | var resp = new(alice.Response) 734 | json.Unmarshal([]byte(source[n]), resp) 735 | return resp 736 | } 737 | -------------------------------------------------------------------------------- /sounds/sounds.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | const ( 4 | // ThingsChainsaw Бензопила. 5 | ThingsChainsaw = "alice-sounds-things-chainsaw-1" 6 | // ThingsExplosion Взрыв. 7 | ThingsExplosion = "alice-sounds-things-explosion-1" 8 | // ThingsWater1 Вода #1 (льется). 9 | ThingsWater1 = "alice-sounds-things-water-1" 10 | // ThingsWater2 Вода #2 (бурлит). 11 | ThingsWater2 = "alice-sounds-things-water-2" 12 | // ThingsWater3 Вода (наливается в стакан). 13 | ThingsWater3 = "alice-sounds-things-water-3" 14 | // ThingsSwitch1 Выключатель #1. 15 | ThingsSwitch1 = "alice-sounds-things-switch-1" 16 | // ThingsSwitch2 Выключатель #2. 17 | ThingsSwitch2 = "alice-sounds-things-switch-2" 18 | // ThingsGun1 Выстрел (дробовик). 19 | ThingsGun1 = "alice-sounds-things-gun-1" 20 | // TransportShipHorn1 Гудок корабля #1. 21 | TransportShipHorn1 = "alice-sounds-transport-ship-horn-1" 22 | // TransportShipHorn2 Гудок корабля #2. 23 | TransportShipHorn2 = "alice-sounds-transport-ship-horn-2" 24 | // ThingsDoor1 Дверь #1. 25 | ThingsDoor1 = "alice-sounds-things-door-1" 26 | // ThingsDoor2 Дверь #2. 27 | ThingsDoor2 = "alice-sounds-things-door-2" 28 | // ThingsGlass1 Стекло (разбивается). 29 | ThingsGlass1 = "alice-sounds-things-glass-1" 30 | // ThingsGlass2 Звон бокалов. 31 | ThingsGlass2 = "alice-sounds-things-glass-2" 32 | // ThingsBell1 Колокол #1. 33 | ThingsBell1 = "alice-sounds-things-bell-1" 34 | // ThingsBell2 Колокол #2. 35 | ThingsBell2 = "alice-sounds-things-bell-2" 36 | // ThingsCar1 Машина (заводится). 37 | ThingsCar1 = "alice-sounds-things-car-1" 38 | // ThingsCar2 Машина (не заводится). 39 | ThingsCar2 = "alice-sounds-things-car-2" 40 | // ThingsSword1 Меч (парирование). 41 | ThingsSword1 = "alice-sounds-things-sword-1" 42 | // ThingsSword2 Меч (выходит из ножен). 43 | ThingsSword2 = "alice-sounds-things-sword-2" 44 | // ThingsSword3 Меч (поединок). 45 | ThingsSword3 = "alice-sounds-things-sword-3" 46 | // ThingsSiren1 Сирена #1. 47 | ThingsSiren1 = "alice-sounds-things-siren-1" 48 | // ThingsSiren2 Сирена #2. 49 | ThingsSiren2 = "alice-sounds-things-siren-2" 50 | // ThingsOldPhone1 Старый телефон #1. 51 | ThingsOldPhone1 = "alice-sounds-things-old-phone-1" 52 | // ThingsOldPhone2 Старый телефон #2. 53 | ThingsOldPhone2 = "alice-sounds-things-old-phone-2" 54 | // ThingsConstruction1 Строительство (пила и молоток). 55 | ThingsConstruction1 = "alice-sounds-things-construction-1" 56 | // ThingsConstruction2 Строительство (отбойный молоток). 57 | ThingsConstruction2 = "alice-sounds-things-construction-2" 58 | // ThingsPhone1 Телефон #1 (звонок). 59 | ThingsPhone1 = "alice-sounds-things-phone-1" 60 | // ThingsPhone2 Телефон #2 (звонок). 61 | ThingsPhone2 = "alice-sounds-things-phone-2" 62 | // ThingsPhone3 Телефон #3 (набор номера). 63 | ThingsPhone3 = "alice-sounds-things-phone-3" 64 | // ThingsPhone4 Телефон #4 (гудок). 65 | ThingsPhone4 = "alice-sounds-things-phone-4" 66 | // ThingsPhone5 Телефон #5 (гудок). 67 | ThingsPhone5 = "alice-sounds-things-phone-5" 68 | // ThingsToilet1 Унитаз. 69 | ThingsToilet1 = "alice-sounds-things-toilet-1" 70 | // ThingsCuckooClock1 Часы с кукушкой #1. 71 | ThingsCuckooClock1 = "alice-sounds-things-cuckoo-clock-1" 72 | // ThingsCuckooClock2 Часы с кукушкой #2. 73 | ThingsCuckooClock2 = "alice-sounds-things-cuckoo-clock-2" 74 | 75 | // AnimalsWolf1 Волк. 76 | AnimalsWolf1 = "alice-sounds-animals-wolf-1" 77 | // AnimalsCrow1 Ворона #1. 78 | AnimalsCrow1 = "alice-sounds-animals-crow-1" 79 | // AnimalsCrow2 Ворона #2. 80 | AnimalsCrow2 = "alice-sounds-animals-crow-2" 81 | // AnimalsCow1 Корова #1. 82 | AnimalsCow1 = "alice-sounds-animals-cow-1" 83 | // AnimalsCow2 Корова #2. 84 | AnimalsCow2 = "alice-sounds-animals-cow-2" 85 | // AnimalsCow3 Корова #3. 86 | AnimalsCow3 = "alice-sounds-animals-cow-3" 87 | // AnimalsCat1 Кошка #1 (мяуканье). 88 | AnimalsCat1 = "alice-sounds-animals-cat-1" 89 | // AnimalsCat2 Кошка #2 (мяуканье). 90 | AnimalsCat2 = "alice-sounds-animals-cat-2" 91 | // AnimalsCat3 Кошка #3 (мяуканье). 92 | AnimalsCat3 = "alice-sounds-animals-cat-3" 93 | // AnimalsCat4 Кошка #4 (мурчание). 94 | AnimalsCat4 = "alice-sounds-animals-cat-4" 95 | // AnimalsCat5 Кошка #5 (шипение). 96 | AnimalsCat5 = "alice-sounds-animals-cat-5" 97 | // AnimalsCuckoo1 Кукушка. 98 | AnimalsCuckoo1 = "alice-sounds-animals-cuckoo-1" 99 | // AnimalsChicken1 Курица. 100 | AnimalsChicken1 = "alice-sounds-animals-chicken-1" 101 | // AnimalsLion1 Лев #1. 102 | AnimalsLion1 = "alice-sounds-animals-lion-1" 103 | // AnimalsLion2 Лев #2. 104 | AnimalsLion2 = "alice-sounds-animals-lion-2" 105 | // AnimalsHorse1 Лошадь #1 (ржание). 106 | AnimalsHorse1 = "alice-sounds-animals-horse-1" 107 | // AnimalsHorse2 Лошадь #2 (фырканье). 108 | AnimalsHorse2 = "alice-sounds-animals-horse-2" 109 | // AnimalsHorseGalloping1 Лошадь #3 (галоп). 110 | AnimalsHorseGalloping1 = "alice-sounds-animals-horse-galloping-1" 111 | // AnimalsHorseWalking1 Лошадь #4 (шаг). 112 | AnimalsHorseWalking1 = "alice-sounds-animals-horse-walking-1" 113 | // AnimalsFrog1 Лягушка. 114 | AnimalsFrog1 = "alice-sounds-animals-frog-1" 115 | // AnimalsSeagull1 Морской котик. 116 | AnimalsSeagull1 = "alice-sounds-animals-seagull-1" 117 | // AnimalsMonkey1 Обезьяна. 118 | AnimalsMonkey1 = "alice-sounds-animals-monkey-1" 119 | // AnimalsSheep1 Овца #1. 120 | AnimalsSheep1 = "alice-sounds-animals-sheep-1" 121 | // AnimalsSheep2 Овца #2 (несколько). 122 | AnimalsSheep2 = "alice-sounds-animals-sheep-2" 123 | // AnimalsRooster1 Петух. 124 | AnimalsRooster1 = "alice-sounds-animals-rooster-1" 125 | // AnimalsElephant1 Слон #1. 126 | AnimalsElephant1 = "alice-sounds-animals-elephant-1" 127 | // AnimalsElephant2 Слон #2. 128 | AnimalsElephant2 = "alice-sounds-animals-elephant-2" 129 | // AnimalsDog1 Собака #1 (лай). 130 | AnimalsDog1 = "alice-sounds-animals-dog-1" 131 | // AnimalsDog2 Собака #2 (рык). 132 | AnimalsDog2 = "alice-sounds-animals-dog-2" 133 | // AnimalsDog3 Собака #3 (скуление). 134 | AnimalsDog3 = "alice-sounds-animals-dog-3" 135 | // AnimalsDog4 Собака #4 (лай). 136 | AnimalsDog4 = "alice-sounds-animals-dog-4" 137 | // AnimalsDog5 Собака #5 (лай). 138 | AnimalsDog5 = "alice-sounds-animals-dog-5" 139 | // AnimalsOwl1 Сова #1. 140 | AnimalsOwl1 = "alice-sounds-animals-owl-1" 141 | // AnimalsOwl2 Сова #2. 142 | AnimalsOwl2 = "alice-sounds-animals-owl-2" 143 | 144 | // GameBoot1 Загрузка (8 бит). 145 | GameBoot1 = "alice-sounds-game-boot-1" 146 | // Game8BitCoin1 Монета (8 бит) #1. 147 | Game8BitCoin1 = "alice-sounds-game-8-bit-coin-1" 148 | // Game8BitCoin2 Монета (8 бит) #2. 149 | Game8BitCoin2 = "alice-sounds-game-8-bit-coin-2" 150 | // GameLoss1 Неудача #1. 151 | GameLoss1 = "alice-sounds-game-loss-1" 152 | // GameLoss2 Неудача #2. 153 | GameLoss2 = "alice-sounds-game-loss-2" 154 | // GameLoss3 Неудача #3. 155 | GameLoss3 = "alice-sounds-game-loss-3" 156 | // GamePing1 Оповещение (8 бит). 157 | GamePing1 = "alice-sounds-game-ping-1" 158 | // GameWin1 Победные фанфары #1. 159 | GameWin1 = "alice-sounds-game-win-1" 160 | // GameWin2 Победные фанфары #2. 161 | GameWin2 = "alice-sounds-game-win-2" 162 | // GameWin3 Победные фанфары #3. 163 | GameWin3 = "alice-sounds-game-win-3" 164 | // Game8BitFlyby1 Полет (8 бит). 165 | Game8BitFlyby1 = "alice-sounds-game-8-bit-flyby-1" 166 | // Game8BitMachineGun1 Пулемет (8 бит). 167 | Game8BitMachineGun1 = "alice-sounds-game-8-bit-machine-gun-1" 168 | // Game8BitPhone1 Телефон (8 бит). 169 | Game8BitPhone1 = "alice-sounds-game-8-bit-phone-1" 170 | // GamePowerup1 Усиление (8 бит) #1. 171 | GamePowerup1 = "alice-sounds-game-powerup-1" 172 | // GamePowerup2 Усиление (8 бит) #2. 173 | GamePowerup2 = "alice-sounds-game-powerup-2" 174 | 175 | // HumanCheer1 Аплодисменты. 176 | HumanCheer1 = "alice-sounds-human-cheer-1" 177 | // HumanCheer2 Болельщики. 178 | HumanCheer2 = "alice-sounds-human-cheer-2" 179 | // HumanKids1 Дети. 180 | HumanKids1 = "alice-sounds-human-kids-1" 181 | // HumanWalkingDead1 Зомби #1 (рык). 182 | HumanWalkingDead1 = "alice-sounds-human-walking-dead-1" 183 | // HumanWalkingDead2 Зомби #2 (стон). 184 | HumanWalkingDead2 = "alice-sounds-human-walking-dead-2" 185 | // HumanWalkingDead3 Зомби #3 (рык с криком). 186 | HumanWalkingDead3 = "alice-sounds-human-walking-dead-3" 187 | // HumanCough1 Кашель #1. 188 | HumanCough1 = "alice-sounds-human-cough-1" 189 | // HumanCough2 Кашель #2. 190 | HumanCough2 = "alice-sounds-human-cough-2" 191 | // HumanLaugh1 Смех #1. 192 | HumanLaugh1 = "alice-sounds-human-laugh-1" 193 | // HumanLaugh2 Смех #2. 194 | HumanLaugh2 = "alice-sounds-human-laugh-2" 195 | // HumanLaugh3 Смех #3 (злодейский). 196 | HumanLaugh3 = "alice-sounds-human-laugh-3" 197 | // HumanLaugh4 Смех #4 (злодейский). 198 | HumanLaugh4 = "alice-sounds-human-laugh-4" 199 | // HumanLaugh5 Смех #5 (детский). 200 | HumanLaugh5 = "alice-sounds-human-laugh-5" 201 | // HumanCrowd1 Толпа #1 (разговоры). 202 | HumanCrowd1 = "alice-sounds-human-crowd-1" 203 | // HumanCrowd2 Толпа #2 (удивление). 204 | HumanCrowd2 = "alice-sounds-human-crowd-2" 205 | // HumanCrowd3 Толпа #3 (аплодисменты). 206 | HumanCrowd3 = "alice-sounds-human-crowd-3" 207 | // HumanCrowd4 Толпа #4 (бурные аплодисменты). 208 | HumanCrowd4 = "alice-sounds-human-crowd-4" 209 | // HumanCrowd5 Толпа #5 (болельщики). 210 | HumanCrowd5 = "alice-sounds-human-crowd-5" 211 | // HumanCrowd6 Толпа #6 (одобрительные крики). 212 | HumanCrowd6 = "alice-sounds-human-crowd-6" 213 | // HumanCrowd7 Толпа #7 (недовольство). 214 | HumanCrowd7 = "alice-sounds-human-crowd-7" 215 | // HumanSneeze1 Чихание #1. 216 | HumanSneeze1 = "alice-sounds-human-sneeze-1" 217 | // HumanSneeze2 Чихание #2. 218 | HumanSneeze2 = "alice-sounds-human-sneeze-2" 219 | // HumanWalkingRoom1 Шаги в комнате. 220 | HumanWalkingRoom1 = "alice-sounds-human-walking-room-1" 221 | // HumanWalkingSnow1 Шаги на снегу. 222 | HumanWalkingSnow1 = "alice-sounds-human-walking-snow-1" 223 | // HumanWalkingLeaves1 Шаги по листьям. 224 | HumanWalkingLeaves1 = "alice-sounds-human-walking-leaves-1" 225 | 226 | // Harp1 Арфа. 227 | Harp1 = "alice-music-harp-1" 228 | // Drums1 Барабанный проигрыш #1. 229 | Drums1 = "alice-music-drums-1" 230 | // Drums2 Барабанный проигрыш #2. 231 | Drums2 = "alice-music-drums-2" 232 | // Drums3 Барабанный проигрыш #3. 233 | Drums3 = "alice-music-drums-3" 234 | // DrumLoop1 Бит #1 (быстро). 235 | DrumLoop1 = "alice-music-drum-loop-1" 236 | // DrumLoop2 Бит #2 (медленно). 237 | DrumLoop2 = "alice-music-drum-loop-2" 238 | // Tambourine80bpm1 Бубен #1 (80 ударов в минуту). 239 | Tambourine80bpm1 = "alice-music-tambourine-80bpm-1" 240 | // Tambourine100bpm1 Бубен #2 (100 ударов в минуту). 241 | Tambourine100bpm1 = "alice-music-tambourine-100bpm-1" 242 | // Tambourine120bpm1 Бубен #3 (120 ударов в минуту). 243 | Tambourine120bpm1 = "alice-music-tambourine-120bpm-1" 244 | // Bagpipes1 Волынка #1. 245 | Bagpipes1 = "alice-music-bagpipes-1" 246 | // Bagpipes2 Волынка #2. 247 | Bagpipes2 = "alice-music-bagpipes-2" 248 | // GuitarC1 Гитара, аккорд C. 249 | GuitarC1 = "alice-music-guitar-c-1" 250 | // GuitarE1 Гитара, аккорд E. 251 | GuitarE1 = "alice-music-guitar-e-1" 252 | // GuitarG1 Гитара, аккорд G. 253 | GuitarG1 = "alice-music-guitar-g-1" 254 | // GuitarA1 Гитара, аккорд А. 255 | GuitarA1 = "alice-music-guitar-a-1" 256 | // Gong1 Гонг #1. 257 | Gong1 = "alice-music-gong-1" 258 | // Gong2 Гонг #2. 259 | Gong2 = "alice-music-gong-2" 260 | // ViolinC1 Скрипка (до). 261 | ViolinC1 = "alice-music-violin-c-1" 262 | // ViolinC2 Скрипка (до верхнее). 263 | ViolinC2 = "alice-music-violin-c-2" 264 | // ViolinA1 Скрипка (ля). 265 | ViolinA1 = "alice-music-violin-a-1" 266 | // ViolinE1 Скрипка (ми). 267 | ViolinE1 = "alice-music-violin-e-1" 268 | // ViolinD1 Скрипка (ре). 269 | ViolinD1 = "alice-music-violin-d-1" 270 | // ViolinB1 Скрипка (си). 271 | ViolinB1 = "alice-music-violin-b-1" 272 | // ViolinG1 Скрипка (соль). 273 | ViolinG1 = "alice-music-violin-g-1" 274 | // ViolinF1 Скрипка (фа). 275 | ViolinF1 = "alice-music-violin-f-1" 276 | // Horn1 Труба болельщика. 277 | Horn1 = "alice-music-horn-1" 278 | // Horn2 Горн. 279 | Horn2 = "alice-music-horn-2" 280 | // PianoC1 Фортепиано (до). 281 | PianoC1 = "alice-music-piano-c-1" 282 | // PianoC2 Фортепиано (до верхнее). 283 | PianoC2 = "alice-music-piano-c-2" 284 | // PianoA1 Фортепиано (ля). 285 | PianoA1 = "alice-music-piano-a-1" 286 | // PianoE1 Фортепиано (ми). 287 | PianoE1 = "alice-music-piano-e-1" 288 | // PianoD1 Фортепиано (ре). 289 | PianoD1 = "alice-music-piano-d-1" 290 | // PianoB1 Фортепиано (си). 291 | PianoB1 = "alice-music-piano-b-1" 292 | // PianoG1 Фортепиано (соль). 293 | PianoG1 = "alice-music-piano-g-1" 294 | // PianoF1 Фортепиано (фа). 295 | PianoF1 = "alice-music-piano-f-1" 296 | 297 | // NatureWind1 Ветер #1. 298 | NatureWind1 = "alice-sounds-nature-wind-1" 299 | // NatureWind2 Ветер #2. 300 | NatureWind2 = "alice-sounds-nature-wind-2" 301 | // NatureThunder1 Гром #1. 302 | NatureThunder1 = "alice-sounds-nature-thunder-1" 303 | // NatureThunder2 Гром #2. 304 | NatureThunder2 = "alice-sounds-nature-thunder-2" 305 | // NatureJungle1 Джунгли #1. 306 | NatureJungle1 = "alice-sounds-nature-jungle-1" 307 | // NatureJungle2 Джунгли #2. 308 | NatureJungle2 = "alice-sounds-nature-jungle-2" 309 | // NatureRain1 Дождь #1. 310 | NatureRain1 = "alice-sounds-nature-rain-1" 311 | // NatureRain2 Дождь #2. 312 | NatureRain2 = "alice-sounds-nature-rain-2" 313 | // NatureForest1 Лес #1. 314 | NatureForest1 = "alice-sounds-nature-forest-1" 315 | // NatureForest2 Лес #2. 316 | NatureForest2 = "alice-sounds-nature-forest-2" 317 | // NatureSea1 Море #1. 318 | NatureSea1 = "alice-sounds-nature-sea-1" 319 | // NatureSea2 Море #2. 320 | NatureSea2 = "alice-sounds-nature-sea-2" 321 | // NatureFire1 Огонь #1. 322 | NatureFire1 = "alice-sounds-nature-fire-1" 323 | // NatureFire2 Огонь #2. 324 | NatureFire2 = "alice-sounds-nature-fire-2" 325 | // NatureStream1 Ручей #1. 326 | NatureStream1 = "alice-sounds-nature-stream-1" 327 | // NatureStream2 Ручей #2. 328 | NatureStream2 = "alice-sounds-nature-stream-2" 329 | ) 330 | --------------------------------------------------------------------------------