bigbluebutton-Github/bbb-graphql-middleware/internal/gql_actions/client.go
Gustavo Trott cccd2d78c7
Refactor the logger to add more context like meetingId, userId and sessionToken. (#21188)
For DEBUG logs it also includes the file/line and function of the caller.
2024-09-18 08:48:36 -04:00

221 lines
6.6 KiB
Go

package gql_actions
import (
"bbb-graphql-middleware/config"
"bbb-graphql-middleware/internal/common"
"bytes"
"encoding/json"
"fmt"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"regexp"
"strings"
"time"
)
var graphqlActionsUrl = config.GetConfig().GraphqlActions.Url
func GraphqlActionsClient(
browserConnection *common.BrowserConnection) error {
browserConnection.Logger.Debug("Starting GraphqlActionsClient")
defer browserConnection.Logger.Debug("Finished GraphqlActionsClient")
RangeLoop:
for {
select {
case <-browserConnection.Context.Done():
break RangeLoop
case <-browserConnection.GraphqlActionsContext.Done():
browserConnection.Logger.Debug("GraphqlActionsContext cancelled!")
break RangeLoop
case fromBrowserMessage := <-browserConnection.FromBrowserToGqlActionsChannel.ReceiveChannel():
{
if fromBrowserMessage == nil {
continue
}
var browserMessage common.BrowserSubscribeMessage
err := json.Unmarshal(fromBrowserMessage, &browserMessage)
if err != nil {
browserConnection.Logger.Errorf("failed to unmarshal message: %v", err)
continue
}
if browserMessage.Type == "subscribe" {
var errorMessage string
var mutationFuncName string
if strings.HasPrefix(browserMessage.Payload.Query, "mutation") {
if funcName, inputs, err := parseGraphQLMutation(browserMessage.Payload.Query, browserMessage.Payload.Variables); err == nil {
mutationFuncName = funcName
if err = SendGqlActionsRequest(funcName, inputs, browserConnection.BBBWebSessionVariables, browserConnection.Logger); err == nil {
//Add Prometheus Metrics
common.GqlMutationsCounter.With(prometheus.Labels{"operationName": browserMessage.Payload.OperationName}).Inc()
} else {
errorMessage = err.Error()
browserConnection.Logger.Error("It was not able to send the request to Graphql Actions", err)
}
} else {
errorMessage = "It was not able to parse graphQL query"
browserConnection.Logger.Error("It was not able to parse graphQL query", err)
}
}
if errorMessage != "" {
//Error on sending action, return error msg to client
browserResponseData := map[string]interface{}{
"id": browserMessage.ID,
"type": "error",
"payload": []interface{}{
map[string]interface{}{
"message": errorMessage,
},
},
}
jsonData, _ := json.Marshal(browserResponseData)
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
} else {
//Action sent successfully, return data msg to client
browserResponseData := map[string]interface{}{
"id": browserMessage.ID,
"type": "next",
"payload": map[string]interface{}{
"data": map[string]interface{}{
mutationFuncName: true,
},
},
}
jsonData, _ := json.Marshal(browserResponseData)
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
}
//Return complete msg to client
browserResponseComplete := map[string]interface{}{
"id": browserMessage.ID,
"type": "complete",
}
jsonData, _ := json.Marshal(browserResponseComplete)
browserConnection.FromHasuraToBrowserChannel.Send(jsonData)
}
//Fallback to Hasura was disabled (keeping the code temporarily)
//fromBrowserToHasuraChannel.Send(fromBrowserMessage)
}
}
}
return nil
}
func SendGqlActionsRequest(funcName string, inputs map[string]interface{}, sessionVariables map[string]string, bcLogger *log.Entry) error {
logger := bcLogger.WithField("funcName", funcName).WithField("inputs", inputs)
data := GqlActionsRequestBody{
Action: GqlActionsAction{
Name: funcName,
},
Input: inputs,
SessionVariables: sessionVariables,
}
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
if graphqlActionsUrl == "" {
return fmt.Errorf("No Graphql Actions Url (BBB_GRAPHQL_MIDDLEWARE_GRAPHQL_ACTIONS_URL) set, aborting")
}
startedAt := time.Now()
response, err := http.Post(graphqlActionsUrl, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer response.Body.Close()
totalDurationMillis := time.Since(startedAt).Milliseconds()
logger = logger.WithField("duration", fmt.Sprintf("%v ms", totalDurationMillis)).WithField("statusCode", response.StatusCode)
logger.Tracef("Executed!")
if totalDurationMillis > 100 {
logger.Infof("Took too long to execute!")
}
if response.StatusCode != 200 {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err == nil {
if message, ok := result["message"].(string); ok {
logger.Errorf(string(jsonData), message, err)
return fmt.Errorf("graphql actions request failed: %s", message)
}
}
return fmt.Errorf("graphql actions request failed: %s", response.Status)
}
return nil
}
type GqlActionsRequestBody struct {
Action GqlActionsAction `json:"action"`
Input map[string]interface{} `json:"input"`
SessionVariables map[string]string `json:"session_variables"`
}
type GqlActionsAction struct {
Name string `json:"name"`
}
func parseGraphQLMutation(query string, variables map[string]interface{}) (string, map[string]interface{}, error) {
// Extract the function name from the query
reFuncName := regexp.MustCompile(`\{\s*(\w+)`)
funcNameMatch := reFuncName.FindStringSubmatch(query)
if len(funcNameMatch) < 2 {
return "", nil, fmt.Errorf("failed to extract function name from query")
}
funcName := funcNameMatch[1]
// Prepare to extract and parse parameters
queryParams := make(map[string]interface{})
reParams := regexp.MustCompile(`\{[^(]+\(([^)]+)\)`)
paramsMatch := reParams.FindStringSubmatch(query)
if len(paramsMatch) >= 2 {
paramSplitRegex := regexp.MustCompile("[,\n]")
params := paramSplitRegex.Split(paramsMatch[1], -1)
for _, param := range params {
cleanedParam := strings.ReplaceAll(param, " ", "")
if cleanedParam == "" {
continue
}
paramParts := strings.Split(cleanedParam, ":")
if len(paramParts) != 2 {
continue // Skip invalid params
}
paramName, paramValue := strings.Trim(paramParts[0], " \t"), paramParts[1]
// Handle variable substitution
if strings.HasPrefix(paramValue, "$") {
varName := strings.TrimPrefix(paramValue, "$")
if value, ok := variables[varName]; ok {
queryParams[paramName] = value
}
} else {
queryParams[paramName] = paramValue
}
}
}
return funcName, queryParams, nil
}