diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9f3bc52 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24.6 + - name: Install dependencies + run: go mod download + - name: Test + run: cd v2 && go test -v ./... + + test-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24.6 + - name: Install dependencies + run: go mod download + - name: Test + run: cd v2 && go test -v ./... \ No newline at end of file diff --git a/v2/cli.go b/v2/cli.go new file mode 100644 index 0000000..977718e --- /dev/null +++ b/v2/cli.go @@ -0,0 +1,141 @@ +package plugin + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "flag" + "os" + "slices" + "strconv" + "strings" + + "github.com/gotify/plugin-api/v2/transport" +) + +// / PluginCli implements the CLI interface for a Gotify plugin. +type PluginCli struct { + flagSet *flag.FlagSet + KexReqFile *os.File + KexRespFile *os.File + Debug bool +} + +// ParsePluginCli parses the CLI arguments and returns a PluginCli instance. +func ParsePluginCli(args []string) (*PluginCli, error) { + flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + var kexReqFileName string + var kexRespFileName string + var debug bool + flagSet.StringVar(&kexReqFileName, "kex-req-file", os.Getenv("GOTIFY_PLUGIN_KEX_REQ_FILE"), "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") + flagSet.StringVar(&kexRespFileName, "kex-resp-file", os.Getenv("GOTIFY_PLUGIN_KEX_RESP_FILE"), "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") + flagSet.BoolVar(&debug, "debug", slices.Contains([]string{"true", "1", "yes", "y"}, strings.ToLower(os.Getenv("GOTIFY_PLUGIN_DEBUG"))), "Enable debug mode.") + if err := flagSet.Parse(args); err != nil { + return nil, err + } + + var kexReqFile *os.File + var kexRespFile *os.File + var err error + + if fdNumber, found := strings.CutPrefix(kexReqFileName, "/proc/self/fd/"); found { + fdNumber, err := strconv.ParseUint(fdNumber, 10, 64) + kexReqFile = os.NewFile(uintptr(fdNumber), kexReqFileName) + if err != nil { + return nil, err + } + } else { + kexReqFile, err = os.OpenFile(kexReqFileName, os.O_WRONLY, 0) + if err != nil { + return nil, err + } + } + if fdNumber, found := strings.CutPrefix(kexRespFileName, "/proc/self/fd/"); found { + fdNumber, err := strconv.ParseUint(fdNumber, 10, 64) + if err != nil { + return nil, err + } + kexRespFile = os.NewFile(uintptr(fdNumber), kexRespFileName) + } else { + kexRespFile, err = os.OpenFile(kexRespFileName, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + } + + return &PluginCli{ + flagSet: flagSet, + KexReqFile: kexReqFile, + KexRespFile: kexRespFile, + Debug: debug, + }, nil +} + +// Kex performs the key exchange through secure file descriptors provided in the arguments. +func (f *PluginCli) Kex(modulePath string, certPool *x509.CertPool) (certChain []tls.Certificate, err error) { + // perform key exchange through secure file descriptors + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: transport.BuildPluginTLSName("*", modulePath), + }, + }, priv) + + if err != nil { + return nil, err + } + if _, err := f.KexReqFile.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + })); err != nil { + return nil, err + } + + var certificateChain []tls.Certificate + + if err := transport.IteratePEMFile(f.KexRespFile, func(block *pem.Block) (continueIterate bool, err error) { + if block.Type == "CERTIFICATE" { + parsedCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return false, err + } + if certPool != nil { + certPool.AddCert(parsedCert) + } + certificateChain = append(certificateChain, tls.Certificate{ + Certificate: [][]byte{block.Bytes}, + Leaf: parsedCert, + }) + return true, nil + } + return true, nil + }); err != nil { + return nil, err + } + + if len(certificateChain) == 0 { + return nil, errors.New("no certificate chain found in kex response file") + } + + certificateChain[0].PrivateKey = priv + + return certificateChain, nil +} + +// Close closes any file descriptors associated with the PluginCli instance. +func (f *PluginCli) Close() error { + if err := f.KexReqFile.Close(); err != nil { + return err + } + if err := f.KexRespFile.Close(); err != nil { + return err + } + return nil +} diff --git a/v2/examples_v1/echo/echo.go b/v2/examples_v1/echo/echo.go new file mode 100644 index 0000000..fb47cfd --- /dev/null +++ b/v2/examples_v1/echo/echo.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/url" + + "github.com/gin-gonic/gin" + "github.com/gotify/plugin-api" +) + +// GetGotifyPluginInfo returns gotify plugin info. +func GetGotifyPluginInfo() plugin.Info { + return plugin.Info{ + ModulePath: "github.com/gotify/server/v2/plugin/example/echo", + Name: "test plugin", + } +} + +// EchoPlugin is the gotify plugin instance. +type EchoPlugin struct { + msgHandler plugin.MessageHandler + storageHandler plugin.StorageHandler + config *Config + basePath string +} + +// SetStorageHandler implements plugin.Storager +func (c *EchoPlugin) SetStorageHandler(h plugin.StorageHandler) { + c.storageHandler = h +} + +// SetMessageHandler implements plugin.Messenger. +func (c *EchoPlugin) SetMessageHandler(h plugin.MessageHandler) { + c.msgHandler = h +} + +// Storage defines the plugin storage scheme +type Storage struct { + CalledTimes int `json:"called_times"` +} + +// Config defines the plugin config scheme +type Config struct { + MagicString string `yaml:"magic_string"` +} + +// DefaultConfig implements plugin.Configurer +func (c *EchoPlugin) DefaultConfig() interface{} { + return &Config{ + MagicString: "hello world", + } +} + +// ValidateAndSetConfig implements plugin.Configurer +func (c *EchoPlugin) ValidateAndSetConfig(config interface{}) error { + c.config = config.(*Config) + return nil +} + +// Enable enables the plugin. +func (c *EchoPlugin) Enable() error { + log.Println("echo plugin enabled") + return nil +} + +// Disable disables the plugin. +func (c *EchoPlugin) Disable() error { + log.Println("echo plugin disbled") + return nil +} + +// RegisterWebhook implements plugin.Webhooker. +func (c *EchoPlugin) RegisterWebhook(baseURL string, g *gin.RouterGroup) { + c.basePath = baseURL + g.GET("/echo", func(ctx *gin.Context) { + + storage, _ := c.storageHandler.Load() + conf := new(Storage) + json.Unmarshal(storage, conf) + conf.CalledTimes++ + newStorage, _ := json.Marshal(conf) + c.storageHandler.Save(newStorage) + + c.msgHandler.SendMessage(plugin.Message{ + Title: "Hello received", + Message: fmt.Sprintf("echo server received a hello message %d times", conf.CalledTimes), + Priority: 2, + Extras: map[string]any{ + "plugin::name": "echo", + }, + }) + ctx.Writer.WriteString(fmt.Sprintf("Magic string is: %s\r\nEcho server running at %secho", c.config.MagicString, c.basePath)) + }) +} + +// GetDisplay implements plugin.Displayer. +func (c *EchoPlugin) GetDisplay(location *url.URL) string { + loc := &url.URL{ + Path: c.basePath, + } + if location != nil { + loc.Scheme = location.Scheme + loc.Host = location.Host + } + loc = loc.ResolveReference(&url.URL{ + Path: "echo", + }) + return "Echo plugin running at: " + loc.String() +} + +// NewGotifyPluginInstance creates a plugin instance for a user context. +func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { + return &EchoPlugin{} +} + +func main() { + panic("this should be built as go plugin") +} diff --git a/v2/examples_v1/echo/echo_test.go b/v2/examples_v1/echo/echo_test.go new file mode 100644 index 0000000..c4edb63 --- /dev/null +++ b/v2/examples_v1/echo/echo_test.go @@ -0,0 +1,230 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http/httptest" + "os" + "reflect" + "slices" + "strings" + "testing" + + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2" + "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestEcho(t *testing.T) { + pluginInfo := GetGotifyPluginInfo() + + client, err := transport.NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + var reqRx, reqTx uintptr + var respRx, respTx uintptr + + transport.NewAnonPipe(&reqRx, &reqTx, true) + transport.NewAnonPipe(&respRx, &respTx, true) + + go func() { + reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) + defer reqFileRx.Close() + respFileTx := os.NewFile(respTx, fmt.Sprintf("/proc/self/fd/%d", respTx)) + defer respFileTx.Close() + if err := client.Kex(reqFileRx, respFileTx); err != nil { + panic(err) + } + }() + + compatV1, err := plugin.NewCompatV1Rpc(&plugin.CompatV1{ + GetPluginInfo: GetGotifyPluginInfo, + GetInstance: func(user papiv1.UserContext) (papiv1.Plugin, error) { + return NewGotifyPluginInstance(user), nil + }, + }, []string{ + "-kex-req-file", fmt.Sprintf("/proc/self/fd/%d", reqTx), + "-kex-resp-file", fmt.Sprintf("/proc/self/fd/%d", respRx), + }) + if err != nil { + t.Fatal(err) + } + + listener, addr, err := transport.NewListener() + if err != nil { + t.Fatal(err) + } + go func() { + compatV1.ServeTLS(listener, "", "") + }() + + rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath)))) + if err != nil { + t.Fatal(err) + } + pluginClient := protobuf.NewPluginClient(rpcClient) + + version, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + if version.Name != pluginInfo.Name { + t.Fatal("expected ", pluginInfo.Name, " got ", version.Name) + } + if version.Version != pluginInfo.Version { + t.Fatal("expected ", pluginInfo.Version, " got ", version.Version) + } + + testUser := &papiv1.UserContext{ + ID: 1, + Name: "alice", + Admin: true, + } + webhookBasePath := "/plugin/echo-test/" + stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + ServerInfo: &protobuf.ServerInfo{ + Version: "1.0.0", + Capabilities: []protobuf.Capability{ + protobuf.Capability_DISPLAYER, + protobuf.Capability_CONFIGURER, + protobuf.Capability_WEBHOOKER, + protobuf.Capability_MESSENGER, + protobuf.Capability_STORAGER, + }, + }, + WebhookBasePath: &webhookBasePath, + Config: []byte{}, + Storage: []byte{}, + }) + if err != nil { + panic(err) + } + defer stream.CloseSend() + + var registeredCapabilities []protobuf.Capability + var passed bool + var storage []byte + expectedMagicString := "hello world" + ratchet := 1 + for { + msg, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + panic(err) + } + switch msg.Update.(type) { + case *protobuf.InstanceUpdate_Capable: + registeredCapabilities = append(registeredCapabilities, msg.GetCapable()) + case *protobuf.InstanceUpdate_Message: + msg := msg.GetMessage() + extrasNameJson := msg.GetExtras()["plugin::name"].GetJson() + if extrasNameJson != "\"echo\"" { + t.Fatal("expected ", "echo", " got ", extrasNameJson) + } + case *protobuf.InstanceUpdate_Storage: + storage = msg.GetStorage() + var decodedStorage Storage + json.Unmarshal(storage, &decodedStorage) + if decodedStorage.CalledTimes < ratchet { + t.Fatal("expected ", ratchet, " got ", decodedStorage.CalledTimes) + } else if decodedStorage.CalledTimes == ratchet+1 { + ratchet++ + } else if decodedStorage.CalledTimes > ratchet+1 { + t.Fatal("expected ", ratchet+1, " got ", decodedStorage.CalledTimes) + } + } + + slices.Sort(registeredCapabilities) + expectedCapabilities := []protobuf.Capability{ + protobuf.Capability_DISPLAYER, + protobuf.Capability_CONFIGURER, + protobuf.Capability_WEBHOOKER, + protobuf.Capability_MESSENGER, + protobuf.Capability_STORAGER, + } + slices.Sort(expectedCapabilities) + if reflect.DeepEqual(registeredCapabilities, expectedCapabilities) { + + displayerClient := protobuf.NewDisplayerClient(rpcClient) + displayResponse, err := displayerClient.Display(context.Background(), &protobuf.DisplayRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + Location: "https://gotify.example.com/", + }) + if err != nil { + panic(err) + } + if displayResponse.GetMarkdown() != "Echo plugin running at: https://gotify.example.com/plugin/echo-test/echo" { + t.Fatal("expected ", "Echo plugin running at: https://gotify.example.com/plugin/echo-test/echo", " got ", displayResponse.GetMarkdown()) + } + + tlsWebhookName := transport.BuildPluginTLSName(transport.PurposePluginWebhook, pluginInfo.ModulePath) + + for range 3 { + testreq := httptest.NewRequest("GET", "https://"+tlsWebhookName+"/plugin/echo-test/echo", nil) + recorder := httptest.NewRecorder() + compatV1.ServeHTTP(recorder, testreq) + if recorder.Code != 200 { + t.Fatal("expected 200 got ", recorder.Code) + } + if !strings.Contains(recorder.Body.String(), "Magic string is: "+expectedMagicString) { + t.Fatal("expected ", "Magic string is: "+expectedMagicString, " got ", recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "Echo server running at "+webhookBasePath+"echo") { + t.Fatal("expected ", "Echo server running at "+webhookBasePath+"echo", " got ", recorder.Body.String()) + } + configurerClient := protobuf.NewConfigurerClient(rpcClient) + expectedMagicString = fmt.Sprintf("test_%d", ratchet) + resp, err := configurerClient.ValidateAndSetConfig(context.Background(), &protobuf.ValidateAndSetConfigRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + Config: &protobuf.Config{ + Config: "magic_string: " + expectedMagicString, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp.GetResponse().(*protobuf.ValidateAndSetConfigResponse_Success) == nil { + t.Fatal("expected success") + } + } + + passed = true + _, err = pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + } + if len(registeredCapabilities) > len(expectedCapabilities) { + t.Fatal("more capabilities than expected") + } + } + if !passed { + t.Fatal("test failed: connection closed before all capabilities were tested") + } + if ratchet != 3 { + t.Fatal("expected called times to be 3 got ", ratchet) + } +} diff --git a/v2/examples_v1/minimal/minimal.go b/v2/examples_v1/minimal/minimal.go new file mode 100644 index 0000000..fb0a4e8 --- /dev/null +++ b/v2/examples_v1/minimal/minimal.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/gotify/plugin-api" +) + +// GetGotifyPluginInfo returns gotify plugin info +func GetGotifyPluginInfo() plugin.Info { + return plugin.Info{ + Name: "minimal plugin", + ModulePath: "github.com/gotify/server/v2/example/minimal", + } +} + +// Plugin is plugin instance +type Plugin struct { + enabled bool +} + +// Enable implements plugin.Plugin +func (c *Plugin) Enable() error { + c.enabled = true + return nil +} + +// Disable implements plugin.Plugin +func (c *Plugin) Disable() error { + c.enabled = false + return nil +} + +// NewGotifyPluginInstance creates a plugin instance for a user context. +func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { + return &Plugin{} +} + +func main() { + panic("this should be built as go plugin") +} diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go new file mode 100644 index 0000000..d8b366d --- /dev/null +++ b/v2/examples_v1/minimal/minimal_test.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + "os" + "testing" + "time" + + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2" + "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" + "google.golang.org/protobuf/types/known/emptypb" +) + +func testMinimalImpl(t *testing.T, listener net.Listener, addr string) { + assert := assert.New(t) + + pluginInfo := GetGotifyPluginInfo() + + client, err := transport.NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + var reqRx, reqTx uintptr + var respRx, respTx uintptr + + transport.NewAnonPipe(&reqRx, &reqTx, true) + transport.NewAnonPipe(&respRx, &respTx, true) + + go func() { + reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) + defer reqFileRx.Close() + respFileTx := os.NewFile(respTx, fmt.Sprintf("/proc/self/fd/%d", respTx)) + defer respFileTx.Close() + client.Kex(reqFileRx, respFileTx) + }() + + var thisInstance *Plugin + instanceInitialized := make(chan struct{}) + + compatV1, err := plugin.NewCompatV1Rpc(&plugin.CompatV1{ + GetPluginInfo: GetGotifyPluginInfo, + GetInstance: func(user papiv1.UserContext) (papiv1.Plugin, error) { + thisInstance = NewGotifyPluginInstance(user).(*Plugin) + defer close(instanceInitialized) + return thisInstance, nil + }, + }, []string{ + "-kex-req-file", fmt.Sprintf("/proc/self/fd/%d", reqTx), + "-kex-resp-file", fmt.Sprintf("/proc/self/fd/%d", respRx), + }) + assert.NoError(err) + + go func() { + compatV1.ServeTLS(listener, "", "") + }() + + rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath))), grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 10 * time.Millisecond, + PermitWithoutStream: true, + })) + assert.NoError(err) + + pluginClient := protobuf.NewPluginClient(rpcClient) + version, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + assert.NoError(err) + assert.Equal(pluginInfo.Name, version.Name) + assert.Equal(pluginInfo.Version, version.Version) + + stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + }) + assert.NoError(err) + + assert.NoError(stream.CloseSend()) + <-instanceInitialized + assert.True(thisInstance.enabled, "plugin should be enabled after connect") + + pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + for { + _, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + } + + streamHang, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + }) + if err != nil { + t.Fatal(err) + } + if err := streamHang.CloseSend(); err != nil { + t.Fatal(err) + } + _, err = streamHang.Recv() + assert.Error(err, "expected error when not sending keepalive") + assert.False(thisInstance.enabled, "plugin should be disabled after hang") + + streamReentrant, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + }) + assert.NoError(err) + pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + if err := streamReentrant.CloseSend(); err != nil { + assert.NoError(err) + } + for { + _, err := streamReentrant.Recv() + if err != nil { + if err == io.EOF { + break + } + assert.NoError(err) + } + } +} + +func TestMinimal(t *testing.T) { + listener, addr, err := transport.NewListener() + if err != nil { + t.Fatal(err) + } + testMinimalImpl(t, listener, addr) +} + +func TestMinimalTCP(t *testing.T) { + listener, addr, err := transport.NewTCPListener() + if err != nil { + t.Fatal(err) + } + testMinimalImpl(t, listener, addr) +} diff --git a/v2/generate.go b/v2/generate.go new file mode 100644 index 0000000..f9d33fd --- /dev/null +++ b/v2/generate.go @@ -0,0 +1,3 @@ +package plugin + +//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/server_events.proto diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go new file mode 100644 index 0000000..3911362 --- /dev/null +++ b/v2/generated/protobuf/config.pb.go @@ -0,0 +1,344 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.32.0 +// source: config.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The YAML configuration data. + Config string `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetConfig() string { + if x != nil { + return x.Config + } + return "" +} + +type DefaultConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the configuration belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DefaultConfigRequest) Reset() { + *x = DefaultConfigRequest{} + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DefaultConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DefaultConfigRequest) ProtoMessage() {} + +func (x *DefaultConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DefaultConfigRequest.ProtoReflect.Descriptor instead. +func (*DefaultConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *DefaultConfigRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + +type ValidateAndSetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the configuration belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // The YAML configuration data. + Config *Config `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateAndSetConfigRequest) Reset() { + *x = ValidateAndSetConfigRequest{} + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateAndSetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateAndSetConfigRequest) ProtoMessage() {} + +func (x *ValidateAndSetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateAndSetConfigRequest.ProtoReflect.Descriptor instead. +func (*ValidateAndSetConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ValidateAndSetConfigRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + +func (x *ValidateAndSetConfigRequest) GetConfig() *Config { + if x != nil { + return x.Config + } + return nil +} + +type ValidateAndSetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The response to the request. + // + // Types that are valid to be assigned to Response: + // + // *ValidateAndSetConfigResponse_Success + // *ValidateAndSetConfigResponse_ValidationError + Response isValidateAndSetConfigResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateAndSetConfigResponse) Reset() { + *x = ValidateAndSetConfigResponse{} + mi := &file_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateAndSetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateAndSetConfigResponse) ProtoMessage() {} + +func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateAndSetConfigResponse.ProtoReflect.Descriptor instead. +func (*ValidateAndSetConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidateAndSetConfigResponse) GetResponse() isValidateAndSetConfigResponse_Response { + if x != nil { + return x.Response + } + return nil +} + +func (x *ValidateAndSetConfigResponse) GetSuccess() *emptypb.Empty { + if x != nil { + if x, ok := x.Response.(*ValidateAndSetConfigResponse_Success); ok { + return x.Success + } + } + return nil +} + +func (x *ValidateAndSetConfigResponse) GetValidationError() *Error { + if x != nil { + if x, ok := x.Response.(*ValidateAndSetConfigResponse_ValidationError); ok { + return x.ValidationError + } + } + return nil +} + +type isValidateAndSetConfigResponse_Response interface { + isValidateAndSetConfigResponse_Response() +} + +type ValidateAndSetConfigResponse_Success struct { + // The success response. + Success *emptypb.Empty `protobuf:"bytes,1,opt,name=success,proto3,oneof"` +} + +type ValidateAndSetConfigResponse_ValidationError struct { + // The validation error response. + ValidationError *Error `protobuf:"bytes,2,opt,name=validation_error,json=validationError,proto3,oneof"` +} + +func (*ValidateAndSetConfigResponse_Success) isValidateAndSetConfigResponse_Response() {} + +func (*ValidateAndSetConfigResponse_ValidationError) isValidateAndSetConfigResponse_Response() {} + +var File_config_proto protoreflect.FileDescriptor + +const file_config_proto_rawDesc = "" + + "\n" + + "\fconfig.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\" \n" + + "\x06Config\x12\x16\n" + + "\x06config\x18\x01 \x01(\tR\x06config\"8\n" + + "\x14DefaultConfigRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\"`\n" + + "\x1bValidateAndSetConfigRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1f\n" + + "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"\x93\x01\n" + + "\x1cValidateAndSetConfigResponse\x122\n" + + "\asuccess\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\asuccess\x123\n" + + "\x10validation_error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x0fvalidationErrorB\n" + + "\n" + + "\bresponse2\x92\x01\n" + + "\n" + + "Configurer\x12/\n" + + "\rDefaultConfig\x12\x15.DefaultConfigRequest\x1a\a.Config\x12S\n" + + "\x14ValidateAndSetConfig\x12\x1c.ValidateAndSetConfigRequest\x1a\x1d.ValidateAndSetConfigResponseB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData []byte +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_config_proto_goTypes = []any{ + (*Config)(nil), // 0: Config + (*DefaultConfigRequest)(nil), // 1: DefaultConfigRequest + (*ValidateAndSetConfigRequest)(nil), // 2: ValidateAndSetConfigRequest + (*ValidateAndSetConfigResponse)(nil), // 3: ValidateAndSetConfigResponse + (*UserContext)(nil), // 4: UserContext + (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*Error)(nil), // 6: Error +} +var file_config_proto_depIdxs = []int32{ + 4, // 0: DefaultConfigRequest.user:type_name -> UserContext + 4, // 1: ValidateAndSetConfigRequest.user:type_name -> UserContext + 0, // 2: ValidateAndSetConfigRequest.config:type_name -> Config + 5, // 3: ValidateAndSetConfigResponse.success:type_name -> google.protobuf.Empty + 6, // 4: ValidateAndSetConfigResponse.validation_error:type_name -> Error + 1, // 5: Configurer.DefaultConfig:input_type -> DefaultConfigRequest + 2, // 6: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest + 0, // 7: Configurer.DefaultConfig:output_type -> Config + 3, // 8: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + file_meta_proto_init() + file_config_proto_msgTypes[3].OneofWrappers = []any{ + (*ValidateAndSetConfigResponse_Success)(nil), + (*ValidateAndSetConfigResponse_ValidationError)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/config_grpc.pb.go b/v2/generated/protobuf/config_grpc.pb.go new file mode 100644 index 0000000..b8f7af1 --- /dev/null +++ b/v2/generated/protobuf/config_grpc.pb.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.32.0 +// source: config.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Configurer_DefaultConfig_FullMethodName = "/Configurer/DefaultConfig" + Configurer_ValidateAndSetConfig_FullMethodName = "/Configurer/ValidateAndSetConfig" +) + +// ConfigurerClient is the client API for Configurer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// A service that allows plugins to be configured through the Gotify server. +type ConfigurerClient interface { + DefaultConfig(ctx context.Context, in *DefaultConfigRequest, opts ...grpc.CallOption) (*Config, error) + ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) +} + +type configurerClient struct { + cc grpc.ClientConnInterface +} + +func NewConfigurerClient(cc grpc.ClientConnInterface) ConfigurerClient { + return &configurerClient{cc} +} + +func (c *configurerClient) DefaultConfig(ctx context.Context, in *DefaultConfigRequest, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Config) + err := c.cc.Invoke(ctx, Configurer_DefaultConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *configurerClient) ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateAndSetConfigResponse) + err := c.cc.Invoke(ctx, Configurer_ValidateAndSetConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ConfigurerServer is the server API for Configurer service. +// All implementations must embed UnimplementedConfigurerServer +// for forward compatibility. +// +// A service that allows plugins to be configured through the Gotify server. +type ConfigurerServer interface { + DefaultConfig(context.Context, *DefaultConfigRequest) (*Config, error) + ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) + mustEmbedUnimplementedConfigurerServer() +} + +// UnimplementedConfigurerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedConfigurerServer struct{} + +func (UnimplementedConfigurerServer) DefaultConfig(context.Context, *DefaultConfigRequest) (*Config, error) { + return nil, status.Errorf(codes.Unimplemented, "method DefaultConfig not implemented") +} +func (UnimplementedConfigurerServer) ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateAndSetConfig not implemented") +} +func (UnimplementedConfigurerServer) mustEmbedUnimplementedConfigurerServer() {} +func (UnimplementedConfigurerServer) testEmbeddedByValue() {} + +// UnsafeConfigurerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ConfigurerServer will +// result in compilation errors. +type UnsafeConfigurerServer interface { + mustEmbedUnimplementedConfigurerServer() +} + +func RegisterConfigurerServer(s grpc.ServiceRegistrar, srv ConfigurerServer) { + // If the following call pancis, it indicates UnimplementedConfigurerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Configurer_ServiceDesc, srv) +} + +func _Configurer_DefaultConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DefaultConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConfigurerServer).DefaultConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Configurer_DefaultConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConfigurerServer).DefaultConfig(ctx, req.(*DefaultConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Configurer_ValidateAndSetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateAndSetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConfigurerServer).ValidateAndSetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Configurer_ValidateAndSetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConfigurerServer).ValidateAndSetConfig(ctx, req.(*ValidateAndSetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Configurer_ServiceDesc is the grpc.ServiceDesc for Configurer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Configurer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Configurer", + HandlerType: (*ConfigurerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DefaultConfig", + Handler: _Configurer_DefaultConfig_Handler, + }, + { + MethodName: "ValidateAndSetConfig", + Handler: _Configurer_ValidateAndSetConfig_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "config.proto", +} diff --git a/v2/generated/protobuf/display.pb.go b/v2/generated/protobuf/display.pb.go new file mode 100644 index 0000000..6348688 --- /dev/null +++ b/v2/generated/protobuf/display.pb.go @@ -0,0 +1,216 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.32.0 +// source: display.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DisplayRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the display belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // The base URL of the plugin control panel. + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplayRequest) Reset() { + *x = DisplayRequest{} + mi := &file_display_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplayRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplayRequest) ProtoMessage() {} + +func (x *DisplayRequest) ProtoReflect() protoreflect.Message { + mi := &file_display_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplayRequest.ProtoReflect.Descriptor instead. +func (*DisplayRequest) Descriptor() ([]byte, []int) { + return file_display_proto_rawDescGZIP(), []int{0} +} + +func (x *DisplayRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + +func (x *DisplayRequest) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +type DisplayResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Response: + // + // *DisplayResponse_Markdown + Response isDisplayResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplayResponse) Reset() { + *x = DisplayResponse{} + mi := &file_display_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplayResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplayResponse) ProtoMessage() {} + +func (x *DisplayResponse) ProtoReflect() protoreflect.Message { + mi := &file_display_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplayResponse.ProtoReflect.Descriptor instead. +func (*DisplayResponse) Descriptor() ([]byte, []int) { + return file_display_proto_rawDescGZIP(), []int{1} +} + +func (x *DisplayResponse) GetResponse() isDisplayResponse_Response { + if x != nil { + return x.Response + } + return nil +} + +func (x *DisplayResponse) GetMarkdown() string { + if x != nil { + if x, ok := x.Response.(*DisplayResponse_Markdown); ok { + return x.Markdown + } + } + return "" +} + +type isDisplayResponse_Response interface { + isDisplayResponse_Response() +} + +type DisplayResponse_Markdown struct { + // The display response in markdown format. + Markdown string `protobuf:"bytes,1,opt,name=markdown,proto3,oneof"` +} + +func (*DisplayResponse_Markdown) isDisplayResponse_Response() {} + +var File_display_proto protoreflect.FileDescriptor + +const file_display_proto_rawDesc = "" + + "\n" + + "\rdisplay.proto\x1a\n" + + "meta.proto\"N\n" + + "\x0eDisplayRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1a\n" + + "\blocation\x18\x02 \x01(\tR\blocation\";\n" + + "\x0fDisplayResponse\x12\x1c\n" + + "\bmarkdown\x18\x01 \x01(\tH\x00R\bmarkdownB\n" + + "\n" + + "\bresponse29\n" + + "\tDisplayer\x12,\n" + + "\aDisplay\x12\x0f.DisplayRequest\x1a\x10.DisplayResponseB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_display_proto_rawDescOnce sync.Once + file_display_proto_rawDescData []byte +) + +func file_display_proto_rawDescGZIP() []byte { + file_display_proto_rawDescOnce.Do(func() { + file_display_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_display_proto_rawDesc), len(file_display_proto_rawDesc))) + }) + return file_display_proto_rawDescData +} + +var file_display_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_display_proto_goTypes = []any{ + (*DisplayRequest)(nil), // 0: DisplayRequest + (*DisplayResponse)(nil), // 1: DisplayResponse + (*UserContext)(nil), // 2: UserContext +} +var file_display_proto_depIdxs = []int32{ + 2, // 0: DisplayRequest.user:type_name -> UserContext + 0, // 1: Displayer.Display:input_type -> DisplayRequest + 1, // 2: Displayer.Display:output_type -> DisplayResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_display_proto_init() } +func file_display_proto_init() { + if File_display_proto != nil { + return + } + file_meta_proto_init() + file_display_proto_msgTypes[1].OneofWrappers = []any{ + (*DisplayResponse_Markdown)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_display_proto_rawDesc), len(file_display_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_display_proto_goTypes, + DependencyIndexes: file_display_proto_depIdxs, + MessageInfos: file_display_proto_msgTypes, + }.Build() + File_display_proto = out.File + file_display_proto_goTypes = nil + file_display_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/display_grpc.pb.go b/v2/generated/protobuf/display_grpc.pb.go new file mode 100644 index 0000000..f13dc36 --- /dev/null +++ b/v2/generated/protobuf/display_grpc.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.32.0 +// source: display.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Displayer_Display_FullMethodName = "/Displayer/Display" +) + +// DisplayerClient is the client API for Displayer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// A service that allows plugins to display content to the user. +type DisplayerClient interface { + Display(ctx context.Context, in *DisplayRequest, opts ...grpc.CallOption) (*DisplayResponse, error) +} + +type displayerClient struct { + cc grpc.ClientConnInterface +} + +func NewDisplayerClient(cc grpc.ClientConnInterface) DisplayerClient { + return &displayerClient{cc} +} + +func (c *displayerClient) Display(ctx context.Context, in *DisplayRequest, opts ...grpc.CallOption) (*DisplayResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DisplayResponse) + err := c.cc.Invoke(ctx, Displayer_Display_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DisplayerServer is the server API for Displayer service. +// All implementations must embed UnimplementedDisplayerServer +// for forward compatibility. +// +// A service that allows plugins to display content to the user. +type DisplayerServer interface { + Display(context.Context, *DisplayRequest) (*DisplayResponse, error) + mustEmbedUnimplementedDisplayerServer() +} + +// UnimplementedDisplayerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDisplayerServer struct{} + +func (UnimplementedDisplayerServer) Display(context.Context, *DisplayRequest) (*DisplayResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Display not implemented") +} +func (UnimplementedDisplayerServer) mustEmbedUnimplementedDisplayerServer() {} +func (UnimplementedDisplayerServer) testEmbeddedByValue() {} + +// UnsafeDisplayerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DisplayerServer will +// result in compilation errors. +type UnsafeDisplayerServer interface { + mustEmbedUnimplementedDisplayerServer() +} + +func RegisterDisplayerServer(s grpc.ServiceRegistrar, srv DisplayerServer) { + // If the following call pancis, it indicates UnimplementedDisplayerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Displayer_ServiceDesc, srv) +} + +func _Displayer_Display_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisplayRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DisplayerServer).Display(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Displayer_Display_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DisplayerServer).Display(ctx, req.(*DisplayRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Displayer_ServiceDesc is the grpc.ServiceDesc for Displayer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Displayer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Displayer", + HandlerType: (*DisplayerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Display", + Handler: _Displayer_Display_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "display.proto", +} diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go new file mode 100644 index 0000000..d263d5e --- /dev/null +++ b/v2/generated/protobuf/meta.pb.go @@ -0,0 +1,933 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.32.0 +// source: meta.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Capability int32 + +const ( + Capability_DISPLAYER Capability = 0 + Capability_MESSENGER Capability = 1 + Capability_CONFIGURER Capability = 2 + Capability_STORAGER Capability = 3 + Capability_WEBHOOKER Capability = 4 +) + +// Enum value maps for Capability. +var ( + Capability_name = map[int32]string{ + 0: "DISPLAYER", + 1: "MESSENGER", + 2: "CONFIGURER", + 3: "STORAGER", + 4: "WEBHOOKER", + } + Capability_value = map[string]int32{ + "DISPLAYER": 0, + "MESSENGER": 1, + "CONFIGURER": 2, + "STORAGER": 3, + "WEBHOOKER": 4, + } +) + +func (x Capability) Enum() *Capability { + p := new(Capability) + *p = x + return p +} + +func (x Capability) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Capability) Descriptor() protoreflect.EnumDescriptor { + return file_meta_proto_enumTypes[0].Descriptor() +} + +func (Capability) Type() protoreflect.EnumType { + return &file_meta_proto_enumTypes[0] +} + +func (x Capability) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Capability.Descriptor instead. +func (Capability) EnumDescriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{0} +} + +type Error struct { + state protoimpl.MessageState `protogen:"open.v1"` + Details *anypb.Any `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Error) Reset() { + *x = Error{} + mi := &file_meta_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{0} +} + +func (x *Error) GetDetails() *anypb.Any { + if x != nil { + return x.Details + } + return nil +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Error) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type UserContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Admin bool `protobuf:"varint,3,opt,name=admin,proto3" json:"admin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserContext) Reset() { + *x = UserContext{} + mi := &file_meta_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserContext) ProtoMessage() {} + +func (x *UserContext) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserContext.ProtoReflect.Descriptor instead. +func (*UserContext) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{1} +} + +func (x *UserContext) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UserContext) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserContext) GetAdmin() bool { + if x != nil { + return x.Admin + } + return false +} + +type Capabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + Displayer *uint32 `protobuf:"varint,1,opt,name=Displayer,proto3,oneof" json:"Displayer,omitempty"` + Configurer *uint32 `protobuf:"varint,2,opt,name=Configurer,proto3,oneof" json:"Configurer,omitempty"` + Webhooker *uint32 `protobuf:"varint,3,opt,name=Webhooker,proto3,oneof" json:"Webhooker,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Capabilities) Reset() { + *x = Capabilities{} + mi := &file_meta_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Capabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Capabilities) ProtoMessage() {} + +func (x *Capabilities) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. +func (*Capabilities) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{2} +} + +func (x *Capabilities) GetDisplayer() uint32 { + if x != nil && x.Displayer != nil { + return *x.Displayer + } + return 0 +} + +func (x *Capabilities) GetConfigurer() uint32 { + if x != nil && x.Configurer != nil { + return *x.Configurer + } + return 0 +} + +func (x *Capabilities) GetWebhooker() uint32 { + if x != nil && x.Webhooker != nil { + return *x.Webhooker + } + return 0 +} + +type Info struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Author string `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Website string `protobuf:"bytes,4,opt,name=website,proto3" json:"website,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + License string `protobuf:"bytes,6,opt,name=license,proto3" json:"license,omitempty"` + ModulePath string `protobuf:"bytes,7,opt,name=module_path,json=modulePath,proto3" json:"module_path,omitempty"` + Capabilities *Capabilities `protobuf:"bytes,8,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Info) Reset() { + *x = Info{} + mi := &file_meta_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Info) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Info) ProtoMessage() {} + +func (x *Info) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Info.ProtoReflect.Descriptor instead. +func (*Info) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{3} +} + +func (x *Info) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Info) GetAuthor() string { + if x != nil { + return x.Author + } + return "" +} + +func (x *Info) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Info) GetWebsite() string { + if x != nil { + return x.Website + } + return "" +} + +func (x *Info) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Info) GetLicense() string { + if x != nil { + return x.License + } + return "" +} + +func (x *Info) GetModulePath() string { + if x != nil { + return x.ModulePath + } + return "" +} + +func (x *Info) GetCapabilities() *Capabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +type ExtrasValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *ExtrasValue_Json + Value isExtrasValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExtrasValue) Reset() { + *x = ExtrasValue{} + mi := &file_meta_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExtrasValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExtrasValue) ProtoMessage() {} + +func (x *ExtrasValue) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExtrasValue.ProtoReflect.Descriptor instead. +func (*ExtrasValue) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{4} +} + +func (x *ExtrasValue) GetValue() isExtrasValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *ExtrasValue) GetJson() string { + if x != nil { + if x, ok := x.Value.(*ExtrasValue_Json); ok { + return x.Json + } + } + return "" +} + +type isExtrasValue_Value interface { + isExtrasValue_Value() +} + +type ExtrasValue_Json struct { + Json string `protobuf:"bytes,1,opt,name=json,proto3,oneof"` +} + +func (*ExtrasValue_Json) isExtrasValue_Value() {} + +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Priority int32 `protobuf:"varint,3,opt,name=priority,proto3" json:"priority,omitempty"` + Extras map[string]*ExtrasValue `protobuf:"bytes,4,rep,name=extras,proto3" json:"extras,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_meta_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{5} +} + +func (x *Message) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Message) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Message) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *Message) GetExtras() map[string]*ExtrasValue { + if x != nil { + return x.Extras + } + return nil +} + +type InstanceUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Update: + // + // *InstanceUpdate_Ping + // *InstanceUpdate_Capable + // *InstanceUpdate_Message + // *InstanceUpdate_Storage + Update isInstanceUpdate_Update `protobuf_oneof:"update"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceUpdate) Reset() { + *x = InstanceUpdate{} + mi := &file_meta_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceUpdate) ProtoMessage() {} + +func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstanceUpdate.ProtoReflect.Descriptor instead. +func (*InstanceUpdate) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{6} +} + +func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { + if x != nil { + return x.Update + } + return nil +} + +func (x *InstanceUpdate) GetPing() *emptypb.Empty { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Ping); ok { + return x.Ping + } + } + return nil +} + +func (x *InstanceUpdate) GetCapable() Capability { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Capable); ok { + return x.Capable + } + } + return Capability_DISPLAYER +} + +func (x *InstanceUpdate) GetMessage() *Message { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Message); ok { + return x.Message + } + } + return nil +} + +func (x *InstanceUpdate) GetStorage() []byte { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Storage); ok { + return x.Storage + } + } + return nil +} + +type isInstanceUpdate_Update interface { + isInstanceUpdate_Update() +} + +type InstanceUpdate_Ping struct { + // ping the server to keep the connection alive + Ping *emptypb.Empty `protobuf:"bytes,1,opt,name=ping,proto3,oneof"` +} + +type InstanceUpdate_Capable struct { + // enable support for a feature, must be one of the capabilities supported by the server + Capable Capability `protobuf:"varint,2,opt,name=capable,proto3,enum=Capability,oneof"` +} + +type InstanceUpdate_Message struct { + // send a message to the user + Message *Message `protobuf:"bytes,3,opt,name=message,proto3,oneof"` +} + +type InstanceUpdate_Storage struct { + // update persistent storage + Storage []byte `protobuf:"bytes,4,opt,name=storage,proto3,oneof"` +} + +func (*InstanceUpdate_Ping) isInstanceUpdate_Update() {} + +func (*InstanceUpdate_Capable) isInstanceUpdate_Update() {} + +func (*InstanceUpdate_Message) isInstanceUpdate_Update() {} + +func (*InstanceUpdate_Storage) isInstanceUpdate_Update() {} + +type UserInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the server info + ServerInfo *ServerInfo `protobuf:"bytes,1,opt,name=serverInfo,proto3" json:"serverInfo,omitempty"` + // the user context + User *UserContext `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` + // the webhook base path + WebhookBasePath *string `protobuf:"bytes,3,opt,name=webhookBasePath,proto3,oneof" json:"webhookBasePath,omitempty"` + // the config + Config []byte `protobuf:"bytes,4,opt,name=config,proto3,oneof" json:"config,omitempty"` + // the storage + Storage []byte `protobuf:"bytes,5,opt,name=storage,proto3,oneof" json:"storage,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserInstanceRequest) Reset() { + *x = UserInstanceRequest{} + mi := &file_meta_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserInstanceRequest) ProtoMessage() {} + +func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserInstanceRequest.ProtoReflect.Descriptor instead. +func (*UserInstanceRequest) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{7} +} + +func (x *UserInstanceRequest) GetServerInfo() *ServerInfo { + if x != nil { + return x.ServerInfo + } + return nil +} + +func (x *UserInstanceRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + +func (x *UserInstanceRequest) GetWebhookBasePath() string { + if x != nil && x.WebhookBasePath != nil { + return *x.WebhookBasePath + } + return "" +} + +func (x *UserInstanceRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +func (x *UserInstanceRequest) GetStorage() []byte { + if x != nil { + return x.Storage + } + return nil +} + +type ServerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` + // supported capabilities of the gotify server itself + Capabilities []Capability `protobuf:"varint,4,rep,packed,name=capabilities,proto3,enum=Capability" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerInfo) Reset() { + *x = ServerInfo{} + mi := &file_meta_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerInfo) ProtoMessage() {} + +func (x *ServerInfo) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerInfo.ProtoReflect.Descriptor instead. +func (*ServerInfo) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{8} +} + +func (x *ServerInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ServerInfo) GetCommit() string { + if x != nil { + return x.Commit + } + return "" +} + +func (x *ServerInfo) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + +func (x *ServerInfo) GetCapabilities() []Capability { + if x != nil { + return x.Capabilities + } + return nil +} + +var File_meta_proto protoreflect.FileDescriptor + +const file_meta_proto_rawDesc = "" + + "\n" + + "\n" + + "meta.proto\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\"e\n" + + "\x05Error\x12.\n" + + "\adetails\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\adetails\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"G\n" + + "\vUserContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + + "\x05admin\x18\x03 \x01(\bR\x05admin\"\xa4\x01\n" + + "\fCapabilities\x12!\n" + + "\tDisplayer\x18\x01 \x01(\rH\x00R\tDisplayer\x88\x01\x01\x12#\n" + + "\n" + + "Configurer\x18\x02 \x01(\rH\x01R\n" + + "Configurer\x88\x01\x01\x12!\n" + + "\tWebhooker\x18\x03 \x01(\rH\x02R\tWebhooker\x88\x01\x01B\f\n" + + "\n" + + "_DisplayerB\r\n" + + "\v_ConfigurerB\f\n" + + "\n" + + "_Webhooker\"\xf6\x01\n" + + "\x04Info\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x18\n" + + "\awebsite\x18\x04 \x01(\tR\awebsite\x12 \n" + + "\vdescription\x18\x05 \x01(\tR\vdescription\x12\x18\n" + + "\alicense\x18\x06 \x01(\tR\alicense\x12\x1f\n" + + "\vmodule_path\x18\a \x01(\tR\n" + + "modulePath\x121\n" + + "\fcapabilities\x18\b \x01(\v2\r.CapabilitiesR\fcapabilities\",\n" + + "\vExtrasValue\x12\x14\n" + + "\x04json\x18\x01 \x01(\tH\x00R\x04jsonB\a\n" + + "\x05value\"\xcc\x01\n" + + "\aMessage\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1a\n" + + "\bpriority\x18\x03 \x01(\x05R\bpriority\x12,\n" + + "\x06extras\x18\x04 \x03(\v2\x14.Message.ExtrasEntryR\x06extras\x1aG\n" + + "\vExtrasEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\"\n" + + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"\xb3\x01\n" + + "\x0eInstanceUpdate\x12,\n" + + "\x04ping\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\x04ping\x12'\n" + + "\acapable\x18\x02 \x01(\x0e2\v.CapabilityH\x00R\acapable\x12$\n" + + "\amessage\x18\x03 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + + "\astorage\x18\x04 \x01(\fH\x00R\astorageB\b\n" + + "\x06update\"\xfa\x01\n" + + "\x13UserInstanceRequest\x12+\n" + + "\n" + + "serverInfo\x18\x01 \x01(\v2\v.ServerInfoR\n" + + "serverInfo\x12 \n" + + "\x04user\x18\x02 \x01(\v2\f.UserContextR\x04user\x12-\n" + + "\x0fwebhookBasePath\x18\x03 \x01(\tH\x00R\x0fwebhookBasePath\x88\x01\x01\x12\x1b\n" + + "\x06config\x18\x04 \x01(\fH\x01R\x06config\x88\x01\x01\x12\x1d\n" + + "\astorage\x18\x05 \x01(\fH\x02R\astorage\x88\x01\x01B\x12\n" + + "\x10_webhookBasePathB\t\n" + + "\a_configB\n" + + "\n" + + "\b_storage\"\x8d\x01\n" + + "\n" + + "ServerInfo\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\x12/\n" + + "\fcapabilities\x18\x04 \x03(\x0e2\v.CapabilityR\fcapabilities*W\n" + + "\n" + + "Capability\x12\r\n" + + "\tDISPLAYER\x10\x00\x12\r\n" + + "\tMESSENGER\x10\x01\x12\x0e\n" + + "\n" + + "CONFIGURER\x10\x02\x12\f\n" + + "\bSTORAGER\x10\x03\x12\r\n" + + "\tWEBHOOKER\x10\x042\xb8\x01\n" + + "\x06Plugin\x12.\n" + + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x12B\n" + + "\x10GracefulShutdown\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n" + + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\x0f.InstanceUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_meta_proto_rawDescOnce sync.Once + file_meta_proto_rawDescData []byte +) + +func file_meta_proto_rawDescGZIP() []byte { + file_meta_proto_rawDescOnce.Do(func() { + file_meta_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc))) + }) + return file_meta_proto_rawDescData +} + +var file_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_meta_proto_goTypes = []any{ + (Capability)(0), // 0: Capability + (*Error)(nil), // 1: Error + (*UserContext)(nil), // 2: UserContext + (*Capabilities)(nil), // 3: Capabilities + (*Info)(nil), // 4: Info + (*ExtrasValue)(nil), // 5: ExtrasValue + (*Message)(nil), // 6: Message + (*InstanceUpdate)(nil), // 7: InstanceUpdate + (*UserInstanceRequest)(nil), // 8: UserInstanceRequest + (*ServerInfo)(nil), // 9: ServerInfo + nil, // 10: Message.ExtrasEntry + (*anypb.Any)(nil), // 11: google.protobuf.Any + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty +} +var file_meta_proto_depIdxs = []int32{ + 11, // 0: Error.details:type_name -> google.protobuf.Any + 3, // 1: Info.capabilities:type_name -> Capabilities + 10, // 2: Message.extras:type_name -> Message.ExtrasEntry + 12, // 3: InstanceUpdate.ping:type_name -> google.protobuf.Empty + 0, // 4: InstanceUpdate.capable:type_name -> Capability + 6, // 5: InstanceUpdate.message:type_name -> Message + 9, // 6: UserInstanceRequest.serverInfo:type_name -> ServerInfo + 2, // 7: UserInstanceRequest.user:type_name -> UserContext + 0, // 8: ServerInfo.capabilities:type_name -> Capability + 5, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue + 12, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 12, // 11: Plugin.GracefulShutdown:input_type -> google.protobuf.Empty + 8, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 4, // 13: Plugin.GetPluginInfo:output_type -> Info + 12, // 14: Plugin.GracefulShutdown:output_type -> google.protobuf.Empty + 7, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate + 13, // [13:16] is the sub-list for method output_type + 10, // [10:13] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_meta_proto_init() } +func file_meta_proto_init() { + if File_meta_proto != nil { + return + } + file_meta_proto_msgTypes[2].OneofWrappers = []any{} + file_meta_proto_msgTypes[4].OneofWrappers = []any{ + (*ExtrasValue_Json)(nil), + } + file_meta_proto_msgTypes[6].OneofWrappers = []any{ + (*InstanceUpdate_Ping)(nil), + (*InstanceUpdate_Capable)(nil), + (*InstanceUpdate_Message)(nil), + (*InstanceUpdate_Storage)(nil), + } + file_meta_proto_msgTypes[7].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), + NumEnums: 1, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_meta_proto_goTypes, + DependencyIndexes: file_meta_proto_depIdxs, + EnumInfos: file_meta_proto_enumTypes, + MessageInfos: file_meta_proto_msgTypes, + }.Build() + File_meta_proto = out.File + file_meta_proto_goTypes = nil + file_meta_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go new file mode 100644 index 0000000..dec3a4e --- /dev/null +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -0,0 +1,214 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.32.0 +// source: meta.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" + Plugin_GracefulShutdown_FullMethodName = "/Plugin/GracefulShutdown" + Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" +) + +// PluginClient is the client API for Plugin service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. +type PluginClient interface { + // get the plugin info + GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) + // graceful shutdown + GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) + // run a user instance + RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) +} + +type pluginClient struct { + cc grpc.ClientConnInterface +} + +func NewPluginClient(cc grpc.ClientConnInterface) PluginClient { + return &pluginClient{cc} +} + +func (c *pluginClient) GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Info) + err := c.cc.Invoke(ctx, Plugin_GetPluginInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Plugin_GracefulShutdown_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_RunUserInstance_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[UserInstanceRequest, InstanceUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[InstanceUpdate] + +// PluginServer is the server API for Plugin service. +// All implementations must embed UnimplementedPluginServer +// for forward compatibility. +// +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. +type PluginServer interface { + // get the plugin info + GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) + // graceful shutdown + GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + // run a user instance + RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error + mustEmbedUnimplementedPluginServer() +} + +// UnimplementedPluginServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPluginServer struct{} + +func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPluginInfo not implemented") +} +func (UnimplementedPluginServer) GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method GracefulShutdown not implemented") +} +func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error { + return status.Errorf(codes.Unimplemented, "method RunUserInstance not implemented") +} +func (UnimplementedPluginServer) mustEmbedUnimplementedPluginServer() {} +func (UnimplementedPluginServer) testEmbeddedByValue() {} + +// UnsafePluginServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PluginServer will +// result in compilation errors. +type UnsafePluginServer interface { + mustEmbedUnimplementedPluginServer() +} + +func RegisterPluginServer(s grpc.ServiceRegistrar, srv PluginServer) { + // If the following call pancis, it indicates UnimplementedPluginServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Plugin_ServiceDesc, srv) +} + +func _Plugin_GetPluginInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).GetPluginInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_GetPluginInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).GetPluginInfo(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_GracefulShutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).GracefulShutdown(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_GracefulShutdown_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).GracefulShutdown(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_RunUserInstance_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(UserInstanceRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(PluginServer).RunUserInstance(m, &grpc.GenericServerStream[UserInstanceRequest, InstanceUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_RunUserInstanceServer = grpc.ServerStreamingServer[InstanceUpdate] + +// Plugin_ServiceDesc is the grpc.ServiceDesc for Plugin service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Plugin_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Plugin", + HandlerType: (*PluginServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetPluginInfo", + Handler: _Plugin_GetPluginInfo_Handler, + }, + { + MethodName: "GracefulShutdown", + Handler: _Plugin_GracefulShutdown_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "RunUserInstance", + Handler: _Plugin_RunUserInstance_Handler, + ServerStreams: true, + }, + }, + Metadata: "meta.proto", +} diff --git a/v2/generated/protobuf/server_events.pb.go b/v2/generated/protobuf/server_events.pb.go new file mode 100644 index 0000000..8fb6d39 --- /dev/null +++ b/v2/generated/protobuf/server_events.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.32.0 +// source: server_events.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ServerEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *ServerEvent_User + Event isServerEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerEvent) Reset() { + *x = ServerEvent{} + mi := &file_server_events_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEvent) ProtoMessage() {} + +func (x *ServerEvent) ProtoReflect() protoreflect.Message { + mi := &file_server_events_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEvent.ProtoReflect.Descriptor instead. +func (*ServerEvent) Descriptor() ([]byte, []int) { + return file_server_events_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerEvent) GetEvent() isServerEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *ServerEvent) GetUser() *UserContext { + if x != nil { + if x, ok := x.Event.(*ServerEvent_User); ok { + return x.User + } + } + return nil +} + +type isServerEvent_Event interface { + isServerEvent_Event() +} + +type ServerEvent_User struct { + // A user has been created or updated. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3,oneof"` +} + +func (*ServerEvent_User) isServerEvent_Event() {} + +var File_server_events_proto protoreflect.FileDescriptor + +const file_server_events_proto_rawDesc = "" + + "\n" + + "\x13server_events.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\":\n" + + "\vServerEvent\x12\"\n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextH\x00R\x04userB\a\n" + + "\x05event2M\n" + + "\x13ServerEventReceiver\x126\n" + + "\fServerEvents\x12\f.ServerEvent\x1a\x16.google.protobuf.Empty(\x01B\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_server_events_proto_rawDescOnce sync.Once + file_server_events_proto_rawDescData []byte +) + +func file_server_events_proto_rawDescGZIP() []byte { + file_server_events_proto_rawDescOnce.Do(func() { + file_server_events_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_server_events_proto_rawDesc), len(file_server_events_proto_rawDesc))) + }) + return file_server_events_proto_rawDescData +} + +var file_server_events_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_server_events_proto_goTypes = []any{ + (*ServerEvent)(nil), // 0: ServerEvent + (*UserContext)(nil), // 1: UserContext + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +} +var file_server_events_proto_depIdxs = []int32{ + 1, // 0: ServerEvent.user:type_name -> UserContext + 0, // 1: ServerEventReceiver.ServerEvents:input_type -> ServerEvent + 2, // 2: ServerEventReceiver.ServerEvents:output_type -> google.protobuf.Empty + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_server_events_proto_init() } +func file_server_events_proto_init() { + if File_server_events_proto != nil { + return + } + file_meta_proto_init() + file_server_events_proto_msgTypes[0].OneofWrappers = []any{ + (*ServerEvent_User)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_server_events_proto_rawDesc), len(file_server_events_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_server_events_proto_goTypes, + DependencyIndexes: file_server_events_proto_depIdxs, + MessageInfos: file_server_events_proto_msgTypes, + }.Build() + File_server_events_proto = out.File + file_server_events_proto_goTypes = nil + file_server_events_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/server_events_grpc.pb.go b/v2/generated/protobuf/server_events_grpc.pb.go new file mode 100644 index 0000000..3768f4a --- /dev/null +++ b/v2/generated/protobuf/server_events_grpc.pb.go @@ -0,0 +1,119 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.32.0 +// source: server_events.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ServerEventReceiver_ServerEvents_FullMethodName = "/ServerEventReceiver/ServerEvents" +) + +// ServerEventReceiverClient is the client API for ServerEventReceiver service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// An optional RPC service that allows plugins to accept updates from the server. +type ServerEventReceiverClient interface { + ServerEvents(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ServerEvent, emptypb.Empty], error) +} + +type serverEventReceiverClient struct { + cc grpc.ClientConnInterface +} + +func NewServerEventReceiverClient(cc grpc.ClientConnInterface) ServerEventReceiverClient { + return &serverEventReceiverClient{cc} +} + +func (c *serverEventReceiverClient) ServerEvents(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ServerEvent, emptypb.Empty], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ServerEventReceiver_ServiceDesc.Streams[0], ServerEventReceiver_ServerEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ServerEvent, emptypb.Empty]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ServerEventReceiver_ServerEventsClient = grpc.ClientStreamingClient[ServerEvent, emptypb.Empty] + +// ServerEventReceiverServer is the server API for ServerEventReceiver service. +// All implementations must embed UnimplementedServerEventReceiverServer +// for forward compatibility. +// +// An optional RPC service that allows plugins to accept updates from the server. +type ServerEventReceiverServer interface { + ServerEvents(grpc.ClientStreamingServer[ServerEvent, emptypb.Empty]) error + mustEmbedUnimplementedServerEventReceiverServer() +} + +// UnimplementedServerEventReceiverServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedServerEventReceiverServer struct{} + +func (UnimplementedServerEventReceiverServer) ServerEvents(grpc.ClientStreamingServer[ServerEvent, emptypb.Empty]) error { + return status.Errorf(codes.Unimplemented, "method ServerEvents not implemented") +} +func (UnimplementedServerEventReceiverServer) mustEmbedUnimplementedServerEventReceiverServer() {} +func (UnimplementedServerEventReceiverServer) testEmbeddedByValue() {} + +// UnsafeServerEventReceiverServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ServerEventReceiverServer will +// result in compilation errors. +type UnsafeServerEventReceiverServer interface { + mustEmbedUnimplementedServerEventReceiverServer() +} + +func RegisterServerEventReceiverServer(s grpc.ServiceRegistrar, srv ServerEventReceiverServer) { + // If the following call pancis, it indicates UnimplementedServerEventReceiverServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ServerEventReceiver_ServiceDesc, srv) +} + +func _ServerEventReceiver_ServerEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ServerEventReceiverServer).ServerEvents(&grpc.GenericServerStream[ServerEvent, emptypb.Empty]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ServerEventReceiver_ServerEventsServer = grpc.ClientStreamingServer[ServerEvent, emptypb.Empty] + +// ServerEventReceiver_ServiceDesc is the grpc.ServiceDesc for ServerEventReceiver service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ServerEventReceiver_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ServerEventReceiver", + HandlerType: (*ServerEventReceiverServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "ServerEvents", + Handler: _ServerEventReceiver_ServerEvents_Handler, + ClientStreams: true, + }, + }, + Metadata: "server_events.proto", +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..28a6d5c --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,30 @@ +module github.com/gotify/plugin-api/v2 + +go 1.24.5 + +require ( + github.com/gin-gonic/gin v1.3.0 + github.com/gotify/plugin-api v1.0.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/sys v0.33.0 + google.golang.org/grpc v1.74.2 + google.golang.org/protobuf v1.36.7 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/json-iterator/go v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..0b00e57 --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,73 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI= +github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto new file mode 100644 index 0000000..c179462 --- /dev/null +++ b/v2/protobuf/config.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message Config { + // The YAML configuration data. + string config = 1; +} + +message DefaultConfigRequest { + // The user context the configuration belongs to. + UserContext user = 1; +} + +message ValidateAndSetConfigRequest { + // The user context the configuration belongs to. + UserContext user = 1; + // The YAML configuration data. + Config config = 2; +} + +message ValidateAndSetConfigResponse { + // The response to the request. + oneof response { + // The success response. + google.protobuf.Empty success = 1; + // The validation error response. + Error validation_error = 2; + } +} + +// A service that allows plugins to be configured through the Gotify server. +service Configurer { + rpc DefaultConfig(DefaultConfigRequest) returns (Config); + rpc ValidateAndSetConfig(ValidateAndSetConfigRequest) returns (ValidateAndSetConfigResponse); +} \ No newline at end of file diff --git a/v2/protobuf/display.proto b/v2/protobuf/display.proto new file mode 100644 index 0000000..0954f8c --- /dev/null +++ b/v2/protobuf/display.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message DisplayRequest { + // The user context the display belongs to. + UserContext user = 1; + // The base URL of the plugin control panel. + string location = 2; +} + +message DisplayResponse { + oneof response { + // The display response in markdown format. + string markdown = 1; + } +} + +// A service that allows plugins to display content to the user. +service Displayer { + rpc Display(DisplayRequest) returns (DisplayResponse); +} + diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto new file mode 100644 index 0000000..cdd6479 --- /dev/null +++ b/v2/protobuf/meta.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "./generated/protobuf"; + +message Error { + google.protobuf.Any details = 1; + string message = 2; + string type = 3; +} + +message UserContext { + uint64 id = 1; + string name = 2; + bool admin = 3; +} + +message Capabilities { + optional uint32 Displayer = 1; + optional uint32 Configurer = 2; + optional uint32 Webhooker = 3; +} + +message Info { + string version = 1; + string author = 2; + string name = 3; + string website = 4; + string description = 5; + string license = 6; + string module_path = 7; + Capabilities capabilities = 8; +} + +message ExtrasValue { + oneof value { + string json = 1; + } +} + +message Message { + string message = 1; + string title = 2; + int32 priority = 3; + map extras = 4; +} + +enum Capability { + DISPLAYER = 0; + MESSENGER = 1; + CONFIGURER = 2; + STORAGER = 3; + WEBHOOKER = 4; +} + +message InstanceUpdate { + oneof update { + // ping the server to keep the connection alive + google.protobuf.Empty ping = 1; + // enable support for a feature, must be one of the capabilities supported by the server + Capability capable = 2; + // send a message to the user + Message message = 3; + // update persistent storage + bytes storage = 4; + } +} + +message UserInstanceRequest { + // the server info + ServerInfo serverInfo = 1; + // the user context + UserContext user = 2; + // the webhook base path + optional string webhookBasePath = 3; + // the config + optional bytes config = 4; + // the storage + optional bytes storage = 5; +} + +message ServerInfo { + string version = 1; + string commit = 2; + string buildDate = 3; + // supported capabilities of the gotify server itself + repeated Capability capabilities = 4; +} + +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. +service Plugin { + // get the plugin info + rpc GetPluginInfo(google.protobuf.Empty) returns (Info); + + // graceful shutdown + rpc GracefulShutdown(google.protobuf.Empty) returns (google.protobuf.Empty); + + // run a user instance + rpc RunUserInstance(UserInstanceRequest) returns (stream InstanceUpdate); +} + diff --git a/v2/protobuf/server_events.proto b/v2/protobuf/server_events.proto new file mode 100644 index 0000000..5656c48 --- /dev/null +++ b/v2/protobuf/server_events.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message ServerEvent { + oneof event { + // A user has been created or updated. + UserContext user = 1; + } +} + +// An optional RPC service that allows plugins to accept updates from the server. +service ServerEventReceiver { + rpc ServerEvents(stream ServerEvent) returns (google.protobuf.Empty); +} diff --git a/v2/shim_v1.go b/v2/shim_v1.go new file mode 100644 index 0000000..53dc22b --- /dev/null +++ b/v2/shim_v1.go @@ -0,0 +1,517 @@ +package plugin + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "log" + "math" + "net/http" + "net/url" + "plugin" + "reflect" + "slices" + "strings" + "sync" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" + "google.golang.org/protobuf/types/known/emptypb" + "gopkg.in/yaml.v3" + + "github.com/gin-gonic/gin" + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" +) + +type GrpcDialer interface { + Dial(ctx context.Context) (*grpc.ClientConn, error) +} + +var ( + httpTimeout = 10 * time.Second + pingRate = 4 * time.Second +) + +func init() { + if testing.Testing() { + httpTimeout = 100 * time.Millisecond + pingRate = 10 * time.Millisecond + } +} + +// CompatV1 is an API interface that is compatible with the V1 API. +type CompatV1 struct { + GetPluginInfo func() papiv1.Info + GetInstance func(user papiv1.UserContext) (papiv1.Plugin, error) +} + +// NewCompatV1FromPlugin creates a new CompatV1 from a native Go plugin. +func NewCompatV1FromPlugin(plugin *plugin.Plugin) (*CompatV1, error) { + getPluginInfo, err := plugin.Lookup("GetGotifyPluginInfo") + if err != nil { + return nil, err + } + getInstance, err := plugin.Lookup("NewGotifyPlugin") + if err != nil { + return nil, err + } + + getPluginInfoChecked, ok := getPluginInfo.(func() papiv1.Info) + if !ok { + return nil, errors.New("GetGotifyPluginInfo is not a function") + } + getInstanceCheckedWithErr, ok := getInstance.(func(user papiv1.UserContext) (papiv1.Plugin, error)) + if !ok { + if getInstanceCheckedWithoutErr, ok := getInstance.(func(user papiv1.UserContext) papiv1.Plugin); ok { + getInstanceCheckedWithErr = func(user papiv1.UserContext) (papiv1.Plugin, error) { + return getInstanceCheckedWithoutErr(user), nil + } + } else { + return nil, errors.New("NewGotifyPlugin is not a function") + } + } + + return &CompatV1{ + GetPluginInfo: getPluginInfoChecked, + GetInstance: getInstanceCheckedWithErr, + }, nil +} + +// CompatV1Shim is a shim that acts like a plugin server and delegates request to +// something that implements a V1-style API interface. +type CompatV1Shim struct { + shutdown chan struct{} + shutdownOnce *sync.Once + mu *sync.RWMutex + compatV1 *CompatV1 + gin *gin.Engine + instances map[uint64]papiv1.Plugin + pluginServer *grpc.Server + pluginInfo papiv1.Info + http.Server +} + +// NewCompatV1Rpc creates a new CompatV1Shim server. +func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { + pluginInfo := compatV1.GetPluginInfo() + + cli, err := ParsePluginCli(cliArgs) + if err != nil { + log.Fatalf("Failed to parse CLI flags: %v", err) + } + defer cli.Close() + + rootCAs := x509.NewCertPool() + certificateChain, err := cli.Kex(pluginInfo.ModulePath, rootCAs) + + tlsConfig := &tls.Config{ + Certificates: certificateChain, + RootCAs: rootCAs, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: rootCAs, + } + + rpcServer := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: httpTimeout, + PermitWithoutStream: true, + }), grpc.ConnectionTimeout(httpTimeout)) + if !cli.Debug { + gin.SetMode(gin.ReleaseMode) + } + + ginEngine := gin.Default() + + self := &CompatV1Shim{ + shutdown: make(chan struct{}), + shutdownOnce: &sync.Once{}, + mu: &sync.RWMutex{}, + instances: make(map[uint64]papiv1.Plugin), + compatV1: compatV1, + gin: ginEngine, + pluginServer: rpcServer, + pluginInfo: pluginInfo, + } + + selfServer := &compatV1ShimServer{ + shim: self, + } + + protobuf.RegisterPluginServer(rpcServer, selfServer) + protobuf.RegisterDisplayerServer(rpcServer, selfServer) + protobuf.RegisterConfigurerServer(rpcServer, selfServer) + + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) + self.Server = http.Server{ + Handler: self, + TLSConfig: tlsConfig, + Protocols: protocols, + ReadTimeout: httpTimeout, + ReadHeaderTimeout: httpTimeout, + WriteTimeout: httpTimeout, + } + + return self, nil +} + +func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil { + http.Error(w, "Must use TLS", http.StatusUpgradeRequired) + return + } + + pluginRpcHostName := transport.BuildPluginTLSName(transport.PurposePluginRPC, h.pluginInfo.ModulePath) + + if r.TLS.ServerName == pluginRpcHostName { + if r.ProtoMajor != 2 { + http.Error(w, "Must use HTTP/2", http.StatusHTTPVersionNotSupported) + return + } + if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { + http.Error(w, "Must use application/grpc content type", http.StatusUnsupportedMediaType) + return + } + h.pluginServer.ServeHTTP(w, r) + + return + } + + pluginWebhookHostName := transport.BuildPluginTLSName(transport.PurposePluginWebhook, h.pluginInfo.ModulePath) + if r.TLS.ServerName == pluginWebhookHostName { + h.gin.ServeHTTP(w, r) + return + } + + http.Error(w, "Virtual host not found", http.StatusNotFound) +} + +type shimV1MessageHandler struct { + stream *protobuf.Plugin_RunUserInstanceServer +} + +func (h *shimV1MessageHandler) SendMessage(msg papiv1.Message) error { + extras := make(map[string]*protobuf.ExtrasValue) + for k, v := range msg.Extras { + jsonValue, err := json.Marshal(v) + if err != nil { + return err + } + extras[k] = &protobuf.ExtrasValue{ + Value: &protobuf.ExtrasValue_Json{ + Json: string(jsonValue), + }, + } + } + return (*h.stream).Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Message{ + Message: &protobuf.Message{ + Message: msg.Message, + Title: msg.Title, + Priority: int32(msg.Priority), + Extras: extras, + }, + }, + }) +} + +type shimV1StorageHandler struct { + mutex *sync.RWMutex + currentStorage []byte + stream *protobuf.Plugin_RunUserInstanceServer +} + +func (h *shimV1StorageHandler) Save(b []byte) error { + h.mutex.Lock() + defer h.mutex.Unlock() + h.currentStorage = slices.Clone(b) + return (*h.stream).Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Storage{ + Storage: b, + }, + }) +} + +func (h *shimV1StorageHandler) Load() (b []byte, err error) { + h.mutex.RLock() + defer h.mutex.RUnlock() + b = slices.Clone(h.currentStorage) + return +} + +type compatV1ShimServer struct { + shim *CompatV1Shim + protobuf.UnimplementedPluginServer + protobuf.UnimplementedDisplayerServer + protobuf.UnimplementedConfigurerServer +} + +func (s *CompatV1Shim) getInstanceByUserId(userId uint64) (papiv1.Plugin, error) { + s.mu.RLock() + instance, ok := s.instances[userId] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + return instance, nil +} + +func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return &protobuf.Info{ + Version: s.shim.pluginInfo.Version, + Author: s.shim.pluginInfo.Author, + Name: s.shim.pluginInfo.Name, + Website: s.shim.pluginInfo.Website, + Description: s.shim.pluginInfo.Description, + License: s.shim.pluginInfo.License, + ModulePath: s.shim.pluginInfo.ModulePath, + }, nil +} + +func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err + } + if displayer, ok := instance.(papiv1.Displayer); ok { + location, err := url.Parse(req.Location) + if err != nil { + return nil, err + } + return &protobuf.DisplayResponse{ + Response: &protobuf.DisplayResponse_Markdown{ + Markdown: displayer.GetDisplay(location), + }, + }, nil + } + return nil, errors.New("instance does not implement displayer") +} + +func (s *compatV1ShimServer) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err + } + if configurer, ok := instance.(papiv1.Configurer); ok { + defaultConfig := configurer.DefaultConfig() + bytes, err := yaml.Marshal(defaultConfig) + if err != nil { + return nil, err + } + return &protobuf.Config{ + Config: string(bytes), + }, nil + } + return nil, errors.New("instance does not implement configurer") +} + +func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err + } + if configurer, ok := instance.(papiv1.Configurer); ok { + currentConfig := configurer.DefaultConfig() + if req.Config != nil { + if reflect.TypeOf(currentConfig).Kind() == reflect.Pointer { + yaml.Unmarshal([]byte(req.Config.Config), currentConfig) + } else { + yaml.Unmarshal([]byte(req.Config.Config), ¤tConfig) + } + } + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return &protobuf.ValidateAndSetConfigResponse{ + Response: &protobuf.ValidateAndSetConfigResponse_ValidationError{ + ValidationError: &protobuf.Error{ + Message: err.Error(), + }, + }, + }, nil + } + return &protobuf.ValidateAndSetConfigResponse{ + Response: &protobuf.ValidateAndSetConfigResponse_Success{ + Success: new(emptypb.Empty), + }, + }, nil + } + return nil, errors.New("instance does not implement configurer") +} + +func (s *compatV1ShimServer) GracefulShutdown(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + s.shim.shutdownOnce.Do(func() { + close(s.shim.shutdown) + }) + return &emptypb.Empty{}, nil +} + +func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { + if req.User.Id > math.MaxUint { + return errors.New("user id is too large") + } + + unlockOnce := new(sync.Once) + + s.shim.mu.Lock() + + defer unlockOnce.Do(func() { + s.shim.mu.Unlock() + }) + + instance, alreadyRunning := s.shim.instances[req.User.Id] + + if !alreadyRunning { + var err error + instance, err = s.shim.compatV1.GetInstance(papiv1.UserContext{ + ID: uint(req.User.Id), + Name: req.User.Name, + Admin: req.User.Admin, + }) + if err != nil { + return err + } + + // enable supported capabilities + if _, ok := instance.(papiv1.Displayer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_DISPLAYER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_DISPLAYER, + }, + }); err != nil { + return err + } + } else { + return errors.New("displayer not supported by server but V1 API does not support backwards compatibility") + } + } + if _, ok := instance.(papiv1.Messenger); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_MESSENGER, + }, + }); err != nil { + return err + } + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + } + } + if _, ok := instance.(papiv1.Configurer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_CONFIGURER, + }, + }); err != nil { + return err + } + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") + } + } + if _, ok := instance.(papiv1.Storager); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_STORAGER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_STORAGER, + }, + }); err != nil { + return err + } + } else { + return errors.New("storager not supported by server but V1 API does not support backwards compatibility") + } + } + if _, ok := instance.(papiv1.Webhooker); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_WEBHOOKER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_WEBHOOKER, + }, + }); err != nil { + return err + } + } else { + return errors.New("webhooker not supported by server but V1 API does not support backwards compatibility") + } + } + + if messenger, ok := instance.(papiv1.Messenger); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { + messenger.SetMessageHandler(&shimV1MessageHandler{ + stream: &stream, + }) + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + } + } + + if configurer, ok := instance.(papiv1.Configurer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { + currentConfig := configurer.DefaultConfig() + if req.Config != nil { + if err := yaml.Unmarshal(req.Config, ¤tConfig); err != nil { + return err + } + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return err + } + } + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") + } + } + + if storager, ok := instance.(papiv1.Storager); ok { + storageHandler := &shimV1StorageHandler{ + mutex: &sync.RWMutex{}, + currentStorage: req.Storage, + stream: &stream, + } + storager.SetStorageHandler(storageHandler) + } + + if webhooker, ok := instance.(papiv1.Webhooker); ok { + if req.WebhookBasePath != nil { + group := s.shim.gin.Group(*req.WebhookBasePath) + webhooker.RegisterWebhook(*req.WebhookBasePath, group) + } + } + } + + if err := instance.Enable(); err != nil { + return err + } + + defer instance.Disable() + + s.shim.instances[req.User.Id] = instance + unlockOnce.Do(func() { + s.shim.mu.Unlock() + }) + + ticker := time.NewTicker(pingRate) + + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Ping{ + Ping: new(emptypb.Empty), + }, + }); err != nil { + return err + } + case <-s.shim.shutdown: + return nil + } + } +} diff --git a/v2/transport/anon_unix.go b/v2/transport/anon_unix.go new file mode 100644 index 0000000..ae80aa7 --- /dev/null +++ b/v2/transport/anon_unix.go @@ -0,0 +1,21 @@ +//go:build unix + +package transport + +import ( + "golang.org/x/sys/unix" +) + +func NewAnonPipe(rx *uintptr, tx *uintptr, cloexec bool) error { + var tmp [2]int + var flags int + if cloexec { + flags = unix.O_CLOEXEC + } + if err := unix.Pipe2(tmp[:], flags); err != nil { + return err + } + *rx = uintptr(tmp[0]) + *tx = uintptr(tmp[1]) + return nil +} diff --git a/v2/transport/anon_windows.go b/v2/transport/anon_windows.go new file mode 100644 index 0000000..1431910 --- /dev/null +++ b/v2/transport/anon_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package transport + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func NewAnonPipe(rx *uintptr, tx *uintptr, cloexec bool) error { + var tmp [2]windows.Handle + var sa windows.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + if !cloexec { + sa.InheritHandle = 1 + } + err := windows.CreatePipe(&tmp[0], &tmp[1], &sa, 0) + if err != nil { + return err + } + *rx = uintptr(tmp[0]) + *tx = uintptr(tmp[1]) + return nil +} diff --git a/v2/transport/pem_file.go b/v2/transport/pem_file.go new file mode 100644 index 0000000..ef88f5d --- /dev/null +++ b/v2/transport/pem_file.go @@ -0,0 +1,32 @@ +package transport + +import ( + "encoding/pem" + "io" +) + +func IteratePEMFile(r io.Reader, callback func(block *pem.Block) (continueIterate bool, err error)) error { + var bufferBytes []byte + for { + var buf [2048]byte + n, err := r.Read(buf[:]) + if err != nil { + if err == io.EOF { + break + } + return err + } + bufferBytes = append(bufferBytes, buf[:n]...) + + for block, rest := pem.Decode(bufferBytes); block != nil; block, rest = pem.Decode(rest) { + continueIterate, err := callback(block) + if err != nil { + return err + } + if !continueIterate { + return nil + } + } + } + return nil +} diff --git a/v2/transport/pipe_net.go b/v2/transport/pipe_net.go new file mode 100644 index 0000000..331a3f3 --- /dev/null +++ b/v2/transport/pipe_net.go @@ -0,0 +1,34 @@ +package transport + +import ( + "context" + "crypto/tls" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func newTCPListener() (net.Listener, error) { + listener, err := net.Listen("tcp", "[::1]:0") + if err != nil { + return nil, err + } + return listener, nil +} + +type GrpcPipeTLS struct { + address string + tlsConfig *tls.Config +} + +func NewGrpcPipeTLS(address string, config *tls.Config) *GrpcPipeTLS { + return &GrpcPipeTLS{ + address: address, + tlsConfig: config, + } +} + +func (p *GrpcPipeTLS) Dial(ctx context.Context) (*grpc.ClientConn, error) { + return grpc.NewClient(p.address, grpc.WithTransportCredentials(credentials.NewTLS(p.tlsConfig))) +} diff --git a/v2/transport/pipe_net_test.go b/v2/transport/pipe_net_test.go new file mode 100644 index 0000000..425dbbd --- /dev/null +++ b/v2/transport/pipe_net_test.go @@ -0,0 +1,100 @@ +package transport + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "net" + "testing" + "time" + + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +type dummyInfraServer struct { + protobuf.UnimplementedPluginServer +} + +func (s *dummyInfraServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return &protobuf.Info{ + Version: "test", + }, nil +} + +func (s *dummyInfraServer) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerInfo, error) { + return &protobuf.ServerInfo{ + Version: "test", + Commit: "test", + BuildDate: time.Now().Format(time.RFC3339), + }, nil +} + +func TestGrpcPipeNet(t *testing.T) { + serverPub, serverPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + tlsClient, err := NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + listener, err := newTCPListener() + if err != nil { + t.Fatal(err) + } + defer listener.Close() + serverCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: BuildPluginTLSName(PurposePluginRPC, "test"), + }, + DNSNames: []string{ + BuildPluginTLSName(PurposePluginRPC, "test"), + }, + PublicKey: serverPub, + }, serverPriv) + serverCsr, err := x509.ParseCertificateRequest(serverCsrBytes) + if err != nil { + t.Fatal(err) + } + serverCertBytes, err := tlsClient.SignPluginCSR("test", serverCsr) + if err != nil { + t.Fatal(err) + } + clientPipe := NewGrpcPipeTLS(fmt.Sprintf("[::1]:%d", listener.Addr().(*net.TCPAddr).Port), tlsClient.ClientTLSConfig("test")) + + serverTLSConfig := tlsClient.ServerTLSConfig() + serverTLSConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{serverCertBytes}, + PrivateKey: serverPriv, + }, + } + + server := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLSConfig))) + protobuf.RegisterPluginServer(server, &dummyInfraServer{}) + go server.Serve(listener) + defer server.GracefulStop() + + conn, err := clientPipe.Dial(context.Background()) + if err != nil { + t.Fatal(err) + } + + infraClient := protobuf.NewPluginClient(conn) + version, err := infraClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + if version.Version != "test" { + t.Fatal("expected test, got ", version.Version) + } + + defer conn.Close() +} diff --git a/v2/transport/pipe_not_unix.go b/v2/transport/pipe_not_unix.go new file mode 100644 index 0000000..88693f6 --- /dev/null +++ b/v2/transport/pipe_not_unix.go @@ -0,0 +1,11 @@ +//go:build !unix + +package transport + +import ( + "net" +) + +func NewListener() (net.Listener, string, error) { + return NewTCPListener() +} diff --git a/v2/transport/pipe_tcp.go b/v2/transport/pipe_tcp.go new file mode 100644 index 0000000..68552db --- /dev/null +++ b/v2/transport/pipe_tcp.go @@ -0,0 +1,14 @@ +package transport + +import ( + "fmt" + "net" +) + +func NewTCPListener() (net.Listener, string, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, "", err + } + return listener, fmt.Sprintf("dns:///%s", listener.Addr().String()), nil +} diff --git a/v2/transport/pipe_test.go b/v2/transport/pipe_test.go new file mode 100644 index 0000000..100f510 --- /dev/null +++ b/v2/transport/pipe_test.go @@ -0,0 +1,59 @@ +package transport + +import ( + "log" + "net" + "net/url" + "testing" +) + +func TestPipe(t *testing.T) { + listener, addrURL, err := NewListener() + if err != nil { + t.Fatalf("failed to create listener: %v", err) + } + defer listener.Close() + + urlParsed, err := url.Parse(addrURL) + if err != nil { + t.Fatalf("failed to parse address URL: %v", err) + } + + var family string + + switch urlParsed.Scheme { + case "unix": + family = "unix" + case "dns": + family = "tcp" + default: + t.Fatalf("unsupported address URL scheme: %s", urlParsed.Scheme) + } + + go func() { + conn, err := net.Dial(family, listener.Addr().String()) + if err != nil { + log.Panicf("failed to dial listener: %v", err) + } + if _, err := conn.Write([]byte("test")); err != nil { + log.Panicf("failed to write to listener: %v", err) + } + conn.Close() + }() + + accepted, err := listener.Accept() + if err != nil { + t.Fatalf("failed to accept listener: %v", err) + } + var buf [1024]byte + n, err := accepted.Read(buf[:]) + if err != nil { + t.Fatalf("failed to read from listener: %v", err) + } + if string(buf[:n]) != "test" { + t.Fatalf("expected test, got %s", string(buf[:n])) + } + accepted.Write([]byte("test")) + accepted.Close() + +} diff --git a/v2/transport/pipe_unix.go b/v2/transport/pipe_unix.go new file mode 100644 index 0000000..d674250 --- /dev/null +++ b/v2/transport/pipe_unix.go @@ -0,0 +1,26 @@ +//go:build unix + +package transport + +import ( + "fmt" + "net" + "os" + "path/filepath" +) + +func NewListener() (net.Listener, string, error) { + tmpDir, err := os.MkdirTemp("", "gotify-plugin-*") + if err != nil { + return nil, "", err + } + if err := os.Chmod(tmpDir, 0700); err != nil { + return nil, "", err + } + pipePath := filepath.Join(tmpDir, "plugin.sock") + listener, err := net.Listen("unix", pipePath) + if err != nil { + return nil, "", err + } + return listener, fmt.Sprintf("unix://%s", pipePath), nil +} diff --git a/v2/transport/transport_auth.go b/v2/transport/transport_auth.go new file mode 100644 index 0000000..2b78ee8 --- /dev/null +++ b/v2/transport/transport_auth.go @@ -0,0 +1,177 @@ +package transport + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" + "slices" + "strings" + "time" +) + +const ( + PurposePluginRPC = "rpc" + PurposePluginWebhook = "webhook" +) + +const ServerTLSName = "server.gotify.home.arpa" + +func BuildPluginTLSName(purpose string, moduleName string) string { + moduleNameParts := strings.Split(moduleName, "/") + for i := range moduleNameParts { + moduleNameParts[i] = hex.EncodeToString([]byte(moduleNameParts[i])) + } + slices.Reverse(moduleNameParts) + return fmt.Sprintf("%s.%s.plugin.gotify.home.arpa", purpose, strings.Join(moduleNameParts, ".")) +} + +type EphemeralTLSClient struct { + caCert *x509.Certificate + caPriv ed25519.PrivateKey +} + +func (s *EphemeralTLSClient) createCertPool() *x509.CertPool { + pool := x509.NewCertPool() + pool.AddCert(s.caCert) + return pool +} + +func (s *EphemeralTLSClient) ServerTLSConfig() *tls.Config { + return &tls.Config{ + ServerName: ServerTLSName, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: s.createCertPool(), + } +} + +func (s *EphemeralTLSClient) ClientTLSConfig(moduleName string) *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{s.caCert.Raw}, + PrivateKey: s.caPriv, + }, + }, + RootCAs: s.createCertPool(), + ServerName: BuildPluginTLSName(PurposePluginRPC, moduleName), + } +} + +func (s *EphemeralTLSClient) CACert() *x509.Certificate { + return s.caCert +} + +func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateRequest) ([]byte, error) { + if err := csr.CheckSignature(); err != nil { + return nil, err + } + certTemplate := &x509.Certificate{ + BasicConstraintsValid: true, + Subject: pkix.Name{ + CommonName: dnsName, + }, + DNSNames: []string{ + dnsName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + IsCA: false, + } + certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, s.caCert, csr.PublicKey, s.caPriv) + if err != nil { + return nil, err + } + return certBytes, nil +} + +func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { + return s.SignCSR(BuildPluginTLSName("*", moduleName), csr) +} + +func (s *EphemeralTLSClient) Kex(req io.Reader, resp io.Writer) error { + var csr *x509.CertificateRequest + + if err := IteratePEMFile(req, func(block *pem.Block) (continueIterate bool, err error) { + if block.Type == "CERTIFICATE REQUEST" { + csr, err = x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return false, err + } + + return false, nil + } + return true, nil + }); err != nil { + return err + } + + if csr == nil { + return errors.New("no certificate request found in kex request file") + } + + dnsName := csr.Subject.CommonName + certBytes, err := s.SignCSR(dnsName, csr) + if err != nil { + return err + } + _, err = resp.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + })) + if err != nil { + return err + } + _, err = resp.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: s.caCert.Raw, + })) + if err != nil { + return err + } + return nil +} + +func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { + caPub, caPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + caCertTemplate := &x509.Certificate{ + BasicConstraintsValid: true, + Subject: pkix.Name{ + CommonName: "gotify Plugin client CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + IsCA: true, + } + caCertBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caPub, caPriv) + if err != nil { + return nil, err + } + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + return nil, err + } + return &EphemeralTLSClient{ + caCert: caCert, + caPriv: caPriv, + }, nil +} diff --git a/v2/transport/transport_auth_test.go b/v2/transport/transport_auth_test.go new file mode 100644 index 0000000..0dd653e --- /dev/null +++ b/v2/transport/transport_auth_test.go @@ -0,0 +1,74 @@ +package transport + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "net" + "testing" + "time" +) + +func TestEphemeralTLSClient(t *testing.T) { + client, err := NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + pluginTlsName := BuildPluginTLSName(PurposePluginRPC, "test") + _, serverPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + serverCSRBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: pluginTlsName, + }, + DNSNames: []string{ + BuildPluginTLSName(PurposePluginRPC, "test"), + }, + }, serverPriv) + if err != nil { + t.Fatal(err) + } + serverCSR, err := x509.ParseCertificateRequest(serverCSRBytes) + if err != nil { + t.Fatal(err) + } + serverCert, err := client.SignPluginCSR("test", serverCSR) + if err != nil { + t.Fatal(err) + } + + s, c := net.Pipe() + defer s.Close() + defer c.Close() + go func() { + serverTLSConfig := client.ServerTLSConfig() + serverTLSConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{serverCert}, + PrivateKey: serverPriv, + }, + } + tlsServer := tls.Server(s, serverTLSConfig) + _, err = tlsServer.Write([]byte("hello")) + if err != nil { + panic(err) + } + }() + + tlsClient := tls.Client(c, client.ClientTLSConfig("test")) + tlsClient.SetDeadline(time.Now().Add(time.Second * 1)) + buf := make([]byte, 1024) + n, err := tlsClient.Read(buf) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != "hello" { + t.Fatal("expected hello, got ", string(buf[:n])) + } + +}