github.com/actions-on-google/gactions@v3.2.0+incompatible/api/request.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package request represents an SDK request. 16 package request 17 18 import ( 19 "encoding/base64" 20 "fmt" 21 "mime" 22 "path" 23 "path/filepath" 24 "sort" 25 26 "github.com/actions-on-google/gactions/api/yamlutils" 27 "github.com/actions-on-google/gactions/log" 28 "github.com/actions-on-google/gactions/project/studio" 29 ) 30 31 const ( 32 // MaxChunkSizeBytes specifies the max size limit on JSON payload for a single request/response in the stream. 33 // It's enforced by server. 34 MaxChunkSizeBytes = 10 * 1024 * 1024 35 // Padding accounts for bytes from surrounding JSON fields coming from the request schema; 36 // 512 Kb buffer is a very generous upper-bound and I don't think we'll hit it in practice. 37 Padding = 512 * 1024 38 ) 39 40 // EncryptSecret returns a map representing a EncryptSecret request populated with clientSecret field. 41 func EncryptSecret(secret string) map[string]interface{} { 42 return map[string]interface{}{ 43 "clientSecret": secret, 44 } 45 } 46 47 // DecryptSecret returns a map representing a DecryptSecret request populated with encryptedClientSecret field. 48 func DecryptSecret(secret string) map[string]interface{} { 49 return map[string]interface{}{ 50 "encryptedClientSecret": secret, 51 } 52 } 53 54 // ReadDraft returns a map representing a ReadDraft request populated with name field. 55 func ReadDraft(name, keyVersion string) map[string]interface{} { 56 req := map[string]interface{}{ 57 "name": fmt.Sprintf("projects/%v/draft", name), 58 } 59 if keyVersion != "" { 60 req["clientSecretEncryptionKeyVersion"] = keyVersion 61 } 62 return req 63 } 64 65 // WriteDraft returns a map representing a WriteDraft request populated with name field. 66 func WriteDraft(name string) map[string]interface{} { 67 return map[string]interface{}{ 68 "parent": fmt.Sprintf("projects/%v", name), 69 } 70 } 71 72 // WritePreview returns a map representing a WriteDraft request populated with name and sandbox fields. 73 func WritePreview(name string, sandbox bool) map[string]interface{} { 74 v := map[string]interface{}{} 75 v["parent"] = fmt.Sprintf("projects/%v", name) 76 v["previewSettings"] = map[string]interface{}{ 77 "sandbox": sandbox, 78 } 79 return v 80 } 81 82 // CreateVersion returns a map representing a WriteVersion request populated with name and sandbox fields. 83 func CreateVersion(name string, channel string) map[string]interface{} { 84 return map[string]interface{}{ 85 "parent": fmt.Sprintf("projects/%v", name), 86 "release_channel": channel, 87 } 88 } 89 90 // ReadVersion returns a map representing a ReadVersion request populated with name and versionId fields. 91 func ReadVersion(name string, versionID string) map[string]interface{} { 92 return map[string]interface{}{ 93 "name": fmt.Sprintf("projects/%v/versions/%v", name, versionID), 94 } 95 } 96 97 // ListReleaseChannels returns a list of release channels with current and pending versions on each channel. 98 func ListReleaseChannels(name string) map[string]interface{} { 99 return map[string]interface{}{ 100 "parent": fmt.Sprintf("projects/%v", name), 101 } 102 } 103 104 // ListVersions returns a map of versions and their metadata for the project. 105 func ListVersions(name string) map[string]interface{} { 106 return map[string]interface{}{ 107 "parent": fmt.Sprintf("projects/%v", name), 108 } 109 } 110 111 // addConfigFiles adds configFiles w/o a resource bundle to a request. 112 func addConfigFiles(req map[string]interface{}, configFiles map[string][]byte, root string) error { 113 cfgs := make(map[string][]interface{}) 114 var keys []string 115 for k := range configFiles { 116 keys = append(keys, k) 117 } 118 sort.Strings(keys) 119 for _, filename := range keys { 120 content := configFiles[filename] 121 log.Infof("Adding %v to configFiles request\n", filepath.Join(root, filename)) 122 mp, err := yamlutils.UnmarshalYAMLToMap(content) 123 if err != nil { 124 return fmt.Errorf("%v has incorrect syntax: %v", filepath.Join(root, filename), err) 125 } 126 m := make(map[string]interface{}) 127 m["filePath"] = filename 128 switch { 129 case studio.IsAccountLinkingSecret(filename): 130 m["accountLinkingSecret"] = mp 131 case studio.IsManifest(filename): 132 m["manifest"] = mp 133 case studio.IsSettings(filename): 134 m["settings"] = mp 135 case studio.IsActions(filename): 136 m["actions"] = mp 137 case studio.IsWebhookDefinition(filename): 138 m["webhook"] = mp 139 case studio.IsIntent(filename): 140 m["intent"] = mp 141 case studio.IsGlobal(filename): 142 m["globalIntentEvent"] = mp 143 case studio.IsType(filename): 144 m["type"] = mp 145 case studio.IsEntitySet(filename): 146 m["entitySet"] = mp 147 case studio.IsPrompt(filename): 148 m["staticPrompt"] = mp 149 case studio.IsScene(filename): 150 m["scene"] = mp 151 case studio.IsVertical(filename): 152 m["verticalSettings"] = mp 153 // Note: This value is not publicly available 154 case studio.IsDeviceFulfillment(filename): 155 m["deviceFulfillment"] = mp 156 case studio.IsResourceBundle(filename): 157 m["resourceBundle"] = mp 158 default: 159 return fmt.Errorf("failed to add %v to a request", filepath.Join(root, filename)) 160 } 161 cfgs["configFiles"] = append(cfgs["configFiles"], m) 162 } 163 req["files"] = map[string]interface{}{ 164 "configFiles": cfgs, 165 } 166 return nil 167 } 168 169 // addDataFiles adds a data files from the chunk to a request. 170 func addDataFiles(req map[string]interface{}, chunk map[string][]byte, root string) error { 171 dfs := map[string][]interface{}{} 172 for filename, content := range chunk { 173 log.Infof("Adding %v to dataFiles request\n", filepath.Join(root, filename)) 174 if path.Ext(filename) == ".zip" { 175 m := map[string]interface{}{ 176 "filePath": filename, 177 "contentType": "application/zip;zip_type=cloud_function", 178 "payload": content, 179 } 180 dfs["dataFiles"] = append(dfs["dataFiles"], m) 181 continue 182 } 183 if path.Ext(filename) == ".flr" { 184 m := map[string]interface{}{ 185 "filePath": filename, 186 "contentType": "x-world/x-vrml", 187 "payload": content, 188 } 189 dfs["dataFiles"] = append(dfs["dataFiles"], m) 190 continue 191 } 192 mime := mime.TypeByExtension(path.Ext(filename)) 193 switch mime { 194 case "audio/mpeg", "image/jpeg", "image/png", "audio/wav", "audio/x-wav": 195 { 196 m := map[string]interface{}{ 197 "filePath": filename, 198 "contentType": mime, 199 "payload": content, 200 } 201 dfs["dataFiles"] = append(dfs["dataFiles"], m) 202 } 203 default: 204 log.Warnf("Can't recognize an extension for %v. The supported extensions are audio/mpeg, image/jpeg, " + 205 "image/png, audio/wav, audio/x-wav found %v", filepath.Join(root, filename), mime) 206 } 207 } 208 if len(dfs) > 0 { 209 req["files"] = map[string]interface{}{ 210 "dataFiles": dfs, 211 } 212 } 213 return nil 214 } 215 216 // SDKStreamer provides an interface to obtain the next JSON request that needs to be sent to 217 // SDK server during HTTP stream. SDK, ESF and GFE each have their own requirements on the 218 // payload and this type implements them. 219 type SDKStreamer struct { 220 files map[string][]byte 221 sizes map[string]int // sizes contains a size that a file occupies in a JSON request 222 dataFilenames []string 223 configFilenames []string 224 makeRequest func() map[string]interface{} 225 root string 226 i int // index of current item in configFilesnames 227 j int // index of current item in dataFilenames 228 chunkSize int 229 } 230 231 // NewStreamer returns an instance of SDKStreamer, initialized with all of the variables 232 // from its arguments. Function expects configFiles to have at least base settings and manifest files. 233 func NewStreamer(configFiles map[string][]byte, dataFiles map[string][]byte, makeRequest func() map[string]interface{}, root string, chunkSize int) SDKStreamer { 234 files := map[string][]byte{} 235 sizes := map[string]int{} 236 var cfgnames, dfnames []string 237 238 for k, v := range configFiles { 239 files[k] = v 240 cfgnames = append(cfgnames, k) 241 sizes[k] = len(v) 242 } 243 for k, v := range dataFiles { 244 files[k] = v 245 dfnames = append(dfnames, k) 246 // Marshal function of JSON library (https://golang.org/pkg/encoding/json/#Marshal) encodes 247 // []byte as a base-64 encoded string. This adds an extra memory overhead when the map is 248 // converted to JSON. Each DataFile is []byte, so this is a good approximation. 249 sizes[k] = len(base64.StdEncoding.EncodeToString(v)) 250 } 251 // We need to sort config files and datafiles based on their size in bytes. 252 // However, settings and manifest files must be inside of the first request, 253 // so these two files take precedence. 254 sortConfigFiles(cfgnames, files, sizes) 255 sort.Slice(dfnames, func(i int, j int) bool { 256 return sizes[dfnames[i]] < sizes[dfnames[j]] 257 }) 258 259 return SDKStreamer{ 260 files: files, 261 dataFilenames: dfnames, 262 configFilenames: cfgnames, 263 makeRequest: makeRequest, 264 root: root, 265 chunkSize: chunkSize, 266 sizes: sizes, 267 } 268 } 269 270 func sortConfigFiles(cfgnames []string, files map[string][]byte, sizes map[string]int) { 271 var pos []int 272 for i, v := range cfgnames { 273 if studio.IsSettings(v) || studio.IsManifest(v) { 274 pos = append(pos, i) 275 } 276 } 277 moveToFront(cfgnames, pos) 278 needSort := cfgnames[len(pos):] 279 sort.Slice(needSort, func(i int, j int) bool { 280 return sizes[needSort[i]] < sizes[needSort[j]] 281 }) 282 for i, v := range needSort { 283 cfgnames[i+len(pos)] = v 284 } 285 } 286 287 func moveToFront(a []string, ps []int) { 288 for i := 0; i < len(ps); i++ { 289 a[i], a[ps[i]] = a[ps[i]], a[i] 290 } 291 } 292 293 // HasNext returns true if there is still another request in the stream. 294 func (s SDKStreamer) HasNext() bool { 295 return (s.i + s.j) < len(s.files) 296 } 297 298 // nextChunk returns the next "chunk" of config files such that 299 // the sum of the size of each individual config file in the chunk 300 // is less than s.chunkSize. 301 func (s *SDKStreamer) nextChunk(a []string, next int) map[string][]byte { 302 chunk := map[string][]byte{} 303 curSize := 0 304 i := 0 305 for curSize < s.chunkSize && i+next < len(a) { 306 name := a[next+i] 307 content := s.files[name] 308 curSize += s.sizes[name] 309 if curSize > s.chunkSize { 310 break 311 } 312 chunk[name] = content 313 i++ 314 } 315 return chunk 316 } 317 318 func (s *SDKStreamer) nextConfigFiles(req map[string]interface{}) error { 319 if s.i == 0 { 320 log.Outln("Sending configuration files...") 321 } 322 chunk := s.nextChunk(s.configFilenames, s.i) 323 if len(chunk) == 0 { 324 return fmt.Errorf("%v exceeds the limit of %v bytes", s.configFilenames[s.i], s.chunkSize) 325 } 326 if err := addConfigFiles(req, chunk, s.root); err != nil { 327 return err 328 } 329 s.i += len(chunk) 330 return nil 331 } 332 333 func (s *SDKStreamer) nextDataFiles(req map[string]interface{}) error { 334 if s.j == 0 { 335 log.Outln("Sending resources...") 336 } 337 chunk := s.nextChunk(s.dataFilenames, s.j) 338 if len(chunk) == 0 { 339 return fmt.Errorf("%v exceeds the limit of %v bytes", s.dataFilenames[s.j], s.chunkSize) 340 } 341 if err := addDataFiles(req, chunk, s.root); err != nil { 342 return err 343 } 344 s.j += len(chunk) 345 return nil 346 } 347 348 // Next returns the next request to be sent to SDK server. It implements following requirements: 349 // 1. Send all config files 350 // 1a. First request will have manifest and all of the settings files (i.e. localized and base) 351 // 1b. Each config file request is less than s.chunkSize. 352 // 2. Send all of data files in one or several requests. Each request will be less than s.chunkSize. 353 // It will return an error if the size of the payload is larger than s.chunkSize. 354 func (s *SDKStreamer) Next() (map[string]interface{}, error) { 355 req := s.makeRequest() 356 if s.i < len(s.configFilenames) { 357 if err := s.nextConfigFiles(req); err != nil { 358 return nil, err 359 } 360 } else if s.j < len(s.dataFilenames) { 361 if err := s.nextDataFiles(req); err != nil { 362 return nil, err 363 } 364 } 365 return req, nil 366 }