requests.go (284 lines of code) (raw):

// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ezcx import ( "bytes" "context" "io" "log" "net/http" cx "cloud.google.com/go/dialogflow/cx/apiv3/cxpb" "github.com/google/uuid" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) // 2022-11-28 type CxParameterType int const ( String CxParameterType = iota Integer Float ) type WebhookRequest struct { cx.WebhookRequest // 2022-10-08: Replaced context.Context with func () context.Context. ctx func() context.Context req *http.Request } func NewWebhookRequest() *WebhookRequest { return new(WebhookRequest) } // WebhookRequest Initializations // Initialize the PageInfo field func (req *WebhookRequest) initPageInfo() { if req.PageInfo == nil { req.PageInfo = new(cx.PageInfo) } if req.PageInfo.FormInfo == nil { req.PageInfo.FormInfo = new(cx.PageInfo_FormInfo) } if req.PageInfo.FormInfo.ParameterInfo == nil { req.PageInfo.FormInfo.ParameterInfo = make([]*cx.PageInfo_FormInfo_ParameterInfo, 0) } } // Initialize the SessionInfo field func (req *WebhookRequest) initSessionInfo() { if req.SessionInfo == nil { req.SessionInfo = new(cx.SessionInfo) } } // Initialize the Payload field func (req *WebhookRequest) initPayload() { if req.Payload == nil { req.Payload = new(structpb.Struct) } if req.Payload.Fields == nil { req.Payload.Fields = make(map[string]*structpb.Value) } } // Returns a pointer to the underlying http.Request. // Useful for providing extended logging. func (req *WebhookRequest) Request() *http.Request { return req.req } func (req *WebhookRequest) Context() context.Context { return req.ctx() } // . func (req *WebhookRequest) Logger() *log.Logger { ctx := req.Context() ctxLg := ctx.Value(Logger) if ctxLg == nil { // During testing, it's possible the user defined logger was not // flowed down. This is provided for convenience. return log.Default() } lg, ok := ctxLg.(*log.Logger) if !ok { return log.Default() } return lg } // Sets (overrides) the PageInfo.ParameterInfos to match the provided map m func (req *WebhookRequest) setPageFormParameters(m map[string]any) error { params := make([]*cx.PageInfo_FormInfo_ParameterInfo, 0) for k, v := range m { var formParameter cx.PageInfo_FormInfo_ParameterInfo pv, err := anyToProto(v) if err != nil { return err } formParameter.DisplayName = k formParameter.Value = pv formParameter.State = cx.PageInfo_FormInfo_ParameterInfo_FILLED params = append(params, &formParameter) } req.PageInfo.FormInfo.ParameterInfo = params return nil } // Sets (overrides) the SessionInfo.Parameters to match the provided map m func (req *WebhookRequest) setSessionParameters(m map[string]any) error { pm, err := anyToProtoMap(m) if err != nil { return err } req.SessionInfo.Parameters = pm return nil } // Sets (overrides the SessionInfo.Parameters) to match the provided map m func (req *WebhookRequest) setPayload(m map[string]any) error { pm, err := anyToProtoMap(m) if err != nil { return err } req.Payload.Fields = pm return nil } // yaquino@2022-10-11: Dialogflow CX API May include "extra" fields that may // throw errors and interface with protojson.Unmarshal. As per the documentation, // these fields may be ignored. Now also pointing at req.WebhookRequest for unmarshalling.. func WebhookRequestFromReader(rd io.Reader) (*WebhookRequest, error) { var req WebhookRequest b, err := io.ReadAll(rd) if err != nil { return nil, err } unmarshaler := &protojson.UnmarshalOptions{ AllowPartial: true, DiscardUnknown: true, } err = unmarshaler.Unmarshal(b, &req.WebhookRequest) if err != nil { return nil, ErrUnmarshalWrapper("WebhookRequestFromReader", err) } return &req, nil } // yaquino@2022-10-07: Refactored to flow http.Request's context to the // WebhookRequest instance. func WebhookRequestFromRequest(r *http.Request) (*WebhookRequest, error) { req, err := WebhookRequestFromReader(r.Body) if err != nil { return nil, err } req.req = r return req, nil } func (req *WebhookRequest) ReadReader(rd io.Reader) error { b, err := io.ReadAll(rd) if err != nil { return err } unmarshaler := &protojson.UnmarshalOptions{ AllowPartial: true, DiscardUnknown: true, } err = unmarshaler.Unmarshal(b, &req.WebhookRequest) if err != nil { return err } return nil } func (req *WebhookRequest) ReadRequest(r *http.Request) error { return req.ReadReader(r.Body) } // Is this the right format?? func (req *WebhookRequest) WriteRequest(w io.Writer) error { m := protojson.MarshalOptions{Indent: "\t"} b, err := m.Marshal(&req.WebhookRequest) if err != nil { return err } r := bytes.NewReader(b) _, err = io.Copy(w, r) if err != nil { return err } return nil } func (req *WebhookRequest) InitializeResponse() *WebhookResponse { return req.initializeResponse() } func (req *WebhookRequest) initializeResponse() *WebhookResponse { resp := NewWebhookResponse() return req.copySession(resp) } func (req *WebhookRequest) copySession(res *WebhookResponse) *WebhookResponse { if res.SessionInfo == nil { res.SessionInfo = new(cx.SessionInfo) } res.SessionInfo.Session = req.SessionInfo.Session return res } func (req *WebhookRequest) CopyPageInfo(res *WebhookResponse) { if req.PageInfo != nil { res.PageInfo = req.PageInfo } } func (req *WebhookRequest) CopySessionInfo(res *WebhookResponse) *WebhookResponse { if req.SessionInfo != nil { res.SessionInfo = req.SessionInfo } return res } func (req *WebhookRequest) CopyPayload(res *WebhookResponse) *WebhookResponse { if req.Payload != nil { res.Payload = req.Payload } return res } func (req *WebhookRequest) GetPageFormParameters() map[string]any { params := make(map[string]any) // Just in case - I don't think we can iterate over a nil map. if req.PageInfo == nil { return nil } if req.PageInfo.FormInfo == nil { return nil } if req.PageInfo.FormInfo.ParameterInfo == nil { return nil } for _, paramInfo := range req.PageInfo.FormInfo.ParameterInfo { params[paramInfo.DisplayName] = protoToAny(paramInfo.Value) } return params } func (req *WebhookRequest) GetSessionParameters() map[string]any { if req.SessionInfo == nil { return nil } if req.SessionInfo.Parameters == nil { return nil } return protoToAnyMap(req.SessionInfo.Parameters) } func (req *WebhookRequest) GetSessionParameter(key string) (any, bool) { // Check if SessionInfo Parameters is nil. if req.SessionInfo == nil { return nil, false } if req.SessionInfo.Parameters == nil { return nil, false } pv, ok := req.SessionInfo.Parameters[key] return protoToAny(pv), ok } func (req *WebhookRequest) GetPayload() map[string]any { if req.Payload == nil { return nil } if req.Payload.Fields == nil { return nil } return protoToAnyMap(req.Payload.Fields) } func (req *WebhookRequest) GetPayloadParameter(key string) (any, bool) { // Just in case - I don't think we can iterate over a nil map. if req.Payload == nil { return nil, false } if req.Payload.Fields == nil { return nil, false } pv, ok := req.Payload.Fields[key] return protoToAny(pv), ok } // Testing func NewTestingWebhookRequest(session, payload, pageform map[string]any) (*WebhookRequest, error) { return NewWebhookRequest().initTestingWebhookRequest(session, payload, pageform) } func (req *WebhookRequest) initTestingWebhookRequest(session, payload, pageform map[string]any) (*WebhookRequest, error) { // Provided for testing, normally http.Request.Context is flowed down. req.ctx = context.Background // All incoming WebhookRequests should have a session. req.initSessionInfo() req.SessionInfo.Session = uuid.New().String() // if session parameters are provided... if session != nil { err := req.setSessionParameters(session) if err != nil { return nil, err } } // if payload parameters are provided... if payload != nil { req.initPayload() err := req.setPayload(payload) if err != nil { return nil, err } } // if pageForm parameters are provided... if pageform != nil { req.initPageInfo() err := req.setPageFormParameters(pageform) if err != nil { return nil, err } } return req, nil } // yaquino: 2022-10-08Review this...! func (req *WebhookRequest) TestCxHandler(out io.Writer, h HandlerFunc) (*WebhookResponse, error) { if req.ctx == nil { req.ctx = context.Background } res := req.initializeResponse() err := h(res, req) if err != nil { return nil, err } err = res.WriteResponse(out) if err != nil { return nil, err } return res, nil }