diff --git a/plugins/host/go/nd_host_websocket.go b/plugins/host/go/nd_host_websocket.go new file mode 100644 index 000000000..adb4a169c --- /dev/null +++ b/plugins/host/go/nd_host_websocket.go @@ -0,0 +1,167 @@ +// Code generated by hostgen. DO NOT EDIT. +// +// This file contains client wrappers for the WebSocket host service. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package main + +import ( + "encoding/json" + "errors" + + "github.com/extism/go-pdk" +) + +// websocket_connect is the host function provided by Navidrome. +// +//go:wasmimport extism:host/user websocket_connect +func websocket_connect(uint64, uint64, uint64) uint64 + +// websocket_sendtext is the host function provided by Navidrome. +// +//go:wasmimport extism:host/user websocket_sendtext +func websocket_sendtext(uint64, uint64) uint64 + +// websocket_sendbinary is the host function provided by Navidrome. +// +//go:wasmimport extism:host/user websocket_sendbinary +func websocket_sendbinary(uint64, uint64) uint64 + +// websocket_close is the host function provided by Navidrome. +// +//go:wasmimport extism:host/user websocket_close +func websocket_close(uint64, int32, uint64) uint64 + +// WebSocketConnectResponse is the response type for WebSocket.Connect. +type WebSocketConnectResponse struct { + NewConnectionID string `json:"newConnectionID,omitempty"` + Error string `json:"error,omitempty"` +} + +// WebSocketConnect calls the websocket_connect host function. +// Connect establishes a WebSocket connection to the specified URL. +// +// Plugins that use this function must also implement the WebSocketCallback capability +// to receive incoming messages and connection events. +// +// Parameters: +// - url: The WebSocket URL to connect to (ws:// or wss://) +// - headers: Optional HTTP headers to include in the handshake request +// - connectionID: Optional unique identifier for the connection. If empty, one will be generated +// +// Returns the connection ID that can be used to send messages or close the connection, +// or an error if the connection fails. +func WebSocketConnect(url string, headers map[string]string, connectionID string) (*WebSocketConnectResponse, error) { + urlMem := pdk.AllocateString(url) + defer urlMem.Free() + headersBytes, err := json.Marshal(headers) + if err != nil { + return nil, err + } + headersMem := pdk.AllocateBytes(headersBytes) + defer headersMem.Free() + connectionIDMem := pdk.AllocateString(connectionID) + defer connectionIDMem.Free() + + // Call the host function + responsePtr := websocket_connect(urlMem.Offset(), headersMem.Offset(), connectionIDMem.Offset()) + + // Read the response from memory + responseMem := pdk.FindMemory(responsePtr) + responseBytes := responseMem.ReadBytes() + + // Parse the response + var response WebSocketConnectResponse + if err := json.Unmarshal(responseBytes, &response); err != nil { + return nil, err + } + + return &response, nil +} + +// WebSocketSendText calls the websocket_sendtext host function. +// SendText sends a text message over an established WebSocket connection. +// +// Parameters: +// - connectionID: The connection identifier returned by Connect +// - message: The text message to send +// +// Returns an error if the connection is not found or if sending fails. +func WebSocketSendText(connectionID string, message string) error { + connectionIDMem := pdk.AllocateString(connectionID) + defer connectionIDMem.Free() + messageMem := pdk.AllocateString(message) + defer messageMem.Free() + + // Call the host function + responsePtr := websocket_sendtext(connectionIDMem.Offset(), messageMem.Offset()) + + // Read the response from memory + responseMem := pdk.FindMemory(responsePtr) + errStr := string(responseMem.ReadBytes()) + + if errStr != "" { + return errors.New(errStr) + } + + return nil +} + +// WebSocketSendBinary calls the websocket_sendbinary host function. +// SendBinary sends binary data over an established WebSocket connection. +// +// Parameters: +// - connectionID: The connection identifier returned by Connect +// - data: The binary data to send +// +// Returns an error if the connection is not found or if sending fails. +func WebSocketSendBinary(connectionID string, data []byte) error { + connectionIDMem := pdk.AllocateString(connectionID) + defer connectionIDMem.Free() + dataMem := pdk.AllocateBytes(data) + defer dataMem.Free() + + // Call the host function + responsePtr := websocket_sendbinary(connectionIDMem.Offset(), dataMem.Offset()) + + // Read the response from memory + responseMem := pdk.FindMemory(responsePtr) + errStr := string(responseMem.ReadBytes()) + + if errStr != "" { + return errors.New(errStr) + } + + return nil +} + +// WebSocketClose calls the websocket_close host function. +// Close gracefully closes a WebSocket connection. +// +// Parameters: +// - connectionID: The connection identifier returned by Connect +// - code: WebSocket close status code (e.g., 1000 for normal closure) +// - reason: Optional human-readable reason for closing +// +// Returns an error if the connection is not found or if closing fails. +func WebSocketClose(connectionID string, code int32, reason string) error { + connectionIDMem := pdk.AllocateString(connectionID) + defer connectionIDMem.Free() + reasonMem := pdk.AllocateString(reason) + defer reasonMem.Free() + + // Call the host function + responsePtr := websocket_close(connectionIDMem.Offset(), code, reasonMem.Offset()) + + // Read the response from memory + responseMem := pdk.FindMemory(responsePtr) + errStr := string(responseMem.ReadBytes()) + + if errStr != "" { + return errors.New(errStr) + } + + return nil +} diff --git a/plugins/host/websocket.go b/plugins/host/websocket.go new file mode 100644 index 000000000..c8c825794 --- /dev/null +++ b/plugins/host/websocket.go @@ -0,0 +1,59 @@ +package host + +import "context" + +// WebSocketService provides WebSocket communication capabilities for plugins. +// +// This service allows plugins to establish WebSocket connections to external services, +// send and receive messages, and manage connection lifecycle. Plugins using this service +// must implement the WebSocketCallback capability to receive incoming messages and +// connection state changes. +// +//nd:hostservice name=WebSocket permission=websocket +type WebSocketService interface { + // Connect establishes a WebSocket connection to the specified URL. + // + // Plugins that use this function must also implement the WebSocketCallback capability + // to receive incoming messages and connection events. + // + // Parameters: + // - url: The WebSocket URL to connect to (ws:// or wss://) + // - headers: Optional HTTP headers to include in the handshake request + // - connectionID: Optional unique identifier for the connection. If empty, one will be generated + // + // Returns the connection ID that can be used to send messages or close the connection, + // or an error if the connection fails. + //nd:hostfunc + Connect(ctx context.Context, url string, headers map[string]string, connectionID string) (newConnectionID string, err error) + + // SendText sends a text message over an established WebSocket connection. + // + // Parameters: + // - connectionID: The connection identifier returned by Connect + // - message: The text message to send + // + // Returns an error if the connection is not found or if sending fails. + //nd:hostfunc + SendText(ctx context.Context, connectionID, message string) error + + // SendBinary sends binary data over an established WebSocket connection. + // + // Parameters: + // - connectionID: The connection identifier returned by Connect + // - data: The binary data to send + // + // Returns an error if the connection is not found or if sending fails. + //nd:hostfunc + SendBinary(ctx context.Context, connectionID string, data []byte) error + + // Close gracefully closes a WebSocket connection. + // + // Parameters: + // - connectionID: The connection identifier returned by Connect + // - code: WebSocket close status code (e.g., 1000 for normal closure) + // - reason: Optional human-readable reason for closing + // + // Returns an error if the connection is not found or if closing fails. + //nd:hostfunc + Close(ctx context.Context, connectionID string, code int32, reason string) error +} diff --git a/plugins/host/websocket_gen.go b/plugins/host/websocket_gen.go new file mode 100644 index 000000000..7757e44d6 --- /dev/null +++ b/plugins/host/websocket_gen.go @@ -0,0 +1,192 @@ +// Code generated by hostgen. DO NOT EDIT. + +package host + +import ( + "context" + "encoding/json" + + extism "github.com/extism/go-sdk" +) + +// WebSocketConnectRequest is the request type for WebSocket.Connect. +type WebSocketConnectRequest struct { + Url string `json:"url"` + Headers map[string]string `json:"headers"` + ConnectionID string `json:"connectionID"` +} + +// WebSocketConnectResponse is the response type for WebSocket.Connect. +type WebSocketConnectResponse struct { + NewConnectionID string `json:"newConnectionID,omitempty"` + Error string `json:"error,omitempty"` +} + +// RegisterWebSocketHostFunctions registers WebSocket service host functions. +// The returned host functions should be added to the plugin's configuration. +func RegisterWebSocketHostFunctions(service WebSocketService) []extism.HostFunction { + return []extism.HostFunction{ + newWebSocketConnectHostFunction(service), + newWebSocketSendTextHostFunction(service), + newWebSocketSendBinaryHostFunction(service), + newWebSocketCloseHostFunction(service), + } +} + +func newWebSocketConnectHostFunction(service WebSocketService) extism.HostFunction { + return extism.NewHostFunctionWithStack( + "websocket_connect", + func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { + // Read JSON request from plugin memory + reqBytes, err := p.ReadBytes(stack[0]) + if err != nil { + websocketWriteError(p, stack, err) + return + } + var req WebSocketConnectRequest + if err := json.Unmarshal(reqBytes, &req); err != nil { + websocketWriteError(p, stack, err) + return + } + + // Call the service method + newconnectionid, err := service.Connect(ctx, req.Url, req.Headers, req.ConnectionID) + if err != nil { + websocketWriteError(p, stack, err) + return + } + // Write JSON response to plugin memory + resp := WebSocketConnectResponse{ + NewConnectionID: newconnectionid, + } + websocketWriteResponse(p, stack, resp) + }, + []extism.ValueType{extism.ValueTypePTR}, + []extism.ValueType{extism.ValueTypePTR}, + ) +} + +func newWebSocketSendTextHostFunction(service WebSocketService) extism.HostFunction { + return extism.NewHostFunctionWithStack( + "websocket_sendtext", + func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { + // Read parameters from stack + connectionID, err := p.ReadString(stack[0]) + if err != nil { + return + } + message, err := p.ReadString(stack[1]) + if err != nil { + return + } + + // Call the service method + err = service.SendText(ctx, connectionID, message) + if err != nil { + // Write error string to plugin memory + if ptr, err := p.WriteString(err.Error()); err == nil { + stack[0] = ptr + } + return + } + // Write empty string to indicate success + if ptr, err := p.WriteString(""); err == nil { + stack[0] = ptr + } + }, + []extism.ValueType{extism.ValueTypePTR, extism.ValueTypePTR}, + []extism.ValueType{extism.ValueTypePTR}, + ) +} + +func newWebSocketSendBinaryHostFunction(service WebSocketService) extism.HostFunction { + return extism.NewHostFunctionWithStack( + "websocket_sendbinary", + func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { + // Read parameters from stack + connectionID, err := p.ReadString(stack[0]) + if err != nil { + return + } + data, err := p.ReadBytes(stack[1]) + if err != nil { + return + } + + // Call the service method + err = service.SendBinary(ctx, connectionID, data) + if err != nil { + // Write error string to plugin memory + if ptr, err := p.WriteString(err.Error()); err == nil { + stack[0] = ptr + } + return + } + // Write empty string to indicate success + if ptr, err := p.WriteString(""); err == nil { + stack[0] = ptr + } + }, + []extism.ValueType{extism.ValueTypePTR, extism.ValueTypePTR}, + []extism.ValueType{extism.ValueTypePTR}, + ) +} + +func newWebSocketCloseHostFunction(service WebSocketService) extism.HostFunction { + return extism.NewHostFunctionWithStack( + "websocket_close", + func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { + // Read parameters from stack + connectionID, err := p.ReadString(stack[0]) + if err != nil { + return + } + code := extism.DecodeI32(stack[1]) + reason, err := p.ReadString(stack[2]) + if err != nil { + return + } + + // Call the service method + err = service.Close(ctx, connectionID, code, reason) + if err != nil { + // Write error string to plugin memory + if ptr, err := p.WriteString(err.Error()); err == nil { + stack[0] = ptr + } + return + } + // Write empty string to indicate success + if ptr, err := p.WriteString(""); err == nil { + stack[0] = ptr + } + }, + []extism.ValueType{extism.ValueTypePTR, extism.ValueTypeI32, extism.ValueTypePTR}, + []extism.ValueType{extism.ValueTypePTR}, + ) +} + +// websocketWriteResponse writes a JSON response to plugin memory. +func websocketWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) { + respBytes, err := json.Marshal(resp) + if err != nil { + websocketWriteError(p, stack, err) + return + } + respPtr, err := p.WriteBytes(respBytes) + if err != nil { + stack[0] = 0 + return + } + stack[0] = respPtr +} + +// websocketWriteError writes an error response to plugin memory. +func websocketWriteError(p *extism.CurrentPlugin, stack []uint64, err error) { + errResp := struct { + Error string `json:"error"` + }{Error: err.Error()} + respBytes, _ := json.Marshal(errResp) + respPtr, _ := p.WriteBytes(respBytes) + stack[0] = respPtr +} diff --git a/plugins/schemas/websocket_callback.yaml b/plugins/schemas/websocket_callback.yaml new file mode 100644 index 000000000..a7b76102e --- /dev/null +++ b/plugins/schemas/websocket_callback.yaml @@ -0,0 +1,136 @@ +version: v1-draft + +exports: + nd_websocket_on_text_message: + description: | + Called when a text message is received on a WebSocket connection. + Plugins that use the WebSocket host service must export this function + to handle incoming text messages. + input: + $ref: "#/components/schemas/OnTextMessageInput" + contentType: application/json + output: + $ref: "#/components/schemas/OnTextMessageOutput" + contentType: application/json + + nd_websocket_on_binary_message: + description: | + Called when a binary message is received on a WebSocket connection. + Plugins that use the WebSocket host service must export this function + to handle incoming binary data. + input: + $ref: "#/components/schemas/OnBinaryMessageInput" + contentType: application/json + output: + $ref: "#/components/schemas/OnBinaryMessageOutput" + contentType: application/json + + nd_websocket_on_error: + description: | + Called when an error occurs on a WebSocket connection. + Plugins that use the WebSocket host service must export this function + to handle connection errors. + input: + $ref: "#/components/schemas/OnErrorInput" + contentType: application/json + output: + $ref: "#/components/schemas/OnErrorOutput" + contentType: application/json + + nd_websocket_on_close: + description: | + Called when a WebSocket connection is closed. + Plugins that use the WebSocket host service must export this function + to handle connection closure events. + input: + $ref: "#/components/schemas/OnCloseInput" + contentType: application/json + output: + $ref: "#/components/schemas/OnCloseOutput" + contentType: application/json + +components: + schemas: + OnTextMessageInput: + description: Input provided when a text message is received + properties: + connection_id: + type: string + description: | + The unique identifier for the WebSocket connection that received the message. + message: + type: string + description: | + The text message content received from the WebSocket. + required: + - connection_id + - message + + OnTextMessageOutput: + description: Output from the text message handler + properties: {} + + OnBinaryMessageInput: + description: Input provided when a binary message is received + properties: + connection_id: + type: string + description: | + The unique identifier for the WebSocket connection that received the message. + data: + type: string + format: byte + description: | + The binary data received from the WebSocket, encoded as base64. + required: + - connection_id + - data + + OnBinaryMessageOutput: + description: Output from the binary message handler + properties: {} + + OnErrorInput: + description: Input provided when an error occurs on a WebSocket connection + properties: + connection_id: + type: string + description: | + The unique identifier for the WebSocket connection where the error occurred. + error: + type: string + description: | + The error message describing what went wrong. + required: + - connection_id + - error + + OnErrorOutput: + description: Output from the error handler + properties: {} + + OnCloseInput: + description: Input provided when a WebSocket connection is closed + properties: + connection_id: + type: string + description: | + The unique identifier for the WebSocket connection that was closed. + code: + type: integer + format: int32 + description: | + The WebSocket close status code (e.g., 1000 for normal closure, + 1001 for going away, 1006 for abnormal closure). + reason: + type: string + description: | + The human-readable reason for the connection closure, if provided. + required: + - connection_id + - code + - reason + + OnCloseOutput: + description: Output from the close handler + properties: {}