Wox/wox.core/ui/http.go

247 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ui
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"wox/plugin"
"wox/setting/definition"
"wox/ui/dto"
"wox/util"
"github.com/olahol/melody"
"github.com/rs/cors"
"github.com/samber/lo"
)
var m *melody.Melody
type websocketMsgType string
const (
WebsocketMsgTypeRequest websocketMsgType = "WebsocketMsgTypeRequest"
WebsocketMsgTypeResponse websocketMsgType = "WebsocketMsgTypeResponse"
)
type WebsocketMsg struct {
RequestId string // unique id for each request
TraceId string // trace id between ui and wox, used for logging
Type websocketMsgType
Method string
Success bool
Data any
}
type RestResponse struct {
Success bool
Message string
Data any
}
func writeSuccessResponse(w http.ResponseWriter, data any) {
d, marshalErr := json.Marshal(RestResponse{
Success: true,
Message: "",
Data: data,
})
if marshalErr != nil {
writeErrorResponse(w, marshalErr.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(d)
}
func writeErrorResponse(w http.ResponseWriter, errMsg string) {
d, _ := json.Marshal(RestResponse{
Success: false,
Message: errMsg,
Data: "",
})
w.Header().Set("Content-Type", "application/json")
w.Write(d)
}
func serveAndWait(ctx context.Context, port int) {
m = melody.New()
m.Config.MaxMessageSize = 1024 * 1024 * 10 // 10MB
m.Config.MessageBufferSize = 1024 * 1024 // 1MB
mux := http.NewServeMux()
for path, callback := range routers {
//add panic handler
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
defer util.GoRecover(ctx, "http request panic", func(err error) {
writeErrorResponse(w, err.Error())
})
callback(w, r)
})
}
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
m.HandleRequest(w, r)
})
m.HandleMessage(func(s *melody.Session, msg []byte) {
ctxNew := util.NewTraceContext()
if strings.Contains(string(msg), string(WebsocketMsgTypeRequest)) {
var request WebsocketMsg
unmarshalErr := json.Unmarshal(msg, &request)
if unmarshalErr != nil {
logger.Error(ctxNew, fmt.Sprintf("failed to unmarshal websocket request: %s", unmarshalErr.Error()))
return
}
util.Go(ctxNew, "handle ui query", func() {
traceCtx := context.WithValue(ctxNew, util.ContextKeyTraceId, request.TraceId)
onUIWebsocketRequest(traceCtx, request)
})
} else if strings.Contains(string(msg), string(WebsocketMsgTypeResponse)) {
var response WebsocketMsg
unmarshalErr := json.Unmarshal(msg, &response)
if unmarshalErr != nil {
logger.Error(ctxNew, fmt.Sprintf("failed to unmarshal websocket response: %s", unmarshalErr.Error()))
return
}
util.Go(ctxNew, "handle ui response", func() {
traceCtx := context.WithValue(ctxNew, util.ContextKeyTraceId, response.TraceId)
onUIWebsocketResponse(traceCtx, response)
})
} else {
logger.Error(ctxNew, fmt.Sprintf("unknown websocket msg: %s", string(msg)))
}
})
logger.Info(ctx, fmt.Sprintf("websocket server start atws://localhost:%d", port))
handler := cors.Default().Handler(mux)
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", port), handler)
if err != nil {
logger.Error(ctx, fmt.Sprintf("failed to start server: %s", err.Error()))
}
}
func requestUI(ctx context.Context, request WebsocketMsg) error {
request.Type = WebsocketMsgTypeRequest
request.Success = true
marshalData, marshalErr := json.Marshal(request)
if marshalErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to marshal websocket request: %s", marshalErr.Error()))
return marshalErr
}
jsonData, _ := json.Marshal(request.Data)
util.GetLogger().Info(ctx, fmt.Sprintf("[Wox -> UI] %s: %s", request.Method, jsonData))
return m.Broadcast(marshalData)
}
func responseUI(ctx context.Context, response WebsocketMsg) {
response.Type = WebsocketMsgTypeResponse
marshalData, marshalErr := json.Marshal(response)
if marshalErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to marshal websocket response: %s", marshalErr.Error()))
return
}
m.Broadcast(marshalData)
}
func responseUISuccessWithData(ctx context.Context, request WebsocketMsg, data any) {
responseUI(ctx, WebsocketMsg{
RequestId: request.RequestId,
TraceId: util.GetContextTraceId(ctx),
Type: WebsocketMsgTypeResponse,
Method: request.Method,
Success: true,
Data: data,
})
}
func responseUISuccess(ctx context.Context, request WebsocketMsg) {
responseUISuccessWithData(ctx, request, nil)
}
func responseUIError(ctx context.Context, request WebsocketMsg, errMsg string) {
responseUI(ctx, WebsocketMsg{
RequestId: request.RequestId,
TraceId: util.GetContextTraceId(ctx),
Type: WebsocketMsgTypeResponse,
Method: request.Method,
Success: false,
Data: errMsg,
})
}
func convertPluginDto(ctx context.Context, pluginDto dto.PluginDto, pluginInstance *plugin.Instance) dto.PluginDto {
if pluginInstance != nil {
logger.Debug(ctx, fmt.Sprintf("get plugin setting: %s", pluginInstance.Metadata.Name))
pluginDto.SettingDefinitions = lo.Filter(pluginInstance.Metadata.SettingDefinitions, func(item definition.PluginSettingDefinitionItem, _ int) bool {
return !lo.Contains(item.DisabledInPlatforms, util.GetCurrentPlatform())
})
// replace dynamic setting definition
var removedKeys []string
for i, settingDefinition := range pluginDto.SettingDefinitions {
if settingDefinition.Type == definition.PluginSettingDefinitionTypeDynamic {
replaced := false
for _, callback := range pluginInstance.DynamicSettingCallbacks {
newSettingDefinitionJson := callback(settingDefinition.Value.GetKey())
var newSettingDefinition definition.PluginSettingDefinitionItem
unmarshalErr := json.Unmarshal([]byte(newSettingDefinitionJson), &newSettingDefinition)
if unmarshalErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to unmarshal dynamic setting: %s", unmarshalErr.Error()))
continue
}
if newSettingDefinition.Value != nil && newSettingDefinition.Type != definition.PluginSettingDefinitionTypeDynamic {
logger.Debug(ctx, fmt.Sprintf("dynamic setting replaced: %s(%s) -> %s(%s)", settingDefinition.Value.GetKey(), settingDefinition.Type, newSettingDefinition.Value.GetKey(), newSettingDefinition.Type))
pluginDto.SettingDefinitions[i] = newSettingDefinition
replaced = true
break
}
}
if !replaced {
logger.Error(ctx, "dynamic setting not replaced")
//remove invalid dynamic setting
removedKeys = append(removedKeys, settingDefinition.Value.GetKey())
}
}
}
//remove invalid dynamic setting
pluginDto.SettingDefinitions = lo.Filter(pluginDto.SettingDefinitions, func(item definition.PluginSettingDefinitionItem, _ int) bool {
if item.Value == nil {
return true
}
return !lo.Contains(removedKeys, item.Value.GetKey())
})
//translate setting definition labels
for i := range pluginDto.SettingDefinitions {
if pluginDto.SettingDefinitions[i].Value != nil {
pluginDto.SettingDefinitions[i].Value.Translate(pluginInstance.API.GetTranslation)
}
}
var definitionSettings = util.NewHashMap[string, string]()
for _, item := range pluginDto.SettingDefinitions {
if item.Value != nil {
settingValue := pluginInstance.API.GetSetting(ctx, item.Value.GetKey())
definitionSettings.Store(item.Value.GetKey(), settingValue)
}
}
pluginDto.Setting = *pluginInstance.Setting
//only return user pre-defined settings
pluginDto.Setting.Settings = definitionSettings
pluginDto.Features = pluginInstance.Metadata.Features
pluginDto.TriggerKeywords = pluginInstance.GetTriggerKeywords()
}
return pluginDto
}