├── web ├── README.md └── src │ ├── global.d.ts │ ├── main.ts │ ├── routes.ts │ ├── components │ ├── Button.svelte │ └── Status.svelte │ ├── queries.ts │ ├── App.svelte │ └── pages │ ├── WebhookDetail.svelte │ └── WebhookList.svelte ├── app ├── db │ ├── mysql │ │ └── mysql.go │ ├── couchdb │ │ └── couchdb.go │ ├── elasticsearch │ │ ├── elasticsearch_test.go │ │ └── elasticsearch.go │ └── mongodb │ │ └── mongodb.go ├── server │ ├── grpc │ │ ├── README.md │ │ ├── transformer.go │ │ ├── health.go │ │ ├── interceptor.go │ │ ├── server.go │ │ ├── api │ │ │ └── service.proto │ │ └── webhook.go │ ├── rest │ │ ├── stat.go │ │ ├── dto │ │ │ ├── stat.go │ │ │ ├── types.go │ │ │ ├── dto.go │ │ │ └── webhook.go │ │ ├── health.go │ │ ├── README.md │ │ ├── server.go │ │ ├── transformer │ │ │ └── webhook.go │ │ ├── server_test.go │ │ └── webhook.go │ └── graphql │ │ ├── README.md │ │ ├── graph │ │ ├── resolver.go │ │ ├── scalar │ │ │ ├── filter.go │ │ │ ├── uint.go │ │ │ └── uint64.go │ │ ├── schema.graphqls │ │ ├── schema.resolvers.go │ │ └── model │ │ │ └── models_gen.go │ │ ├── Dockerfile │ │ ├── transformer │ │ └── webhook_connection.go │ │ ├── gqlgen.yml │ │ └── server.go ├── mq │ ├── redis │ │ ├── redis_test.go │ │ ├── consumer.go │ │ └── redis.go │ ├── nsq │ │ └── nsq.go │ └── nats │ │ ├── api.go │ │ └── nats.go ├── util │ ├── util.go │ └── util_test.go ├── shared │ ├── webhook_test.go │ ├── interface.go │ └── webhook.go ├── entity │ └── webhook.go └── main.go ├── .vscode ├── settings.json └── extensions.json ├── .protolint.yaml ├── TODO.md ├── .gitignore ├── tsconfig.json ├── Makefile ├── Dockerfile ├── cmd ├── version.go ├── root.go └── cmd.go ├── .github ├── workflows │ ├── release.yml │ └── test.yml └── dependabot.yml ├── .realize.yaml ├── config.yaml ├── LICENSE ├── docker-compose.yml ├── package.json ├── k8s └── webhook.yaml ├── protobuf ├── webhook.proto ├── webhook_grpc.pb.go └── webhook.pb.go ├── rollup.config.js ├── go.mod └── README.md /web/README.md: -------------------------------------------------------------------------------- 1 | # Webhook UI -------------------------------------------------------------------------------- /app/db/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | -------------------------------------------------------------------------------- /app/server/grpc/README.md: -------------------------------------------------------------------------------- 1 | # gRPC API 2 | -------------------------------------------------------------------------------- /app/server/rest/stat.go: -------------------------------------------------------------------------------- 1 | package http 2 | -------------------------------------------------------------------------------- /app/db/couchdb/couchdb.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | -------------------------------------------------------------------------------- /app/server/graphql/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL API 2 | -------------------------------------------------------------------------------- /web/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /app/server/rest/dto/stat.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type Stat struct { 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /app/mq/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "testing" 4 | 5 | func TestRedis(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | func FormatPort(port int) string { 6 | return ":" + strconv.Itoa(port) 7 | } 8 | -------------------------------------------------------------------------------- /app/db/elasticsearch/elasticsearch_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import "testing" 4 | 5 | func TestElasticsearch(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.protolint.yaml: -------------------------------------------------------------------------------- 1 | lint: 2 | rules: 3 | no_default: true 4 | 5 | add: 6 | - MESSAGE_NAMES_UPPER_CAMEL_CASE 7 | - SERVICE_NAMES_UPPER_CAMEL_CASE -------------------------------------------------------------------------------- /app/db/mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type db struct { 8 | indexName string 9 | timeout time.Duration 10 | } 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - log request and response in database 2 | - support pagination (after and before) 3 | - support filter 4 | - documentation 5 | - test 6 | - change to sveltekit instead 7 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /app/server/rest/dto/types.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | type DateTime time.Time 6 | 7 | func (dt DateTime) MarshalJSON() ([]byte, error) { 8 | return []byte(time.Time(dt).Format(`"2006-01-02T15:04:05Z"`)), nil 9 | } 10 | -------------------------------------------------------------------------------- /app/shared/webhook_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/si3nloong/webhook/cmd" 8 | ) 9 | 10 | func TestWebhook(t *testing.T) { 11 | svr := NewServer(&cmd.Config{}) 12 | log.Println(svr) 13 | } 14 | -------------------------------------------------------------------------------- /app/server/rest/health.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func (s *Server) health(w http.ResponseWriter, r *http.Request) { 9 | writeJson(w, http.StatusOK, json.RawMessage(`{"message":"ok"}`)) 10 | } 11 | -------------------------------------------------------------------------------- /web/src/routes.ts: -------------------------------------------------------------------------------- 1 | import WebhookList from "./pages/WebhookList.svelte"; 2 | import WebhookDetail from "./pages/WebhookDetail.svelte"; 3 | 4 | const routes = { 5 | "/": WebhookList, 6 | "/webhook/:id": WebhookDetail, 7 | }; 8 | 9 | export default routes; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | main 3 | webhook 4 | *.lock 5 | *.log 6 | node_modules 7 | /public/build/ 8 | .DS_Store 9 | public 10 | coverage.txt 11 | package-lock.json 12 | pnpm-lock.yaml 13 | .DS_Store 14 | /build 15 | /.svelte-kit 16 | /package 17 | test 18 | .realize.yaml 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // "rootDir": "web/src", 3 | "extends": "@tsconfig/svelte/tsconfig.json", 4 | "exclude": ["__sapper__/*", "web/public/*"], 5 | "include": [ 6 | "web/src/**/*", 7 | "node_modules/svelte-history-router/src/**/*", 8 | "**/*.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /app/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestFormatPort(t *testing.T) { 10 | require.Equal(t, ":3000", FormatPort(3000)) 11 | require.Equal(t, ":6739", FormatPort(6739)) 12 | require.Equal(t, ":8080", FormatPort(8080)) 13 | } 14 | -------------------------------------------------------------------------------- /app/server/graphql/graph/resolver.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import "github.com/si3nloong/webhook/app/shared" 4 | 5 | // This file will not be regenerated automatically. 6 | // 7 | // It serves as dependency injection for your app, add any dependencies you require here. 8 | 9 | type Resolver struct { 10 | shared.WebhookServer 11 | } 12 | -------------------------------------------------------------------------------- /app/mq/nsq/nsq.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/nsqio/go-nsq" 7 | ) 8 | 9 | type nsqMQ struct { 10 | } 11 | 12 | func New() *nsqMQ { 13 | config := nsq.NewConfig() 14 | consumer, err := nsq.NewConsumer("topic", "channel", config) 15 | if err != nil { 16 | log.Fatal(err) 17 | panic(err) 18 | } 19 | 20 | log.Println(consumer) 21 | return &nsqMQ{} 22 | } 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pb: 2 | protoc --proto_path=./protobuf \ 3 | --go_out=./protobuf --go_opt=paths=source_relative \ 4 | --go-grpc_out=./protobuf --go-grpc_opt=paths=source_relative \ 5 | ./protobuf/*.proto && \ 6 | protoc-go-inject-tag -input="./protobuf/*.pb.go" && \ 7 | echo "proto code generation successful" 8 | 9 | build: 10 | go build -o webhook main.go 11 | 12 | # @rm -rf ./protobuf/*.proto && \ 13 | # -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine as golang 2 | 3 | ADD . /go/app 4 | 5 | WORKDIR /go/app 6 | 7 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o main ./app/main.go 8 | 9 | FROM alpine:latest 10 | 11 | COPY --from=golang /usr/local/go/lib/time/zoneinfo.zip / 12 | COPY --from=golang /go/app/main /app/main 13 | 14 | ENV ZONEINFO=/zoneinfo.zip 15 | 16 | WORKDIR /app 17 | 18 | ENTRYPOINT ./main -------------------------------------------------------------------------------- /app/server/rest/dto/dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type Item struct { 4 | Item interface{} `json:"item"` 5 | } 6 | 7 | type Items struct { 8 | Items []interface{} `json:"items"` 9 | Count int64 `json:"count"` 10 | Size int `json:"size"` 11 | Links struct { 12 | Self string `json:"self"` 13 | Previous string `json:"previous"` 14 | Next string `json:"next"` 15 | } `json:"_links"` 16 | } 17 | -------------------------------------------------------------------------------- /app/mq/nats/api.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/si3nloong/webhook/app/entity" 8 | ) 9 | 10 | func (q *natsMQ) Publish(ctx context.Context, data *entity.WebhookRequest) error { 11 | b, err := json.Marshal(data) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | if _, err := q.js.Publish(q.subj, b); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /app/server/graphql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine as golang 2 | 3 | ADD . /go/app 4 | 5 | WORKDIR /go/app 6 | 7 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o main ./main.go 8 | 9 | FROM alpine:latest 10 | 11 | COPY --from=golang /usr/local/go/lib/time/zoneinfo.zip / 12 | COPY --from=golang /go/app/main /app/main 13 | 14 | ENV ZONEINFO=/zoneinfo.zip 15 | 16 | WORKDIR /app 17 | 18 | ENTRYPOINT ./main -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(versionCmd) 11 | } 12 | 13 | var versionCmd = &cobra.Command{ 14 | Use: "version", 15 | Short: "v", 16 | Long: `All software has versions. This is Hugo's`, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | fmt.Println("Hugo Static Site Generator v0.9 -- HEAD") 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | // Used for flags. 12 | cfgFile string 13 | userLicense string 14 | 15 | rootCmd = &cobra.Command{ 16 | Use: "webhook", 17 | Short: "A generator for Cobra based Applications", 18 | Long: `webhook is a CLI library for Go that empowers applications.`, 19 | } 20 | ) 21 | 22 | func Execute() { 23 | if err := rootCmd.Execute(); err != nil { 24 | fmt.Fprintln(os.Stderr, err) 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish distribution to Dockerhub 2 | on: 3 | release: 4 | types: [ created ] 5 | 6 | jobs: 7 | checkout: 8 | name: Build Container 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Inject slug/short variables 12 | uses: rlespinasse/github-slug-action@v3.x 13 | 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Go 1.17 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.17 -------------------------------------------------------------------------------- /app/server/grpc/transformer.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/si3nloong/webhook/app/entity" 5 | pb "github.com/si3nloong/webhook/protobuf" 6 | "google.golang.org/protobuf/types/known/timestamppb" 7 | ) 8 | 9 | func toWebhookProto(wh *entity.WebhookRequest) (proto *pb.Webhook) { 10 | proto = new(pb.Webhook) 11 | proto.Id = wh.ID.String() 12 | proto.Body = wh.Body 13 | proto.Method = wh.Method 14 | proto.Retries = uint32(len(wh.Attempts)) 15 | proto.CreatedAt = timestamppb.New(wh.CreatedAt) 16 | proto.UpdatedAt = timestamppb.New(wh.UpdatedAt) 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /app/server/grpc/health.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/si3nloong/webhook/protobuf" 7 | ) 8 | 9 | func (s *Server) Check(ctx context.Context, req *pb.HealthCheckRequest) (*pb.HealthCheckResponse, error) { 10 | resp := new(pb.HealthCheckResponse) 11 | resp.Status = pb.HealthCheckResponse_SERVING 12 | return resp, nil 13 | } 14 | 15 | func (s *Server) Watch(req *pb.HealthCheckRequest, stream pb.WebhookService_WatchServer) error { 16 | resp := new(pb.HealthCheckResponse) 17 | resp.Status = pb.HealthCheckResponse_SERVING 18 | return stream.SendMsg(resp) 19 | } 20 | -------------------------------------------------------------------------------- /.realize.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | legacy: 3 | force: false 4 | interval: 0s 5 | schema: 6 | - name: go-webhook 7 | path: ./app 8 | env: 9 | WEBHOOK_MESSAGE_QUEUE_REDIS_CLUSTER: true 10 | WEBHOOK_MESSAGE_QUEUE_REDIS_HOST: "localhost" 11 | WEBHOOK_MESSAGE_QUEUE_REDIS_PORT: "6379" 12 | WEBHOOK_MESSAGE_QUEUE_REDIS_PASSWORD: "" 13 | commands: 14 | install: 15 | status: true 16 | method: go install 17 | run: 18 | status: true 19 | watcher: 20 | extensions: 21 | - go 22 | paths: 23 | - / 24 | ignored_paths: 25 | - .git 26 | - .realize 27 | - vendor 28 | -------------------------------------------------------------------------------- /app/server/grpc/interceptor.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | func authorizationInterceptor(authToken string) grpcauth.AuthFunc { 12 | return func(ctx context.Context) (context.Context, error) { 13 | token, err := grpcauth.AuthFromMD(ctx, "bearer") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if token != authToken { 19 | return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %q", token) 20 | } 21 | 22 | return ctx, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/graphql/graph/scalar/filter.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | 8 | "github.com/99designs/gqlgen/graphql" 9 | ) 10 | 11 | // MarshalFilter : 12 | func MarshalFilter(v json.RawMessage) graphql.Marshaler { 13 | return graphql.WriterFunc(func(w io.Writer) { 14 | // fmt.Fprint(w, strconv.FormatUint(uint64(v), 10)) 15 | }) 16 | } 17 | 18 | // UnmarshalFilter : 19 | func UnmarshalFilter(v interface{}) (json.RawMessage, error) { 20 | switch vi := v.(type) { 21 | case map[string]interface{}: 22 | log.Println("filter =>", vi) 23 | return nil, nil 24 | 25 | default: 26 | return json.RawMessage(`{}`), nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: gomod 9 | directory: "/" 10 | target-branch: "dev" 11 | schedule: 12 | interval: daily 13 | 14 | - package-ecosystem: "npm" # See documentation for possible values 15 | directory: "/" # Location of package manifests 16 | target-branch: "dev" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # HTTP server 2 | enabled: true 3 | port: 3000 4 | no_of_worker: 2 5 | max_pending_webhook: 10000 6 | 7 | monitor: 8 | enabled: true 9 | 10 | # gRPC server 11 | grpc: 12 | enabled: true # enable gRPC server 13 | api_key: "abcd" 14 | port: 9000 15 | 16 | # Database - Elasticsearch 17 | elasticsearch: 18 | host: "http://localhost:9200" 19 | index_name: "" 20 | username: "" 21 | password: "" 22 | 23 | db: 24 | engine: "elasticsearch" 25 | 26 | message_queue: 27 | engine: "redis" # possible value is redis, nats, nsq 28 | topic: "webhook" 29 | queue_group: "webhook" 30 | redis: 31 | cluster: false 32 | addr: "127.0.0.1:6379" 33 | password: "" 34 | db: 1 35 | nats: 36 | js: true # indicate whether use jetstream or not 37 | stream: "webhook" 38 | queue: "webhook" -------------------------------------------------------------------------------- /app/server/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 5 | "github.com/si3nloong/webhook/app/shared" 6 | "github.com/si3nloong/webhook/cmd" 7 | pb "github.com/si3nloong/webhook/protobuf" 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | type Server struct { 12 | ws shared.WebhookServer 13 | pb.UnimplementedWebhookServiceServer 14 | } 15 | 16 | func NewServer(cfg *cmd.Config, ws shared.WebhookServer) *grpc.Server { 17 | opts := make([]grpc.ServerOption, 0) 18 | if cfg.GRPC.ApiKey != "" { 19 | opts = append(opts, grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(authorizationInterceptor(cfg.GRPC.ApiKey)))) 20 | } 21 | 22 | grpcServer := grpc.NewServer(opts...) 23 | svr := &Server{ws: ws} 24 | pb.RegisterWebhookServiceServer(grpcServer, svr) 25 | return grpcServer 26 | } 27 | -------------------------------------------------------------------------------- /web/src/components/Status.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {paths[1]} 13 | 14 | 36 | -------------------------------------------------------------------------------- /app/server/rest/README.md: -------------------------------------------------------------------------------- 1 | # rest API 2 | 3 | - Health check - [/health]() 4 | - Get webhook list - [/v1/webhooks](/app/http/rest/README.md#get-webhooks) 5 | - Find webhook by id - [/v1/webhook/:id](/app/http/rest/README.md#find-webhook) 6 | - Send a webhook - [/v1/webhook/send](/app/http/rest/README.md#send-webhook) 7 | 8 | ## Get Webhooks 9 | 10 | URL: /v1/webhooks 11 | 12 | Method: **GET** 13 | 14 | ## Find Webhook by ID 15 | 16 | URL: /v1/webhook/:id 17 | 18 | Method: **GET** 19 | 20 | ## Send Webhook 21 | 22 | URL: /v1/webhook/send 23 | 24 | Method: **POST** 25 | 26 | | Name | Data Type | Description | Required | 27 | | ------- | ------------------- | ------------------ | :------: | 28 | | url | `string` | URI | ✅ | 29 | | headers | `map[string]string` | HTTP headers | ❌ | 30 | | body | `string` | HTTP body | ❌ | 31 | | retry | `uint` | Maximum of retries | ❌ | 32 | -------------------------------------------------------------------------------- /app/server/rest/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/si3nloong/webhook/app/shared" 10 | ) 11 | 12 | type Server struct { 13 | *mux.Router 14 | shared.WebhookServer 15 | } 16 | 17 | func NewServer(ws shared.WebhookServer) *Server { 18 | svr := new(Server) 19 | svr.WebhookServer = ws 20 | svr.Router = mux.NewRouter() 21 | 22 | svr.Router.HandleFunc("/health", svr.health) 23 | svr.Router.HandleFunc("/v1/webhooks", svr.listWebhooks).Methods("GET") 24 | svr.Router.HandleFunc("/v1/webhook/{id}", svr.findWebhook).Methods("GET") 25 | svr.Router.HandleFunc("/v1/webhook/send", svr.sendWebhook).Methods("POST") 26 | return svr 27 | } 28 | 29 | func writeJson(w http.ResponseWriter, statusCode int, dest interface{}) { 30 | w.Header().Set("Content-Type", "application/json") 31 | w.WriteHeader(statusCode) 32 | b, err := json.Marshal(dest) 33 | if err != nil { 34 | log.Println(err) 35 | } 36 | w.Write(b) 37 | } 38 | -------------------------------------------------------------------------------- /app/shared/interface.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/si3nloong/webhook/app/entity" 7 | pb "github.com/si3nloong/webhook/protobuf" 8 | ) 9 | 10 | type Repository interface { 11 | CreateWebhook(ctx context.Context, data *entity.WebhookRequest) error 12 | GetWebhooks(ctx context.Context, curCursor string, limit uint) (datas []*entity.WebhookRequest, nextCursor string, totalCount int64, err error) 13 | FindWebhook(ctx context.Context, id string) (*entity.WebhookRequest, error) 14 | UpdateWebhook(ctx context.Context, id string, attempt *entity.Attempt) error 15 | } 16 | 17 | type MessageQueue interface { 18 | Publish(ctx context.Context, data *entity.WebhookRequest) error 19 | } 20 | 21 | type WebhookServer interface { 22 | Repository 23 | Validate(src interface{}) error 24 | Publish(ctx context.Context, req *pb.SendWebhookRequest) (*entity.WebhookRequest, error) 25 | LogError(err error) 26 | 27 | // TODO: better name (rename please) 28 | VarCtx(ctx context.Context, src interface{}, tag string) error 29 | } 30 | -------------------------------------------------------------------------------- /app/server/rest/dto/webhook.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // Webhook : 4 | type Webhook struct { 5 | ID string `json:"id"` 6 | URL string `json:"url"` 7 | Method string `json:"method"` 8 | Headers map[string]string `json:"headers"` 9 | Body string `json:"body"` 10 | Timeout uint `json:"timeout"` 11 | LastStatusCode uint `json:"lastStatusCode"` 12 | CreatedAt DateTime `json:"createdAt"` 13 | UpdatedAt DateTime `json:"updatedAt"` 14 | } 15 | 16 | // WebhookDetail : 17 | type WebhookDetail struct { 18 | Webhook 19 | NoOfRetries int `json:"noOfRetries"` 20 | Attempts []WebhookRetry `json:"attempts"` 21 | } 22 | 23 | // WebhookRetry : 24 | type WebhookRetry struct { 25 | Headers map[string]string `json:"headers"` 26 | Body string `json:"body"` 27 | StatusCode uint `json:"statusCode"` 28 | ElapsedTime int64 `json:"elapsedTime"` 29 | CreatedAt DateTime `json:"created"` 30 | } 31 | -------------------------------------------------------------------------------- /app/entity/webhook.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/segmentio/ksuid" 7 | ) 8 | 9 | type WebhookRequestStatus int 10 | 11 | const ( 12 | WebhookRequestPending WebhookRequestStatus = iota 13 | WebhookRequestFailed 14 | WebhookRequestSuccess 15 | ) 16 | 17 | // WebhookRequest : 18 | type WebhookRequest struct { 19 | ID ksuid.KSUID `json:"id"` 20 | Method string `json:"method"` 21 | URL string `json:"url"` 22 | Headers map[string]string `json:"headers"` 23 | Body string `json:"body"` 24 | Timeout uint `json:"timeout"` 25 | Attempts []Attempt `json:"attempts"` 26 | CreatedAt time.Time `json:"createdAt"` 27 | UpdatedAt time.Time `json:"updatedAt"` 28 | } 29 | 30 | // Attempt : 31 | type Attempt struct { 32 | Body string `json:"body"` 33 | Headers map[string]string `json:"headers"` 34 | StatusCode uint `json:"statusCode"` 35 | ElapsedTime int64 `json:"elapsedTime"` 36 | CreatedAt time.Time `json:"createdAt"` 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SianLoong. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | graph: 4 | build: 5 | # Set the context path to current directory 6 | context: . 7 | # Define the Dockerfile we going to use 8 | dockerfile: ./Dockerfile 9 | restart: on-failure 10 | # Set environment variables 11 | environment: 12 | WEBHOOK_PORT: "3000" 13 | networks: 14 | - intranet 15 | depends_on: 16 | - redis 17 | - jetstream 18 | links: # Link to containers in another service 19 | - redis 20 | - jetstream 21 | ports: 22 | - "8080:8080" 23 | 24 | # Define the `jetstream` service 25 | jetstream: 26 | image: "nats:latest" 27 | restart: on-failure 28 | command: server -js 29 | ports: 30 | - 4222:4222 31 | networks: 32 | - intranet 33 | # Define the storage path 34 | volumes: 35 | - jsm:/tmp/jetstream 36 | 37 | # Define the `redis` service 38 | redis: 39 | image: "redis:alpine" 40 | restart: on-failure 41 | networks: 42 | - intranet 43 | 44 | volumes: 45 | jsm: {} 46 | 47 | networks: 48 | intranet: 49 | driver: bridge -------------------------------------------------------------------------------- /app/server/graphql/graph/scalar/uint.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | 10 | "github.com/99designs/gqlgen/graphql" 11 | ) 12 | 13 | // MarshalUint : 14 | func MarshalUint(v uint) graphql.Marshaler { 15 | return graphql.WriterFunc(func(w io.Writer) { 16 | fmt.Fprint(w, strconv.FormatUint(uint64(v), 10)) 17 | }) 18 | } 19 | 20 | // UnmarshalUint : 21 | func UnmarshalUint(v interface{}) (uint, error) { 22 | switch vi := v.(type) { 23 | case string: 24 | u64, err := strconv.ParseUint(vi, 10, 64) 25 | if err != nil { 26 | return 0, err 27 | } 28 | return uint(u64), nil 29 | 30 | case int: 31 | if vi < 0 { 32 | return 0, errors.New("unsigned integer cannot be negative") 33 | } 34 | return uint(vi), nil 35 | 36 | case int64: 37 | if vi < 0 { 38 | return 0, errors.New("unsigned integer cannot be negative") 39 | } 40 | return uint(vi), nil 41 | 42 | case json.Number: 43 | i64, err := vi.Int64() 44 | if err != nil { 45 | return 0, err 46 | } 47 | if i64 < 0 { 48 | return 0, errors.New("unsigned integer cannot be negative") 49 | } 50 | return uint(i64), nil 51 | 52 | default: 53 | return 0, fmt.Errorf("%T is not an int", v) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/server/graphql/graph/scalar/uint64.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | 10 | "github.com/99designs/gqlgen/graphql" 11 | ) 12 | 13 | // MarshalUint64 : 14 | func MarshalUint64(v uint64) graphql.Marshaler { 15 | return graphql.WriterFunc(func(w io.Writer) { 16 | fmt.Fprint(w, strconv.FormatUint(v, 10)) 17 | }) 18 | } 19 | 20 | // UnmarshalUint64 : 21 | func UnmarshalUint64(v interface{}) (uint64, error) { 22 | switch vi := v.(type) { 23 | case string: 24 | u64, err := strconv.ParseUint(vi, 10, 64) 25 | if err != nil { 26 | return 0, err 27 | } 28 | return u64, nil 29 | 30 | case int: 31 | if vi < 0 { 32 | return 0, errors.New("unsigned integer cannot be negative") 33 | } 34 | return uint64(vi), nil 35 | 36 | case int64: 37 | if vi < 0 { 38 | return 0, errors.New("unsigned integer cannot be negative") 39 | } 40 | return uint64(vi), nil 41 | 42 | case json.Number: 43 | i64, err := vi.Int64() 44 | if err != nil { 45 | return 0, err 46 | } 47 | if i64 < 0 { 48 | return 0, errors.New("unsigned integer cannot be negative") 49 | } 50 | return uint64(i64), nil 51 | 52 | default: 53 | return 0, fmt.Errorf("%T is not an int", v) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook-monorepo", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "author": "Si3nLoong (https://github.com/si3nloong)", 6 | "homepage": "https://github.com/si3nloong/webhook", 7 | "scripts": { 8 | "build": "rollup -c", 9 | "dev": "rollup -c -w", 10 | "start": "sirv web/public --no-clear --single", 11 | "check": "svelte-check --tsconfig ./tsconfig.json" 12 | }, 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^24.0.1", 15 | "@rollup/plugin-node-resolve": "^13.0.5", 16 | "@rollup/plugin-typescript": "^8.2.5", 17 | "@tsconfig/svelte": "^3.0.0", 18 | "rollup": "^2.58.0", 19 | "rollup-plugin-css-only": "^3.1.0", 20 | "rollup-plugin-livereload": "^2.0.5", 21 | "rollup-plugin-svelte": "^7.1.0", 22 | "rollup-plugin-terser": "^7.0.2", 23 | "svelte": "^3.43.1", 24 | "svelte-check": "^2.2.6", 25 | "svelte-preprocess": "^4.9.8", 26 | "tslib": "^2.3.1", 27 | "typescript": "4.9.5" 28 | }, 29 | "dependencies": { 30 | "@apollo/client": "^3.5.10", 31 | "dayjs": "^1.10.7", 32 | "graphql": "^16.1.0", 33 | "sass": "^1.42.1", 34 | "sirv-cli": "^2.0.1", 35 | "svelte-apollo": "^0.5.0", 36 | "svelte-history-router": "^1.0.0-beta.16" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/server/rest/transformer/webhook.go: -------------------------------------------------------------------------------- 1 | package transformer 2 | 3 | import ( 4 | "github.com/si3nloong/webhook/app/entity" 5 | "github.com/si3nloong/webhook/app/server/rest/dto" 6 | ) 7 | 8 | func ToWebhook(data *entity.WebhookRequest) (o *dto.Webhook) { 9 | o = new(dto.Webhook) 10 | o.ID = data.ID.String() 11 | o.URL = data.URL 12 | o.Method = data.Method 13 | o.Headers = make(map[string]string) 14 | o.Body = data.Body 15 | o.Timeout = data.Timeout 16 | noOfRetries := len(data.Attempts) 17 | if noOfRetries > 0 { 18 | o.LastStatusCode = data.Attempts[noOfRetries-1].StatusCode 19 | } 20 | o.CreatedAt = dto.DateTime(data.CreatedAt) 21 | o.UpdatedAt = dto.DateTime(data.UpdatedAt) 22 | return 23 | } 24 | 25 | func ToWebhookDetail(data *entity.WebhookRequest) (o *dto.WebhookDetail) { 26 | o = new(dto.WebhookDetail) 27 | o.Webhook = *ToWebhook(data) 28 | o.NoOfRetries = len(data.Attempts) 29 | o.Attempts = make([]dto.WebhookRetry, 0) 30 | for _, r := range data.Attempts { 31 | attempt := dto.WebhookRetry{} 32 | attempt.Headers = make(map[string]string) 33 | attempt.Body = r.Body 34 | attempt.ElapsedTime = r.ElapsedTime 35 | attempt.StatusCode = r.StatusCode 36 | attempt.CreatedAt = dto.DateTime(r.CreatedAt) 37 | 38 | o.Attempts = append(o.Attempts, attempt) 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /app/server/grpc/api/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | // import "google/protobuf/empty.proto"; 5 | // import "google/protobuf/timestamp.proto"; 6 | 7 | service CurlHookService { 8 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 9 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 10 | rpc SendWebhook(SendWebhookRequest) returns (SendWebhookResponse); 11 | } 12 | 13 | message HealthCheckRequest { 14 | string service = 1; 15 | } 16 | 17 | message HealthCheckResponse { 18 | enum ServingStatus { 19 | UNKNOWN = 0; 20 | SERVING = 1; 21 | NOT_SERVING = 2; 22 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 23 | } 24 | ServingStatus status = 1; 25 | } 26 | 27 | message SendWebhookRequest { 28 | enum HttpMethod { 29 | GET = 0; 30 | POST = 1; 31 | } 32 | HttpMethod method = 1; 33 | // @gotags: validate:"required,url,max=1000" 34 | string url = 2; 35 | map headers = 3; 36 | // @gotags: validate:"max=2048" 37 | string body = 4; 38 | // @gotags: validate:"lte=10" 39 | uint32 retry = 5; 40 | enum RetryMechanism { 41 | BACKOFF = 0; 42 | LINEAR = 1; 43 | } 44 | RetryMechanism retryMechanism = 6; 45 | uint32 timeout = 7; 46 | } 47 | 48 | message SendWebhookResponse { 49 | string query = 1; 50 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | 8 | jobs: 9 | build-go: 10 | name: Build Go 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Inject slug/short variables 14 | uses: rlespinasse/github-slug-action@v3.x 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Go 1.17 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.17 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Test Data Race 33 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 34 | 35 | - name: Codecov 36 | uses: codecov/codecov-action@v2.0.3 37 | with: 38 | token: ${{secrets.CODECOV_TOKEN}} 39 | 40 | # codecov: 41 | # needs: [ checkout ] 42 | # name: Codecov 43 | # runs-on: ubuntu-latest 44 | # steps: 45 | # - name: Codecov 46 | # uses: codecov/codecov-action@v2.0.3 47 | # with: 48 | # token: ${{secrets.CODECOV_TOKEN}} 49 | 50 | -------------------------------------------------------------------------------- /app/server/grpc/webhook.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | pb "github.com/si3nloong/webhook/protobuf" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | func (s *Server) GetWebhooks(ctx context.Context, req *pb.ListWebhooksRequest) (*pb.ListWebhooksResponse, error) { 12 | if err := s.ws.Validate(req); err != nil { 13 | return nil, status.Convert(err).Err() 14 | } 15 | 16 | datas, nextCursor, _, err := s.ws.GetWebhooks(ctx, req.PageToken, 100) 17 | if err != nil { 18 | return nil, status.Convert(err).Err() 19 | } 20 | 21 | resp := new(pb.ListWebhooksResponse) 22 | for _, data := range datas { 23 | resp.Webhooks = append(resp.Webhooks, toWebhookProto(data)) 24 | } 25 | resp.NextPageToken = nextCursor 26 | return resp, nil 27 | } 28 | 29 | func (s *Server) GetWebhook(ctx context.Context, req *pb.GetWebhookRequest) (*pb.GetWebhookResponse, error) { 30 | if err := s.ws.Validate(req); err != nil { 31 | return nil, status.Convert(err).Err() 32 | } 33 | 34 | data, err := s.ws.FindWebhook(ctx, req.Id) 35 | if err != nil { 36 | return nil, status.Convert(err).Err() 37 | } 38 | 39 | resp := new(pb.GetWebhookResponse) 40 | resp.Webhook = toWebhookProto(data) 41 | return resp, nil 42 | } 43 | 44 | func (s *Server) SendWebhook(ctx context.Context, req *pb.SendWebhookRequest) (*pb.SendWebhookResponse, error) { 45 | if err := s.ws.Validate(req); err != nil { 46 | return nil, status.Convert(err).Err() 47 | } 48 | 49 | // push to message queue 50 | data, err := s.ws.Publish(ctx, req) 51 | if err != nil { 52 | return nil, status.Convert(err).Err() 53 | } 54 | 55 | log.Println(data) 56 | resp := new(pb.SendWebhookResponse) 57 | return resp, nil 58 | } 59 | -------------------------------------------------------------------------------- /app/mq/redis/consumer.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/adjust/rmq/v4" 8 | pb "github.com/si3nloong/webhook/protobuf" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | type taskConsumer struct { 13 | name string 14 | AutoAck bool 15 | AutoFinish bool 16 | SleepDuration time.Duration 17 | cb func(*pb.SendWebhookRequest) error 18 | 19 | LastDelivery rmq.Delivery 20 | LastDeliveries []rmq.Delivery 21 | 22 | finish chan int 23 | } 24 | 25 | func newTaskConsumer(cb func(*pb.SendWebhookRequest) error) rmq.Consumer { 26 | return &taskConsumer{ 27 | // name: name, 28 | cb: cb, 29 | AutoAck: true, 30 | AutoFinish: true, 31 | finish: make(chan int), 32 | } 33 | } 34 | 35 | func (c *taskConsumer) String() string { 36 | return c.name 37 | } 38 | 39 | func (c *taskConsumer) Consume(delivery rmq.Delivery) { 40 | req := new(pb.SendWebhookRequest) 41 | if err := proto.Unmarshal([]byte(delivery.Payload()), req); err != nil { 42 | return 43 | } 44 | 45 | log.Println("hERE 1") 46 | log.Println(delivery) 47 | 48 | if err := c.cb(req); err != nil { 49 | return 50 | } 51 | 52 | delivery.Ack() 53 | // log.Println(delivery.Reject()) 54 | // c.LastDelivery = delivery 55 | // c.LastDeliveries = append(c.LastDeliveries, delivery) 56 | 57 | // if c.SleepDuration > 0 { 58 | // time.Sleep(c.SleepDuration) 59 | // } 60 | // if c.AutoAck { 61 | // if err := delivery.Ack(); err != nil { 62 | // panic(err) 63 | // } 64 | // } 65 | // if !c.AutoFinish { 66 | // <-c.finish 67 | // } 68 | } 69 | 70 | func (c *taskConsumer) Finish() { 71 | c.finish <- 1 72 | } 73 | 74 | func (c *taskConsumer) FinishAll() { 75 | close(c.finish) 76 | } 77 | -------------------------------------------------------------------------------- /app/server/rest/server_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/si3nloong/webhook/app/entity" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type mockRepository struct { 15 | webhooks []*entity.WebhookRequest 16 | } 17 | 18 | func (m *mockRepository) CreateWebhook(ctx context.Context, data *entity.WebhookRequest) error { 19 | m.webhooks = append(m.webhooks, data) 20 | return nil 21 | } 22 | 23 | func (m *mockRepository) FindWebhook(ctx context.Context, id string) (*entity.WebhookRequest, error) { 24 | for _, wh := range m.webhooks { 25 | if wh.ID.String() == id { 26 | return wh, nil 27 | } 28 | } 29 | return nil, errors.New("data not found") 30 | } 31 | 32 | func (m *mockRepository) UpdateWebhook(ctx context.Context, id string, attempt *entity.Attempt) error { 33 | for _, wh := range m.webhooks { 34 | if wh.ID.String() == id { 35 | wh.Attempts = append(wh.Attempts, *attempt) 36 | break 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | type mockWebhookServer struct { 43 | } 44 | 45 | func TestServer(t *testing.T) { 46 | svr := new(Server) 47 | // ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | // // fmt.Fprintf(w, expected) 49 | // })) 50 | // defer ts.Close() 51 | 52 | req, err := http.NewRequest("GET", "/health", nil) 53 | require.NoError(t, err) 54 | 55 | rr := httptest.NewRecorder() 56 | handler := http.HandlerFunc(svr.health) 57 | 58 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 59 | // directly and pass in our Request and ResponseRecorder. 60 | handler.ServeHTTP(rr, req) 61 | 62 | require.Equal(t, http.StatusOK, rr.Code) 63 | } 64 | -------------------------------------------------------------------------------- /k8s/webhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webhook 5 | labels: 6 | app: webhook 7 | spec: 8 | ports: 9 | # HTTP/rest server 10 | - port: 3000 11 | targetPort: 3000 12 | protocol: TCP 13 | name: http 14 | # Monitor APIs 15 | - port: 3222 16 | targetPort: 3222 17 | protocol: TCP 18 | name: monitor 19 | # gRPC server 20 | - port: 5222 21 | targetPort: 5222 22 | protocol: TCP 23 | name: grpc 24 | type: ClusterIP 25 | selector: 26 | app: webhook 27 | 28 | --- 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: webhook 33 | labels: 34 | app: webhook 35 | spec: 36 | replicas: 1 37 | selector: 38 | matchLabels: 39 | app: webhook 40 | template: 41 | metadata: 42 | labels: 43 | app: webhook 44 | spec: 45 | terminationGracePeriodSeconds: 60 46 | containers: 47 | - name: webhook 48 | image: webhook 49 | imagePullPolicy: Always 50 | ports: 51 | - containerPort: 3000 52 | name: http 53 | - containerPort: 3222 54 | name: monitor 55 | - containerPort: 5222 56 | name: grpc 57 | # envFrom: 58 | # - configMapRef: 59 | # name: 60 | env: 61 | - name: PORT 62 | value: "5001" 63 | 64 | livenessProbe: 65 | httpGet: 66 | path: /health 67 | port: 3000 68 | initialDelaySeconds: 10 69 | timeoutSeconds: 5 70 | readinessProbe: 71 | httpGet: 72 | path: /health 73 | port: 3000 74 | initialDelaySeconds: 5 75 | periodSeconds: 5 76 | 77 | 78 | -------------------------------------------------------------------------------- /web/src/queries.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | type HttpHeader = [{ key: string; value: string }]; 4 | 5 | type WebhookAttempt = { 6 | body: string; 7 | headers: HttpHeader; 8 | elapsedTime: number; 9 | createdAt: string; 10 | }; 11 | 12 | export type Webhook = { 13 | id: string; 14 | method: string; 15 | url: string; 16 | body: string; 17 | headers: HttpHeader; 18 | timeout: number; 19 | noOfRetries: number; 20 | attempts: [WebhookAttempt]; 21 | lastStatusCode: number; 22 | createdAt: string; 23 | updatedAt: string; 24 | }; 25 | 26 | export type GetWebhooks = { 27 | webhooks: { 28 | nodes: [Webhook]; 29 | totalCount: number; 30 | pageInfo: { 31 | startCursor?: string; 32 | endCursor?: string; 33 | hasPreviousPage: boolean; 34 | hasNextPage: boolean; 35 | }; 36 | }; 37 | }; 38 | 39 | export const GET_WEBHOOKS = gql` 40 | query GetWebhooks($first: Uint!) { 41 | webhooks(first: $first, filter: {}) { 42 | nodes { 43 | id 44 | url 45 | method 46 | lastStatusCode 47 | createdAt 48 | } 49 | totalCount 50 | pageInfo { 51 | startCursor 52 | endCursor 53 | hasPreviousPage 54 | hasNextPage 55 | } 56 | } 57 | } 58 | `; 59 | 60 | export const FIND_WEBHOOK = gql` 61 | query FindWebhookByID($id: ID!) { 62 | webhook(id: $id) { 63 | id 64 | url 65 | method 66 | headers { 67 | key 68 | value 69 | } 70 | body 71 | timeout 72 | lastStatusCode 73 | attempts { 74 | body 75 | headers { 76 | key 77 | value 78 | } 79 | elapsedTime 80 | createdAt 81 | } 82 | createdAt 83 | updatedAt 84 | } 85 | } 86 | `; 87 | -------------------------------------------------------------------------------- /app/server/graphql/transformer/webhook_connection.go: -------------------------------------------------------------------------------- 1 | package transformer 2 | 3 | import ( 4 | "github.com/si3nloong/webhook/app/entity" 5 | "github.com/si3nloong/webhook/app/server/graphql/graph/model" 6 | ) 7 | 8 | func ToWebhookConnection(in []*entity.WebhookRequest, curCursor, nextCursor string, totalCount int64) (conn *model.WebhookConnection) { 9 | conn = new(model.WebhookConnection) 10 | conn.Nodes = ToLogs(in) 11 | conn.PageInfo = new(model.PageInfo) 12 | if curCursor != "" { 13 | conn.PageInfo.StartCursor = &curCursor 14 | } 15 | if nextCursor != "" { 16 | conn.PageInfo.EndCursor = &nextCursor 17 | conn.PageInfo.HasNextPage = true 18 | } 19 | conn.TotalCount = uint64(totalCount) 20 | return 21 | } 22 | 23 | func ToLogs(in []*entity.WebhookRequest) (out []*model.Webhook) { 24 | out = make([]*model.Webhook, len(in)) 25 | for idx := range in { 26 | out[idx] = ToWebhook(in[idx]) 27 | } 28 | return 29 | } 30 | 31 | func ToWebhook(in *entity.WebhookRequest) (out *model.Webhook) { 32 | out = new(model.Webhook) 33 | out.ID = in.ID.String() 34 | out.URL = in.URL 35 | out.Method = model.HTTPMethod(in.Method) 36 | out.Headers = make([]*model.HTTPHeader, 0) 37 | for k, v := range in.Headers { 38 | out.Headers = append(out.Headers, &model.HTTPHeader{Key: k, Value: v}) 39 | } 40 | out.Body = in.Body 41 | out.Timeout = in.Timeout 42 | out.Attempts = make([]*model.WebhookAttempt, len(in.Attempts)) 43 | for idx, a := range in.Attempts { 44 | attempt := model.WebhookAttempt{} 45 | attempt.ElapsedTime = a.ElapsedTime 46 | attempt.Headers = make([]*model.HTTPHeader, 0) 47 | for k, v := range a.Headers { 48 | attempt.Headers = append(attempt.Headers, &model.HTTPHeader{Key: k, Value: v}) 49 | } 50 | attempt.Body = a.Body 51 | attempt.StatusCode = uint(a.StatusCode) 52 | attempt.CreatedAt = a.CreatedAt 53 | 54 | out.LastStatusCode = a.StatusCode 55 | out.Attempts[idx] = &attempt 56 | } 57 | out.CreatedAt = in.CreatedAt 58 | out.UpdatedAt = in.UpdatedAt 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /app/server/graphql/graph/schema.graphqls: -------------------------------------------------------------------------------- 1 | # GraphQL schema example 2 | # 3 | # https://gqlgen.com/getting-started/ 4 | scalar Uint 5 | scalar Uint64 6 | scalar Int64 7 | scalar DateTime 8 | scalar Filter 9 | 10 | schema { 11 | query: Query 12 | # mutation: Mutation 13 | } 14 | 15 | # Directive 16 | directive @validate( 17 | rule: String! 18 | ) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION 19 | 20 | type PageInfo { 21 | hasNextPage: Boolean! 22 | hasPreviousPage: Boolean! 23 | startCursor: ID 24 | endCursor: ID 25 | } 26 | 27 | type Query { 28 | """ 29 | Get webhooks 30 | """ 31 | webhooks( 32 | after: ID 33 | before: ID 34 | first: Uint @validate(rule: "omitempty,required,min=1,max=100") 35 | last: Uint @validate(rule: "omitempty,required,min=1,max=100") 36 | filter: Filter 37 | ): WebhookConnection! 38 | 39 | """ 40 | Find webhook by id 41 | """ 42 | webhook(id: ID!): Webhook! 43 | } 44 | 45 | type WebhookConnection { 46 | """ 47 | A list of nodes. 48 | """ 49 | nodes: [Webhook!]! 50 | """ 51 | Information to aid in pagination. 52 | """ 53 | pageInfo: PageInfo! 54 | """ 55 | Identifies the total count of items in the connection. 56 | """ 57 | totalCount: Uint64! 58 | } 59 | 60 | type Webhook { 61 | id: ID! 62 | url: String! 63 | method: HttpMethod! 64 | headers: [HttpHeader!]! 65 | body: String! 66 | attempts: [WebhookAttempt!]! 67 | timeout: Uint! 68 | lastStatusCode: Uint! 69 | createdAt: DateTime! 70 | updatedAt: DateTime! 71 | } 72 | 73 | type WebhookAttempt { 74 | headers: [HttpHeader!]! 75 | body: String! 76 | elapsedTime: Int64! 77 | statusCode: Uint! 78 | createdAt: DateTime! 79 | } 80 | 81 | enum HttpMethod { 82 | GET 83 | POST 84 | PUT 85 | PATCH 86 | DELETE 87 | HEAD 88 | OPTIONS 89 | } 90 | 91 | type HttpHeader { 92 | key: String! 93 | value: String! 94 | } 95 | 96 | enum WebhookStatus { 97 | SUCCESS 98 | FAILED 99 | EXPIRED 100 | } 101 | -------------------------------------------------------------------------------- /web/src/App.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | 34 |
35 | push("/")}>Webhook UI 36 |
37 |
38 | 39 |
40 | 41 | 81 | -------------------------------------------------------------------------------- /app/server/graphql/graph/schema.resolvers.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | 10 | "github.com/si3nloong/webhook/app/server/graphql/graph/generated" 11 | "github.com/si3nloong/webhook/app/server/graphql/graph/model" 12 | "github.com/si3nloong/webhook/app/server/graphql/transformer" 13 | ) 14 | 15 | func (r *queryResolver) Webhooks(ctx context.Context, after *string, before *string, first *uint, last *uint, filter json.RawMessage) (*model.WebhookConnection, error) { 16 | var ( 17 | limit = defaultLimit 18 | curCursor string 19 | ) 20 | if first != nil && *first <= defaultLimit { 21 | limit = *first 22 | } else if last != nil && *last <= defaultLimit { 23 | limit = *last 24 | } 25 | if after != nil { 26 | curCursor = *after 27 | } 28 | 29 | datas, nextCursor, totalCount, err := r.GetWebhooks(ctx, curCursor, limit) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return transformer.ToWebhookConnection( 35 | datas, 36 | curCursor, 37 | nextCursor, 38 | totalCount, 39 | ), nil 40 | } 41 | 42 | func (r *queryResolver) Webhook(ctx context.Context, id string) (*model.Webhook, error) { 43 | data, err := r.FindWebhook(ctx, id) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return transformer.ToWebhook(data), nil 49 | } 50 | 51 | // Query returns generated.QueryResolver implementation. 52 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 53 | 54 | type queryResolver struct{ *Resolver } 55 | 56 | // !!! WARNING !!! 57 | // The code below was going to be deleted when updating resolvers. It has been copied here so you have 58 | // one last chance to move it out of harms way if you want. There are two reasons this happens: 59 | // - When renaming or deleting a resolver the old code will be put in here. You can safely delete 60 | // it when you're done. 61 | // - You have helper methods in this file. Move them out to keep these resolver files clean. 62 | const defaultLimit = uint(100) 63 | -------------------------------------------------------------------------------- /app/server/graphql/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - graph/*.graphqls 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: graph/generated/generated.go 8 | package: generated 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: graph/model/models_gen.go 18 | package: model 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | layout: follow-schema 23 | dir: graph 24 | package: graph 25 | 26 | # Optional: turn on use `gqlgen:"fieldName"` tags in your models 27 | # struct_tag: json 28 | 29 | # Optional: turn on to use []Thing instead of []*Thing 30 | # omit_slice_element_pointers: false 31 | 32 | # Optional: set to speed up generation time by not performing a final validation pass. 33 | # skip_validation: true 34 | 35 | # gqlgen will search for any type names in the schema in these go packages 36 | # if they match it will use them, otherwise it will generate them. 37 | autobind: 38 | - "github.com/si3nloong/webhook/app/server/graphql/graph/model" 39 | 40 | # This section declares type mapping between the GraphQL and go type systems 41 | # 42 | # The first line in each type will be used as defaults for resolver arguments and 43 | # modelgen, the others will be allowed when binding to fields. Configure them to 44 | # your liking 45 | models: 46 | ID: 47 | model: 48 | - github.com/99designs/gqlgen/graphql.ID 49 | - github.com/99designs/gqlgen/graphql.Int 50 | - github.com/99designs/gqlgen/graphql.Int64 51 | - github.com/99designs/gqlgen/graphql.Int32 52 | Int: 53 | model: 54 | - github.com/99designs/gqlgen/graphql.Int 55 | - github.com/99designs/gqlgen/graphql.Int64 56 | - github.com/99designs/gqlgen/graphql.Int32 57 | Int64: 58 | model: github.com/99designs/gqlgen/graphql.Int64 59 | DateTime: 60 | model: github.com/99designs/gqlgen/graphql.Time 61 | Uint: 62 | model: github.com/si3nloong/webhook/app/server/graphql/graph/scalar.Uint 63 | Uint64: 64 | model: github.com/si3nloong/webhook/app/server/graphql/graph/scalar.Uint64 65 | Filter: 66 | model: github.com/si3nloong/webhook/app/server/graphql/graph/scalar.Filter 67 | -------------------------------------------------------------------------------- /protobuf/webhook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protobuf; 4 | 5 | option go_package = "/protobuf"; 6 | // import "google/protobuf/empty.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | 9 | // follow spec of https://cloud.google.com/apis/design/naming_convention 10 | service WebhookService { 11 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 12 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 13 | rpc ListWebhooks(ListWebhooksRequest) returns (ListWebhooksResponse); 14 | rpc GetWebhook(GetWebhookRequest) returns (GetWebhookResponse); 15 | rpc SendWebhook(SendWebhookRequest) returns (SendWebhookResponse); 16 | // rpc RetryWebhook(SendWebhookRequest) returns (SendWebhookResponse); 17 | } 18 | 19 | message HealthCheckRequest { 20 | string service = 1; 21 | } 22 | 23 | message HealthCheckResponse { 24 | enum ServingStatus { 25 | UNKNOWN = 0; 26 | SERVING = 1; 27 | NOT_SERVING = 2; 28 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 29 | } 30 | ServingStatus status = 1; 31 | } 32 | 33 | message ListWebhooksRequest { 34 | // @gotags: validate:"omitempty,required" 35 | string page_token = 1; 36 | // @gotags: validate:"omitempty,max=100" 37 | uint32 page_size = 2; 38 | } 39 | 40 | message ListWebhooksResponse { 41 | repeated Webhook webhooks = 1; 42 | string next_page_token = 2; 43 | } 44 | 45 | message GetWebhookRequest { 46 | // @gotags: validate:"required" 47 | string id = 1; 48 | } 49 | 50 | message GetWebhookResponse { 51 | Webhook webhook = 1; 52 | } 53 | 54 | message SendWebhookRequest { 55 | enum HttpMethod { 56 | GET = 0; 57 | POST = 1; 58 | } 59 | HttpMethod method = 1; 60 | // @gotags: validate:"required,url,max=1000" 61 | string url = 2; 62 | map headers = 3; 63 | // @gotags: validate:"max=2048" 64 | string body = 4; 65 | // @gotags: validate:"omitempty,lte=10" 66 | uint32 retry = 5; 67 | enum RetryStrategy { 68 | BACKOFF = 0; 69 | LINEAR = 1; 70 | } 71 | RetryStrategy retry_strategy = 6; 72 | // @gotags: validate:"omitempty,lte=10000" 73 | uint32 timeout = 7; 74 | // @gotags: validate:"lte=3" 75 | uint32 concurrent = 8; 76 | } 77 | 78 | message SendWebhookResponse { 79 | string query = 1; 80 | } 81 | 82 | message Webhook { 83 | string id = 1; 84 | string method = 2; 85 | string body = 3; 86 | uint32 retries = 4; 87 | google.protobuf.Timestamp created_at = 5; 88 | google.protobuf.Timestamp updated_at = 6; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /app/mq/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | "time" 8 | 9 | "github.com/adjust/rmq/v4" 10 | "github.com/go-redis/redis/v8" 11 | "github.com/si3nloong/webhook/app/entity" 12 | "github.com/si3nloong/webhook/cmd" 13 | ) 14 | 15 | type redisMQ struct { 16 | err chan error 17 | subs []string 18 | client redis.Cmdable 19 | conn rmq.Connection 20 | queue rmq.Queue 21 | cleaner *rmq.Cleaner 22 | } 23 | 24 | func New(cfg *cmd.Config, cb func(delivery rmq.Delivery)) (*redisMQ, error) { 25 | mq := new(redisMQ) 26 | mq.err = make(chan error) 27 | 28 | if cfg.MessageQueue.Redis.Cluster { 29 | clusterClient := redis.NewClusterClient(&redis.ClusterOptions{ 30 | Addrs: strings.Split(cfg.MessageQueue.Redis.Addr, ","), 31 | Username: cfg.MessageQueue.Redis.Username, 32 | Password: cfg.MessageQueue.Redis.Password, 33 | }) 34 | mq.client = clusterClient 35 | } else { 36 | client := redis.NewClient(&redis.Options{ 37 | Addr: cfg.MessageQueue.Redis.Addr, 38 | Username: cfg.MessageQueue.Redis.Username, 39 | Password: cfg.MessageQueue.Redis.Password, 40 | DB: cfg.MessageQueue.Redis.DB, 41 | }) 42 | mq.client = client 43 | } 44 | 45 | if err := mq.client.Ping(context.Background()).Err(); err != nil { 46 | return nil, err 47 | } 48 | 49 | conn, err := rmq.OpenConnectionWithRedisClient(cfg.MessageQueue.Topic, mq.client, mq.err) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | q, err := conn.OpenQueue(cfg.MessageQueue.QueueGroup) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | if err := q.StartConsuming(3, 3*time.Second); err != nil { 60 | return nil, err 61 | } 62 | 63 | // setup consumers 64 | for i := 0; i < cfg.NoOfWorker; i++ { 65 | name, err := q.AddConsumerFunc(cfg.MessageQueue.QueueGroup, cb) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | mq.subs = append(mq.subs, name) 71 | } 72 | 73 | mq.cleaner = rmq.NewCleaner(conn) 74 | mq.conn = conn 75 | mq.queue = q 76 | return mq, nil 77 | } 78 | 79 | func (mq *redisMQ) Publish(ctx context.Context, data *entity.WebhookRequest) error { 80 | b, err := json.Marshal(data) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if err := mq.queue.PublishBytes(b); err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // func (mq *redisMQ) GracefulStop() error { 93 | // mq.queue.Destroy() 94 | // switch mq.queue.StopConsuming() { 95 | 96 | // } 97 | // return nil 98 | // } 99 | -------------------------------------------------------------------------------- /app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/go-playground/validator/v10" 13 | 14 | rpc "github.com/si3nloong/webhook/app/server/grpc" 15 | rest "github.com/si3nloong/webhook/app/server/rest" 16 | "github.com/si3nloong/webhook/app/shared" 17 | "github.com/si3nloong/webhook/app/util" 18 | "github.com/si3nloong/webhook/cmd" 19 | "github.com/spf13/viper" 20 | "google.golang.org/grpc" 21 | ) 22 | 23 | func main() { 24 | 25 | var ( 26 | grpcSvr *grpc.Server 27 | quit = make(chan os.Signal, 1) 28 | v = validator.New() 29 | wsCfg = new(cmd.Config) 30 | ) 31 | 32 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | 36 | // get current path 37 | pwd, err := os.Getwd() 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | viper.AddConfigPath(pwd) 43 | viper.SetConfigType("yaml") 44 | viper.SetConfigName("config") 45 | viper.SetEnvPrefix("webhook") 46 | viper.AutomaticEnv() 47 | 48 | if err := viper.ReadInConfig(); err != nil { 49 | panic(err) 50 | } 51 | 52 | // set the default value for configuration 53 | wsCfg.SetDefault() 54 | // read config into struct 55 | if err := viper.Unmarshal(&wsCfg); err != nil { 56 | panic(err) 57 | } 58 | 59 | // validate yaml value 60 | if err := v.StructCtx(ctx, wsCfg); err != nil { 61 | panic(err) 62 | } 63 | 64 | ws := shared.NewServer(wsCfg) 65 | 66 | // serve HTTP 67 | if wsCfg.Enabled { 68 | go func() error { 69 | log.Printf("HTTP server serve at %v", wsCfg.Port) 70 | log.Fatal(http.ListenAndServe(util.FormatPort(wsCfg.Port), rest.NewServer(ws))) 71 | return nil 72 | }() 73 | } 74 | 75 | // serve gRPC 76 | if wsCfg.GRPC.Enabled { 77 | grpcSvr = rpc.NewServer(wsCfg, ws) 78 | 79 | go func() error { 80 | lis, err := net.Listen("tcp", util.FormatPort(wsCfg.GRPC.Port)) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | log.Printf("gRPC server serve at %v", wsCfg.GRPC.Port) 86 | if err := grpcSvr.Serve(lis); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | }() 92 | } 93 | 94 | select { 95 | case <-quit: 96 | // close gRPC server if it's exists 97 | if grpcSvr != nil { 98 | grpcSvr.GracefulStop() 99 | } 100 | log.Println("Quit") 101 | 102 | case <-ctx.Done(): 103 | log.Println("ctx.Done!") 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from "rollup-plugin-svelte"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import livereload from "rollup-plugin-livereload"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import sveltePreprocess from "svelte-preprocess"; 7 | import typescript from "@rollup/plugin-typescript"; 8 | import css from "rollup-plugin-css-only"; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) server.kill(0); 17 | } 18 | 19 | return { 20 | writeBundle() { 21 | if (server) return; 22 | server = require("child_process").spawn( 23 | "npm", 24 | ["run", "start", "--", "--dev"], 25 | { 26 | stdio: ["ignore", "inherit", "inherit"], 27 | shell: true, 28 | } 29 | ); 30 | 31 | process.on("SIGTERM", toExit); 32 | process.on("exit", toExit); 33 | }, 34 | }; 35 | } 36 | 37 | export default { 38 | input: "web/src/main.ts", 39 | output: { 40 | sourcemap: true, 41 | format: "iife", 42 | name: "app", 43 | file: "web/public/build/bundle.js", 44 | }, 45 | plugins: [ 46 | svelte({ 47 | preprocess: sveltePreprocess({ sourceMap: !production }), 48 | compilerOptions: { 49 | // enable run-time checks when not in production 50 | dev: !production, 51 | }, 52 | }), 53 | // we'll extract any component CSS out into 54 | // a separate file - better for performance 55 | css({ output: "bundle.css" }), 56 | 57 | // If you have external dependencies installed from 58 | // npm, you'll most likely need these plugins. In 59 | // some cases you'll need additional configuration - 60 | // consult the documentation for details: 61 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 62 | resolve({ 63 | browser: true, 64 | dedupe: ["svelte"], 65 | }), 66 | commonjs(), 67 | typescript({ 68 | sourceMap: !production, 69 | inlineSources: !production, 70 | }), 71 | 72 | // In dev mode, call `npm run start` once 73 | // the bundle has been generated 74 | !production && serve(), 75 | 76 | // Watch the `public` directory and refresh the 77 | // browser on changes when not in production 78 | !production && livereload("web/public"), 79 | 80 | // If we're building for production (npm run build 81 | // instead of npm run dev), minify 82 | production && terser(), 83 | ], 84 | watch: { 85 | clearScreen: false, 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /app/server/graphql/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/99designs/gqlgen/graphql/handler" 11 | "github.com/gorilla/mux" 12 | "github.com/rs/cors" 13 | "github.com/si3nloong/webhook/app/server/graphql/graph" 14 | "github.com/si3nloong/webhook/app/server/graphql/graph/generated" 15 | "github.com/si3nloong/webhook/app/shared" 16 | "github.com/si3nloong/webhook/cmd" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | const defaultPort = "8080" 21 | 22 | func main() { 23 | var cfg cmd.Config 24 | // get current path 25 | pwd, err := os.Getwd() 26 | if err != nil { 27 | panic(err) 28 | } 29 | viper.AddConfigPath(pwd) 30 | viper.SetConfigType("yaml") 31 | viper.SetConfigName("config") 32 | 33 | viper.SetEnvPrefix("webhook") 34 | viper.AutomaticEnv() 35 | 36 | if err := viper.ReadInConfig(); err != nil { 37 | panic(err) 38 | } 39 | 40 | // log.Println("config =>", viper.ConfigFileUsed()) 41 | 42 | // set the default value for configuration 43 | cfg.SetDefault() 44 | // read config into struct 45 | if err := viper.Unmarshal(&cfg); err != nil { 46 | panic(err) 47 | } 48 | 49 | port := os.Getenv("PORT") 50 | if port == "" { 51 | port = defaultPort 52 | } 53 | 54 | ws := shared.NewServer(&cfg) 55 | 56 | r := mux.NewRouter() 57 | 58 | // Add CORS middleware around every request 59 | // See https://github.com/rs/cors for full option listing 60 | r.Use(cors.New(cors.Options{ 61 | AllowedOrigins: []string{"*"}, 62 | AllowCredentials: true, 63 | AllowedHeaders: []string{ 64 | "Apollographql-Client-Name", 65 | "Apollographql-Client-Version", 66 | "Content-Type", 67 | }, 68 | }).Handler) 69 | 70 | resolver := generated.Config{Resolvers: &graph.Resolver{WebhookServer: ws}} 71 | resolver.Directives.Validate = func(ctx context.Context, obj interface{}, next graphql.Resolver, rule string) (interface{}, error) { 72 | pc := graphql.GetPathContext(ctx) 73 | // graphql.GetFieldContext(ctx) 74 | if pc.Field == nil { 75 | return next(ctx) 76 | } 77 | 78 | v, ok := obj.(map[string]interface{}) 79 | if !ok { 80 | return next(ctx) 81 | } 82 | 83 | if err := ws.VarCtx(ctx, v[*pc.Field], rule); err != nil { 84 | // log.Println(reflect.TypeOf(err)) 85 | return nil, err 86 | } 87 | 88 | return next(ctx) 89 | } 90 | srv := handler.NewDefaultServer(generated.NewExecutableSchema(resolver)) 91 | // http.Handle("/", playground.Handler("GraphQL playground", "/query")) 92 | r.Handle("/", srv) 93 | 94 | log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 95 | log.Fatal(http.ListenAndServe(":"+port, r)) 96 | } 97 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/si3nloong/webhook 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.11.0 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/nats-io/nats.go v1.24.0 9 | github.com/spf13/cobra v1.6.1 10 | github.com/spf13/viper v1.12.0 11 | github.com/valyala/fasthttp v1.37.0 12 | google.golang.org/grpc v1.53.0 13 | google.golang.org/protobuf v1.28.1 14 | ) 15 | 16 | require ( 17 | github.com/agnivade/levenshtein v1.1.1 // indirect 18 | github.com/andybalholm/brotli v1.0.4 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 22 | github.com/fsnotify/fsnotify v1.5.4 // indirect 23 | github.com/go-playground/locales v0.14.0 // indirect 24 | github.com/go-playground/universal-translator v0.18.0 // indirect 25 | github.com/golang/protobuf v1.5.2 // indirect 26 | github.com/golang/snappy v0.0.3 // indirect 27 | github.com/gorilla/websocket v1.5.0 // indirect 28 | github.com/hashicorp/golang-lru v0.5.4 // indirect 29 | github.com/hashicorp/hcl v1.0.0 // indirect 30 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 31 | github.com/klauspost/compress v1.15.0 // indirect 32 | github.com/leodido/go-urn v1.2.1 // indirect 33 | github.com/magiconair/properties v1.8.6 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/nats-io/nkeys v0.3.0 // indirect 36 | github.com/nats-io/nuid v1.0.1 // indirect 37 | github.com/pelletier/go-toml v1.9.5 // indirect 38 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/spf13/afero v1.8.2 // indirect 41 | github.com/spf13/cast v1.5.0 // indirect 42 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/subosito/gotenv v1.3.0 // indirect 45 | github.com/tidwall/match v1.1.1 // indirect 46 | github.com/tidwall/pretty v1.2.0 // indirect 47 | github.com/valyala/bytebufferpool v1.0.0 // indirect 48 | golang.org/x/crypto v0.5.0 // indirect 49 | golang.org/x/net v0.5.0 // indirect 50 | golang.org/x/sys v0.4.0 // indirect 51 | golang.org/x/text v0.6.0 // indirect 52 | google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect 53 | gopkg.in/ini.v1 v1.66.4 // indirect 54 | gopkg.in/yaml.v2 v2.4.0 // indirect 55 | gopkg.in/yaml.v3 v3.0.1 // indirect 56 | ) 57 | 58 | require ( 59 | github.com/99designs/gqlgen v0.17.19 60 | github.com/adjust/rmq/v4 v4.0.2-0.20211012160217-2c375330b960 61 | github.com/avast/retry-go/v3 v3.1.1 62 | github.com/elastic/go-elasticsearch/v7 v7.17.1 63 | github.com/go-sql-driver/mysql v1.6.0 64 | github.com/gorilla/mux v1.8.0 65 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 66 | github.com/nats-io/nats-server/v2 v2.4.0 // indirect 67 | github.com/nsqio/go-nsq v1.1.0 68 | github.com/rs/cors v1.8.2 69 | github.com/segmentio/ksuid v1.0.4 70 | github.com/stretchr/testify v1.8.2 71 | github.com/tidwall/gjson v1.14.1 72 | github.com/vektah/gqlparser/v2 v2.5.1 73 | ) 74 | -------------------------------------------------------------------------------- /web/src/pages/WebhookDetail.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | {#if $webhook.data} 25 |

{$webhook.data.webhook.method} {$webhook.data.webhook.url}

26 |
{$webhook.data.webhook.timeout}ms (Milliseconds)
27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 |
Headers 32 | {#each $webhook.data.webhook.headers as item} 33 |
{item.key} {item.value}
34 | {/each} 35 |
Body{$webhook.data.webhook.body}
42 |
43 | {dayjs($webhook.data.webhook.createdAt).format("DD MMM YYYY HH:mmA")} 44 |
45 |
46 | {dayjs($webhook.data.webhook.updatedAt).format("DD MMM YYYY HH:mmA")} 47 |
48 | {#each $webhook.data.webhook.attempts as item, i} 49 |
50 |
51 |
Attempt {i + 1}
52 |
53 | ({item.elapsedTime}ms) {dayjs(item.createdAt).format( 54 | "DD MMM YYYY, HH:mm:ssA" 55 | )} 56 |
57 |
58 |
59 | 60 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 |
Headers 63 | {#each item.headers as item} 64 |
{item.key} {item.value}
65 | {/each} 66 |
Body{item.body}
73 |
74 |
75 | {/each} 76 | {:else if $webhook.error} 77 |
Webhook not found
78 | {/if} 79 |
80 | 81 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Webhook Server 6 | 7 | > A golang webhook server comply with at least once deliver. 8 | 9 | [![Build](https://github.com/si3nloong/webhook/workflows/Testing/badge.svg?branch=master)](https://github.com/si3nloong/webhook/actions/workflows/test.yml) 10 | [![Release](https://img.shields.io/github/v/tag/si3nloong/webhook)](https://github.com/si3nloong/webhook/releases) 11 | [![Go Report](https://goreportcard.com/badge/github.com/si3nloong/webhook)](https://goreportcard.com/report/github.com/si3nloong/webhook) 12 | [![Go Coverage](https://codecov.io/gh/si3nloong/webhook/branch/master/graph/badge.svg)](https://codecov.io/gh/si3nloong/webhook) 13 | [![LICENSE](https://img.shields.io/github/license/si3nloong/webhook)](https://github.com/si3nloong/webhook/blob/master/LICENSE) 14 | 15 | ## 🔨 Installation 16 | 17 | ```bash 18 | go get github.com/si3nloong/webhook 19 | ``` 20 | 21 | ## ⚙️ Configuration 22 | 23 | ```yaml 24 | # HTTP server 25 | enabled: true 26 | port: 3000 27 | no_of_worker: 2 28 | max_pending_webhook: 10000 29 | 30 | # gRPC server 31 | grpc: 32 | enabled: true # enable gRPC server 33 | api_key: "abcd" 34 | port: 9000 35 | 36 | message_queue: 37 | engine: "redis" # possible value is redis, nats, nsq 38 | topic: "webhook" 39 | queue_group: "webhook" 40 | redis: 41 | cluster: false 42 | addr: "127.0.0.1:6379" 43 | password: "" 44 | db: 1 45 | nats: 46 | js: true # indicate whether use jetstream or not 47 | ``` 48 | 49 | ## ✨ Features 50 | 51 | - Support [YAML](https://yaml.org/) and [env](https://en.wikipedia.org/wiki/Env) configuration. 52 | - Automatically re-send webhook if the response is fail. 53 | - [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) API ready. 54 | - [GraphQL](https://graphql.org/) API ready. 55 | - Support [gRPC](https://grpc.io/) protocol. 56 | - Allow to send a webhook using [cURL](https://curl.se/) command 57 | - Support Inmemory, [Redis](https://redis.io/), [NATS](https://nats.io/), NSQ as [Message Queue](https://en.wikipedia.org/wiki/Message_queue) engine. 58 | - Support [Elasticsearch](https://www.elastic.co/) as Persistent Volume engine. 59 | - Dockerize. 60 | - Configurable. 61 | - [Kubernetes](https://kubernetes.io/) ready. 62 | - Headless friendly. 63 | - CLI ready. 64 | 65 | 66 | ## ⚡️ rest APIs 67 | 68 | Please refer to [here](/app/http/rest/README.md). 69 | 70 | ## ⚡️ GraphQL API 71 | 72 | Please refer to [here](/app/http/graphql/README.md). 73 | 74 | ## 💡 gRPC API 75 | 76 | Please refer to [here](/app/grpc/README.md). 77 | 78 | ## ⚠️ Disclaimer 79 | 80 | This project still under development, don't use this in production! 81 | 82 | ## 🎉 Big Thanks To 83 | 84 | Thanks to these awesome companies for their support of Open Source developers ❤ 85 | 86 | [![GitHub](https://jstools.dev/img/badges/github.svg)](https://github.com/open-source) 87 | [![NPM](https://jstools.dev/img/badges/npm.svg)](https://www.npmjs.com/) 88 | 89 | ## License 90 | 91 | Copyright 2021 SianLoong 92 | 93 | Licensed under the [MIT License](/LICENSE). 94 | -------------------------------------------------------------------------------- /app/server/rest/webhook.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/si3nloong/webhook/app/server/rest/dto" 10 | "github.com/si3nloong/webhook/app/server/rest/transformer" 11 | pb "github.com/si3nloong/webhook/protobuf" 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | func (s *Server) listWebhooks(w http.ResponseWriter, r *http.Request) { 16 | ctx := r.Context() 17 | 18 | datas, _, totalCount, err := s.GetWebhooks(ctx, "", 100) 19 | if err != nil { 20 | return 21 | } 22 | 23 | items := new(dto.Items) 24 | items.Items = make([]interface{}, 0) 25 | for _, data := range datas { 26 | items.Items = append(items.Items, transformer.ToWebhook(data)) 27 | } 28 | items.Size = len(datas) 29 | items.Count = totalCount 30 | items.Links.Self = r.URL.Host 31 | items.Links.Previous = "" 32 | items.Links.Self = "" 33 | 34 | writeJson(w, http.StatusOK, items) 35 | } 36 | 37 | func (s *Server) findWebhook(w http.ResponseWriter, r *http.Request) { 38 | ctx := r.Context() 39 | 40 | id := strings.TrimSpace(mux.Vars(r)["id"]) 41 | if id == "" { 42 | return 43 | } 44 | 45 | data, err := s.FindWebhook(ctx, id) 46 | if err != nil { 47 | return 48 | } 49 | 50 | writeJson(w, http.StatusOK, &dto.Item{Item: transformer.ToWebhookDetail(data)}) 51 | } 52 | 53 | func (s *Server) sendWebhook(w http.ResponseWriter, r *http.Request) { 54 | ctx := r.Context() 55 | 56 | var i struct { 57 | URL string `json:"url" validate:"required,url,max=1000"` 58 | Method string `json:"method" validate:"omitempty,oneof=GET POST PATCH PUT DELETE"` 59 | Body string `json:"body" validate:"max=2048"` 60 | Headers map[string]string `json:"headers"` 61 | Retry struct { 62 | Max uint8 `json:"max" validate:"omitempty,required,max=10"` 63 | Strategy string `json:"strategy" validate:"omitempty,required,oneof=backoff"` 64 | } `json:"retry"` 65 | Concurrent uint8 `json:"concurrent"` 66 | Timeout uint `json:"timeout" validate:"omitempty,required,max=10000"` 67 | } 68 | 69 | // default value 70 | i.Method = "GET" 71 | i.Retry.Max = 10 72 | i.Retry.Strategy = "backoff" 73 | i.Timeout = 1000 // 1 second 74 | 75 | if err := json.NewDecoder(r.Body).Decode(&i); err != nil { 76 | writeJson(w, http.StatusBadRequest, err) 77 | return 78 | } 79 | 80 | i.URL = strings.TrimSpace(i.URL) 81 | i.Method = strings.TrimSpace(i.Method) 82 | i.Body = strings.TrimSpace(i.Body) 83 | 84 | if err := s.Validate(i); err != nil { 85 | writeJson(w, http.StatusUnprocessableEntity, err) 86 | return 87 | } 88 | 89 | req := pb.SendWebhookRequest{} 90 | 91 | switch i.Method { 92 | case fasthttp.MethodGet: 93 | req.Method = pb.SendWebhookRequest_GET 94 | case fasthttp.MethodPost: 95 | req.Method = pb.SendWebhookRequest_POST 96 | case fasthttp.MethodPatch: 97 | req.Method = pb.SendWebhookRequest_POST 98 | case fasthttp.MethodPut: 99 | req.Method = pb.SendWebhookRequest_POST 100 | case fasthttp.MethodDelete: 101 | req.Method = pb.SendWebhookRequest_POST 102 | } 103 | 104 | req.Url = i.URL 105 | req.Body = i.Body 106 | req.Headers = i.Headers 107 | req.Retry = uint32(i.Retry.Max) 108 | 109 | // push to nats 110 | data, err := s.Publish(ctx, &req) 111 | if err != nil { 112 | writeJson(w, http.StatusInternalServerError, err) 113 | return 114 | } 115 | 116 | writeJson(w, http.StatusOK, &dto.Item{Item: transformer.ToWebhook(data)}) 117 | } 118 | -------------------------------------------------------------------------------- /app/mq/nats/nats.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "reflect" 8 | "sync" 9 | "time" 10 | 11 | "github.com/avast/retry-go/v3" 12 | "github.com/nats-io/nats.go" 13 | "github.com/si3nloong/webhook/cmd" 14 | pb "github.com/si3nloong/webhook/protobuf" 15 | "github.com/valyala/fasthttp" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | type natsMQ struct { 20 | mu sync.RWMutex 21 | subj string 22 | js nats.JetStreamContext 23 | subs []*nats.Subscription 24 | } 25 | 26 | func New(cfg *cmd.Config) (*natsMQ, error) { 27 | q := new(natsMQ) 28 | 29 | // Connect to NATS 30 | nc, err := nats.Connect(nats.DefaultURL) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // if err := handleMessage(&pb.SendWebhookRequest{ 36 | // Url: "https://183jkashdkjhasjkdh.com", 37 | // }); err != nil { 38 | // panic(err) 39 | // } 40 | 41 | log.Println(nc.Statistics) 42 | 43 | // Create JetStream Context 44 | js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | streamName := "webhook" 50 | stream, err := js.StreamInfo(streamName) 51 | log.Println(stream, err) 52 | // js.DeleteStream(streamName) 53 | if err == nats.ErrStreamNotFound { 54 | if _, err := js.AddStream(&nats.StreamConfig{ 55 | Name: streamName, 56 | Subjects: []string{"test"}, 57 | }); err != nil { 58 | panic(err) 59 | } 60 | } else if err != nil { 61 | panic(err) 62 | } 63 | 64 | for i := 0; i < cfg.NoOfWorker; i++ { 65 | q.subs = append(q.subs) 66 | } 67 | 68 | { 69 | sub, err := js.QueueSubscribe( 70 | "test", 71 | "webhook1", 72 | q.onQueueSubscribe, 73 | nats.ManualAck(), 74 | nats.AckWait(10*time.Second), 75 | nats.MaxDeliver(10), 76 | ) 77 | 78 | log.Println("Subscription =>", sub, err) 79 | } 80 | 81 | q.js = js 82 | 83 | return q, nil 84 | } 85 | 86 | func (mq *natsMQ) onQueueSubscribe(msg *nats.Msg) { 87 | log.Println("Handle message ========>") 88 | log.Println(string(msg.Data)) 89 | // log.Println(msg) 90 | req := new(pb.SendWebhookRequest) 91 | 92 | if err := proto.Unmarshal(msg.Data, req); err != nil { 93 | log.Println(err) 94 | return 95 | } 96 | 97 | if err := handleMessage(req); err != nil { 98 | log.Println(err) 99 | // msg.Nak() 100 | return 101 | } 102 | 103 | // if everything ok, acknowledge and don't retry 104 | msg.Ack() 105 | } 106 | 107 | func handleMessage(req *pb.SendWebhookRequest) error { 108 | retry.Do( 109 | func() error { 110 | return nil 111 | }, 112 | retry.Attempts(3), 113 | ) 114 | httpReq := fasthttp.AcquireRequest() 115 | httpResp := fasthttp.AcquireResponse() 116 | defer fasthttp.ReleaseRequest(httpReq) 117 | defer fasthttp.ReleaseResponse(httpResp) 118 | httpReq.Header.SetRequestURI(req.Url) 119 | httpReq.Header.SetMethod(req.Method.String()) 120 | 121 | for k, v := range req.Headers { 122 | httpReq.Header.Add(k, v) 123 | } 124 | httpReq.AppendBodyString(req.Body) 125 | 126 | // By default timeout is 5 seconds 127 | timeout := 5 * time.Second 128 | if req.Timeout > 0 { 129 | timeout = time.Second * time.Duration(req.Timeout) 130 | } 131 | 132 | log.Println("Request =======>") 133 | log.Println(httpReq.String()) 134 | 135 | var dnsError *net.DNSError 136 | if err := fasthttp.DoTimeout(httpReq, httpResp, timeout); errors.As(err, &dnsError) { 137 | // If it's a invalid host, drop the request directly 138 | log.Println(err, reflect.TypeOf(err)) 139 | return err 140 | } else if err != nil { 141 | return err 142 | } 143 | 144 | log.Println("Response =======>") 145 | log.Println(httpResp.String()) 146 | statusCode := httpResp.StatusCode() 147 | 148 | // 100 - 199 149 | if statusCode < fasthttp.StatusOK { 150 | // 200 - 399 151 | } else if statusCode >= fasthttp.StatusBadRequest { 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (q *natsMQ) GracefulStop() error { 158 | // return q. 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /app/server/graphql/graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type HTTPHeader struct { 13 | Key string `json:"key"` 14 | Value string `json:"value"` 15 | } 16 | 17 | type PageInfo struct { 18 | HasNextPage bool `json:"hasNextPage"` 19 | HasPreviousPage bool `json:"hasPreviousPage"` 20 | StartCursor *string `json:"startCursor"` 21 | EndCursor *string `json:"endCursor"` 22 | } 23 | 24 | type Webhook struct { 25 | ID string `json:"id"` 26 | URL string `json:"url"` 27 | Method HTTPMethod `json:"method"` 28 | Headers []*HTTPHeader `json:"headers"` 29 | Body string `json:"body"` 30 | Attempts []*WebhookAttempt `json:"attempts"` 31 | Timeout uint `json:"timeout"` 32 | LastStatusCode uint `json:"lastStatusCode"` 33 | CreatedAt time.Time `json:"createdAt"` 34 | UpdatedAt time.Time `json:"updatedAt"` 35 | } 36 | 37 | type WebhookAttempt struct { 38 | Headers []*HTTPHeader `json:"headers"` 39 | Body string `json:"body"` 40 | ElapsedTime int64 `json:"elapsedTime"` 41 | StatusCode uint `json:"statusCode"` 42 | CreatedAt time.Time `json:"createdAt"` 43 | } 44 | 45 | type WebhookConnection struct { 46 | // A list of nodes. 47 | Nodes []*Webhook `json:"nodes"` 48 | // Information to aid in pagination. 49 | PageInfo *PageInfo `json:"pageInfo"` 50 | // Identifies the total count of items in the connection. 51 | TotalCount uint64 `json:"totalCount"` 52 | } 53 | 54 | type HTTPMethod string 55 | 56 | const ( 57 | HTTPMethodGet HTTPMethod = "GET" 58 | HTTPMethodPost HTTPMethod = "POST" 59 | HTTPMethodPut HTTPMethod = "PUT" 60 | HTTPMethodPatch HTTPMethod = "PATCH" 61 | HTTPMethodDelete HTTPMethod = "DELETE" 62 | HTTPMethodHead HTTPMethod = "HEAD" 63 | HTTPMethodOptions HTTPMethod = "OPTIONS" 64 | ) 65 | 66 | var AllHTTPMethod = []HTTPMethod{ 67 | HTTPMethodGet, 68 | HTTPMethodPost, 69 | HTTPMethodPut, 70 | HTTPMethodPatch, 71 | HTTPMethodDelete, 72 | HTTPMethodHead, 73 | HTTPMethodOptions, 74 | } 75 | 76 | func (e HTTPMethod) IsValid() bool { 77 | switch e { 78 | case HTTPMethodGet, HTTPMethodPost, HTTPMethodPut, HTTPMethodPatch, HTTPMethodDelete, HTTPMethodHead, HTTPMethodOptions: 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | func (e HTTPMethod) String() string { 85 | return string(e) 86 | } 87 | 88 | func (e *HTTPMethod) UnmarshalGQL(v interface{}) error { 89 | str, ok := v.(string) 90 | if !ok { 91 | return fmt.Errorf("enums must be strings") 92 | } 93 | 94 | *e = HTTPMethod(str) 95 | if !e.IsValid() { 96 | return fmt.Errorf("%s is not a valid HttpMethod", str) 97 | } 98 | return nil 99 | } 100 | 101 | func (e HTTPMethod) MarshalGQL(w io.Writer) { 102 | fmt.Fprint(w, strconv.Quote(e.String())) 103 | } 104 | 105 | type WebhookStatus string 106 | 107 | const ( 108 | WebhookStatusSuccess WebhookStatus = "SUCCESS" 109 | WebhookStatusFailed WebhookStatus = "FAILED" 110 | WebhookStatusExpired WebhookStatus = "EXPIRED" 111 | ) 112 | 113 | var AllWebhookStatus = []WebhookStatus{ 114 | WebhookStatusSuccess, 115 | WebhookStatusFailed, 116 | WebhookStatusExpired, 117 | } 118 | 119 | func (e WebhookStatus) IsValid() bool { 120 | switch e { 121 | case WebhookStatusSuccess, WebhookStatusFailed, WebhookStatusExpired: 122 | return true 123 | } 124 | return false 125 | } 126 | 127 | func (e WebhookStatus) String() string { 128 | return string(e) 129 | } 130 | 131 | func (e *WebhookStatus) UnmarshalGQL(v interface{}) error { 132 | str, ok := v.(string) 133 | if !ok { 134 | return fmt.Errorf("enums must be strings") 135 | } 136 | 137 | *e = WebhookStatus(str) 138 | if !e.IsValid() { 139 | return fmt.Errorf("%s is not a valid WebhookStatus", str) 140 | } 141 | return nil 142 | } 143 | 144 | func (e WebhookStatus) MarshalGQL(w io.Writer) { 145 | fmt.Fprint(w, strconv.Quote(e.String())) 146 | } 147 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type MessageQueueEngine string 13 | 14 | const ( 15 | MessageQueueEngineRedis MessageQueueEngine = "redis" 16 | MessageQueueEngineNats MessageQueueEngine = "nats" 17 | MessageQueueEngineNSQ MessageQueueEngine = "nsq" 18 | ) 19 | 20 | type DatabaseEngine string 21 | 22 | const ( 23 | DatabaseEngineElasticsearch DatabaseEngine = "elasticsearch" 24 | ) 25 | 26 | type Config struct { 27 | Enabled bool `mapstructure:"enabled"` 28 | Port int `mapstructure:"port"` 29 | Debug bool `mapstructure:"debug"` 30 | Retry uint `mapstructure:"retry" validate:"lte=50"` 31 | RetryMechanism string `mapstructure:"retry_mechanism"` 32 | NoOfWorker int `mapstructure:"no_of_worker" validate:"required,lte=100"` 33 | Monitor struct { 34 | Enabled bool `mapstructure:"enabled"` 35 | Port int `mapstructure:"port"` 36 | } `mapstructure:"monitor"` 37 | Elasticsearch struct { 38 | Host string `mapstructure:"host" validate:"required"` 39 | Username string `mapstructure:"username"` 40 | Password string `mapstructure:"password"` 41 | ApiKey string `mapstructure:"api_key"` 42 | } `mapstructure:"elasticsearch"` 43 | DB struct { 44 | Engine DatabaseEngine `mapstructure:"engine" validate:"oneof=elasticsearch"` 45 | } `mapstructure:"db"` 46 | GRPC struct { 47 | Enabled bool `mapstructure:"enabled"` 48 | ApiKey string `mapstructure:"api_key"` 49 | Port int `mapstructure:"port"` 50 | } `mapstructure:"grpc"` 51 | MessageQueue struct { 52 | Engine MessageQueueEngine `mapstructure:"engine" validate:"oneof=redis nats"` 53 | Topic string `mapstructure:"topic" validate:"alphanum"` 54 | QueueGroup string `mapstructure:"queue_group" validate:"alphanum"` 55 | Redis struct { 56 | Cluster bool `mapstructure:"cluster"` 57 | Addr string `mapstructure:"addr"` 58 | Username string `mapstructure:"username"` 59 | Password string `mapstructure:"password"` 60 | DB int `mapstructure:"db"` 61 | } `mapstructure:"redis"` 62 | NATS struct { 63 | JetStream bool `mapstructure:"js"` 64 | } `mapstructure:"nats"` 65 | NSQ struct { 66 | } `mapstructure:"nsq"` 67 | } `mapstructure:"message_queue"` 68 | } 69 | 70 | func (c *Config) SetDefault() { 71 | c.NoOfWorker = runtime.NumCPU() 72 | c.Enabled = true 73 | c.Port = 3000 74 | c.Monitor.Port = 3222 75 | c.GRPC.Port = 5222 76 | // c.GRPC.Enabled = true 77 | c.MessageQueue.Redis.Addr = "localhost:6379" 78 | c.MessageQueue.NATS.JetStream = true 79 | } 80 | 81 | func init() { 82 | cobra.OnInitialize(initConfig) 83 | 84 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/config.yaml)") 85 | rootCmd.PersistentFlags().StringP("author", "a", "SianLoong", "author name for copyright attribution") 86 | rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "MIT") 87 | rootCmd.PersistentFlags().StringP("version", "v", "", "1.0.0-alpha0") 88 | // rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") 89 | // viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("author")) 90 | // viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper")) 91 | // viper.SetDefault("author", "NAME HERE ") 92 | // viper.SetDefault("license", "apache") 93 | 94 | // log.Println("here 2") 95 | // viper.SetDefault("config", "config.yaml") 96 | // viper.SetDefault("CURLHOOK_REDIS_PORT", "6379") 97 | // viper.SetDefault("CURLHOOK_REDIS_CLUSTER", false) 98 | // viper.SetDefault("grpc.") 99 | 100 | // log.Println(cfgFile) 101 | // rootCmd.AddCommand(addCmd) 102 | // rootCmd.AddCommand(initCmd) 103 | } 104 | 105 | func initConfig() { 106 | if cfgFile != "" { 107 | // Use config file from the flag. 108 | viper.SetConfigFile(cfgFile) 109 | } else { 110 | // Find home directory. 111 | home, err := os.UserHomeDir() 112 | cobra.CheckErr(err) 113 | 114 | // Search config in home directory with name ".cobra" (without extension). 115 | viper.AddConfigPath(home) 116 | viper.SetConfigType("yaml") 117 | viper.SetConfigName(".cobra") 118 | } 119 | 120 | viper.AutomaticEnv() 121 | 122 | if err := viper.ReadInConfig(); err == nil { 123 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/db/elasticsearch/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "time" 11 | 12 | "github.com/elastic/go-elasticsearch/v7" 13 | "github.com/elastic/go-elasticsearch/v7/esapi" 14 | "github.com/segmentio/ksuid" 15 | "github.com/si3nloong/webhook/app/entity" 16 | "github.com/si3nloong/webhook/cmd" 17 | "github.com/tidwall/gjson" 18 | ) 19 | 20 | type db struct { 21 | indexName string 22 | client *elasticsearch.Client 23 | timeout time.Duration 24 | } 25 | 26 | func New(cfg *cmd.Config) (*db, error) { 27 | esConfig := elasticsearch.Config{ 28 | Addresses: []string{ 29 | cfg.Elasticsearch.Host, 30 | }, 31 | Username: cfg.Elasticsearch.Username, 32 | Password: cfg.Elasticsearch.Password, 33 | APIKey: cfg.Elasticsearch.ApiKey, 34 | } 35 | 36 | // es, err := elasticsearch.NewDefaultClient() 37 | client, err := elasticsearch.NewClient(esConfig) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | res, err := client.Info() 43 | if err != nil { 44 | return nil, err 45 | } 46 | defer res.Body.Close() 47 | 48 | b, _ := ioutil.ReadAll(res.Body) 49 | log.Println(string(b)) 50 | 51 | v := new(db) 52 | v.indexName = "webhook_index" 53 | v.client = client 54 | v.timeout = 1 * time.Minute 55 | return v, nil 56 | } 57 | 58 | func (c *db) GetWebhooks(ctx context.Context, curCursor string, limit uint) (datas []*entity.WebhookRequest, nextCursor string, totalCount int64, err error) { 59 | opts := make([]func(*esapi.SearchRequest), 0) 60 | 61 | opts = append(opts, c.client.Search.WithContext(ctx)) 62 | opts = append(opts, c.client.Search.WithIndex(c.indexName)) 63 | opts = append(opts, c.client.Search.WithSize(int(limit+1))) 64 | opts = append(opts, c.client.Search.WithTrackTotalHits(true)) 65 | opts = append(opts, c.client.Search.WithSort("createdAt:desc")) 66 | // opts = append(opts, c.client.Search.WithPretty()) 67 | 68 | // if after != nil { 69 | // // opts = append(opts, c.client.Search.WithQuery(fmt.Sprintf(`{"range": {"id": {"gte": %q}}}`, curCursor))) 70 | // } 71 | res, err := c.client.Search(opts...) 72 | if err != nil { 73 | return nil, "", 0, err 74 | } 75 | defer res.Body.Close() 76 | 77 | var buf bytes.Buffer 78 | if _, err := buf.ReadFrom(res.Body); err != nil { 79 | return nil, "", 0, err 80 | } 81 | 82 | // log.Println(buf.String()) 83 | totalCount = gjson.GetBytes(buf.Bytes(), "hits.total.value").Int() 84 | result := gjson.GetBytes(buf.Bytes(), "hits.hits").Array() 85 | noOfRecord := uint(len(result)) 86 | 87 | for _, r := range result { 88 | data := entity.WebhookRequest{} 89 | err = json.Unmarshal([]byte(r.Get("_source").Raw), &data) 90 | if err != nil { 91 | return 92 | } 93 | 94 | datas = append(datas, &data) 95 | } 96 | 97 | // means has cursor 98 | if noOfRecord > limit { 99 | last := datas[limit] 100 | nextCursor = last.ID.String() 101 | datas = datas[:limit] 102 | } 103 | return 104 | } 105 | 106 | func (c *db) FindWebhook(ctx context.Context, id string) (data *entity.WebhookRequest, err error) { 107 | // Instantiate a request object 108 | req := esapi.GetRequest{ 109 | Index: c.indexName, 110 | DocumentID: id, 111 | } 112 | 113 | res, err := req.Do(ctx, c.client) 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer res.Body.Close() 118 | 119 | var o struct { 120 | Source interface{} `json:"_source"` 121 | } 122 | 123 | data = new(entity.WebhookRequest) 124 | o.Source = data 125 | err = json.NewDecoder(res.Body).Decode(&o) 126 | return 127 | } 128 | 129 | func (c *db) CreateWebhook(ctx context.Context, data *entity.WebhookRequest) error { 130 | blr := new(bytes.Buffer) 131 | 132 | nilTime := time.Time{} 133 | utcNow := time.Now().UTC() 134 | if data.ID == ksuid.Nil { 135 | data.ID = ksuid.New() 136 | } 137 | if data.CreatedAt == nilTime { 138 | data.CreatedAt = utcNow 139 | } 140 | if data.UpdatedAt == nilTime { 141 | data.UpdatedAt = utcNow 142 | } 143 | if err := json.NewEncoder(blr).Encode(data); err != nil { 144 | return err 145 | } 146 | 147 | // Instantiate a request object 148 | req := esapi.IndexRequest{ 149 | Index: c.indexName, 150 | DocumentID: data.ID.String(), 151 | Body: blr, 152 | Refresh: "true", 153 | } 154 | 155 | if _, err := req.Do(ctx, c.client); err != nil { 156 | return err 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (c *db) UpdateWebhook(ctx context.Context, id string, attempt *entity.Attempt) error { 163 | b, err := json.Marshal(attempt) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | var buf bytes.Buffer 169 | buf.WriteString(fmt.Sprintf(`{ 170 | "script": { 171 | "source": "ctx._source.attempts.addAll(params.attempt);", 172 | "lang": "painless", 173 | "params" : { 174 | "attempt" : [%s] 175 | } 176 | } 177 | }`, b)) 178 | 179 | res, err := c.client.Update( 180 | c.indexName, 181 | id, 182 | &buf, 183 | c.client.Update.WithContext(ctx), 184 | c.client.Update.WithPretty(), 185 | ) 186 | if err != nil { 187 | return err 188 | } 189 | defer res.Body.Close() 190 | 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /web/src/pages/WebhookList.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
36 |
37 |
38 |
Method
39 |
40 | 45 |
46 |
47 |
48 |
Status
49 |
50 | 56 |
57 |
58 |
59 |
Start Time
60 |
61 |
62 |
63 |
64 |
End Time
65 |
66 |
67 |
68 |
Limit Results
69 |
70 | 71 |
72 | 77 |
78 |
79 |
80 | 86 | {#if $webhooks.loading} 87 |
Loading...
88 | {:else if $webhooks.data} 89 | {#if $webhooks.data.webhooks.nodes.length > 0} 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {#each $webhooks.data.webhooks.nodes as item} 99 | 100 | 101 | 106 | 107 | 108 | 109 | 110 | {/each} 111 |
StatusWebhook IDMethodURLDate
push(`/webhook/${item.id}`)} 103 | >{item.id}{item.method}{item.url}{dayjs(item.createdAt).format("DD MMM YYYY, HH:mm:ss A")}
112 |
113 |
Result {$webhooks.data.webhooks.totalCount}
114 |
115 | 120 | 125 |
126 |
127 | {:else} 128 |
No Record
129 | {/if} 130 | {/if} 131 |
132 | 133 | 236 | -------------------------------------------------------------------------------- /app/shared/webhook.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | "net" 9 | "reflect" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | "unsafe" 14 | 15 | "github.com/adjust/rmq/v4" 16 | "github.com/avast/retry-go/v3" 17 | "github.com/go-playground/validator/v10" 18 | _ "github.com/go-sql-driver/mysql" 19 | "github.com/segmentio/ksuid" 20 | "github.com/si3nloong/webhook/app/entity" 21 | "github.com/si3nloong/webhook/app/mq/nats" 22 | "github.com/si3nloong/webhook/app/mq/redis" 23 | "github.com/si3nloong/webhook/cmd" 24 | pb "github.com/si3nloong/webhook/protobuf" 25 | "github.com/valyala/fasthttp" 26 | ) 27 | 28 | /* 29 | Flow ---------> 30 | send webhook ---> insert into Queue 31 | get from queue ---> fire webhook 32 | fire webhook ---> record stat (add success count or log error) 33 | */ 34 | 35 | type webhookServer struct { 36 | Repository 37 | v *validator.Validate 38 | mq MessageQueue 39 | } 40 | 41 | func NewServer(cfg *cmd.Config) WebhookServer { 42 | var ( 43 | err error 44 | svr = &webhookServer{ 45 | v: validator.New(), 46 | } 47 | ) 48 | 49 | // setup Database 50 | switch cfg.DB.Engine { 51 | case cmd.DatabaseEngineElasticsearch: 52 | // svr.Repository, err = es.New(cfg) 53 | default: 54 | // panic(fmt.Sprintf("invalid database engine %s", cfg.DB.Engine)) 55 | } 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | log.Println("Database engine =>", cfg.DB.Engine) 61 | log.Println("Message queue engine =>", cfg.MessageQueue.Engine) 62 | 63 | // setup Message Queueing 64 | switch cfg.MessageQueue.Engine { 65 | case cmd.MessageQueueEngineRedis: 66 | { 67 | svr.mq, err = redis.New(cfg, func(msg rmq.Delivery) { 68 | var ( 69 | data = entity.WebhookRequest{} 70 | errs error 71 | ) 72 | 73 | // capture error if exists when it's end 74 | defer func() { 75 | if errs != nil { 76 | svr.logErrorIfAny(errs) 77 | svr.logErrorIfAny(msg.Reject()) 78 | return 79 | } 80 | 81 | svr.logErrorIfAny(msg.Ack()) 82 | }() 83 | 84 | if errs = json.Unmarshal([]byte(msg.Payload()), &data); errs != nil { 85 | return 86 | } 87 | 88 | if errs = svr.fireWebhook(&data); errs != nil { 89 | return 90 | } 91 | }) 92 | } 93 | case cmd.MessageQueueEngineNSQ: 94 | { 95 | } 96 | case cmd.MessageQueueEngineNats: 97 | { 98 | svr.mq, err = nats.New(cfg) 99 | } 100 | default: 101 | // by default it will select in-memory pubsub 102 | } 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | return svr 108 | } 109 | 110 | func (s *webhookServer) Validate(src interface{}) error { 111 | return s.v.Struct(src) 112 | } 113 | 114 | func (s *webhookServer) VarCtx(ctx context.Context, src interface{}, tag string) error { 115 | return s.v.VarCtx(ctx, src, tag) 116 | } 117 | 118 | func (s *webhookServer) logErrorIfAny(err error) { 119 | if err != nil { 120 | s.LogError(err) 121 | } 122 | } 123 | 124 | func (*webhookServer) LogError(err error) { 125 | log.Println("Error", err) 126 | } 127 | 128 | func (s *webhookServer) Publish(ctx context.Context, req *pb.SendWebhookRequest) (*entity.WebhookRequest, error) { 129 | utcNow := time.Now().UTC() 130 | 131 | // Store the request to DB first before publishing it to message queue 132 | data := entity.WebhookRequest{} 133 | data.ID = ksuid.New() 134 | data.Method = req.Method.String() 135 | data.URL = req.Url 136 | data.Headers = make(map[string]string) 137 | for k, v := range req.Headers { 138 | data.Headers[k] = v 139 | } 140 | data.Body = req.Body 141 | data.Timeout = 3000 // 3 seconds 142 | if req.Timeout > 0 { 143 | data.Timeout = uint(req.Timeout) 144 | } 145 | data.Attempts = make([]entity.Attempt, 0) 146 | data.CreatedAt = utcNow 147 | data.UpdatedAt = utcNow 148 | 149 | if err := s.CreateWebhook(ctx, &data); err != nil { 150 | return nil, err 151 | } 152 | 153 | if err := s.mq.Publish(ctx, &data); err != nil { 154 | return nil, err 155 | } 156 | return &data, nil 157 | } 158 | 159 | func (s *webhookServer) fireWebhook(data *entity.WebhookRequest) error { 160 | ctx := context.TODO() 161 | startTime := time.Now().UTC() 162 | opts := make([]retry.Option, 0) 163 | 164 | log.Println("SendWebhook now....!!!") 165 | 166 | opts = append(opts, retry.Attempts(10)) 167 | // if req.RetryMechanism > 0 { 168 | opts = append(opts, retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration { 169 | log.Println("backoff retrying") 170 | log.Println(n, err, config) 171 | // fmt.Println("Server fails with: " + err.Error()) 172 | // if retriable, ok := err.(*retry.RetriableError); ok { 173 | // fmt.Printf("Client follows server recommendation to retry after %v\n", retriable.RetryAfter) 174 | // return retriable.RetryAfter 175 | // } 176 | 177 | // apply a default exponential back off strategy 178 | return retry.BackOffDelay(n, err, config) 179 | })) 180 | // } 181 | 182 | errs := retry.Do( 183 | func() error { 184 | httpReq := fasthttp.AcquireRequest() 185 | httpResp := fasthttp.AcquireResponse() 186 | defer fasthttp.ReleaseRequest(httpReq) 187 | defer fasthttp.ReleaseResponse(httpResp) 188 | httpReq.Header.SetRequestURI(data.URL) 189 | httpReq.Header.SetMethod(data.Method) 190 | 191 | for k, v := range data.Headers { 192 | httpReq.Header.Add(k, v) 193 | } 194 | httpReq.AppendBodyString(data.Body) 195 | 196 | // By default timeout is 3 seconds 197 | timeout := 3 * time.Second 198 | // if data.Timeout > 0 { 199 | // timeout = time.Second * time.Duration(req.Timeout) 200 | // } 201 | 202 | log.Println("Request =======>") 203 | log.Println(httpReq.String()) 204 | 205 | var dnsError *net.DNSError 206 | if err := fasthttp.DoTimeout(httpReq, httpResp, timeout); errors.As(err, &dnsError) { 207 | // If it's an invalid host, drop the request directly 208 | log.Println("Error 1 =======>", err) 209 | return retry.Unrecoverable(err) 210 | } else if err != nil { 211 | switch t := err.(type) { 212 | case *net.OpError: 213 | // if it's an unknown host, drop it 214 | if t.Op == "dial" { 215 | println("Unknown host") 216 | } else if t.Op == "read" { 217 | println("Connection refused") 218 | } 219 | 220 | case syscall.Errno: 221 | if t == syscall.ECONNREFUSED { 222 | println("Connection refused") 223 | } 224 | } 225 | log.Println("Error 2 =======>", err, reflect.TypeOf(err)) 226 | return err 227 | } 228 | 229 | log.Println("Response =======>") 230 | log.Println(httpResp.String()) 231 | statusCode := httpResp.StatusCode() 232 | var body string 233 | i64, _ := strconv.ParseInt(string(httpResp.Header.Peek("Content-Length")), 10, 64) 234 | // discard the response if body bigger than 1mb 235 | if i64 < 1048 { 236 | body = string(httpResp.Body()) 237 | } 238 | 239 | utcNow := time.Now().UTC() 240 | att := entity.Attempt{} 241 | att.Headers = make(map[string]string) 242 | httpResp.Header.VisitAll(func(key, value []byte) { 243 | att.Headers[b2s(key)] = b2s(value) 244 | }) 245 | att.Body = body 246 | att.ElapsedTime = time.Now().UTC().Sub(startTime).Milliseconds() 247 | att.StatusCode = uint(statusCode) 248 | att.CreatedAt = utcNow 249 | 250 | if err := s.UpdateWebhook(ctx, data.ID.String(), &att); err != nil { 251 | return err 252 | } 253 | 254 | // 100 - 199 255 | if statusCode < fasthttp.StatusOK { 256 | log.Println("100 - 199") 257 | // 500 258 | } else if statusCode >= fasthttp.StatusInternalServerError { 259 | log.Println("500") 260 | // return &requestError{body: httpResp.String()} 261 | // 400 262 | return errors.New("error 500") 263 | } else if statusCode >= fasthttp.StatusBadRequest { 264 | log.Println("400") 265 | // retry 266 | return errors.New("error 404") 267 | } 268 | 269 | return nil 270 | }, 271 | opts..., 272 | ) 273 | 274 | return errs 275 | } 276 | 277 | type requestError struct { 278 | body string 279 | } 280 | 281 | func (e requestError) Error() string { 282 | return "" 283 | } 284 | 285 | func b2s(b []byte) string { 286 | return *(*string)(unsafe.Pointer(&b)) 287 | } 288 | -------------------------------------------------------------------------------- /protobuf/webhook_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package protobuf 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // WebhookServiceClient is the client API for WebhookService service. 18 | // 19 | // 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. 20 | type WebhookServiceClient interface { 21 | Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) 22 | Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (WebhookService_WatchClient, error) 23 | ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) 24 | GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*GetWebhookResponse, error) 25 | SendWebhook(ctx context.Context, in *SendWebhookRequest, opts ...grpc.CallOption) (*SendWebhookResponse, error) 26 | } 27 | 28 | type webhookServiceClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewWebhookServiceClient(cc grpc.ClientConnInterface) WebhookServiceClient { 33 | return &webhookServiceClient{cc} 34 | } 35 | 36 | func (c *webhookServiceClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { 37 | out := new(HealthCheckResponse) 38 | err := c.cc.Invoke(ctx, "/protobuf.WebhookService/Check", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | func (c *webhookServiceClient) Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (WebhookService_WatchClient, error) { 46 | stream, err := c.cc.NewStream(ctx, &WebhookService_ServiceDesc.Streams[0], "/protobuf.WebhookService/Watch", opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | x := &webhookServiceWatchClient{stream} 51 | if err := x.ClientStream.SendMsg(in); err != nil { 52 | return nil, err 53 | } 54 | if err := x.ClientStream.CloseSend(); err != nil { 55 | return nil, err 56 | } 57 | return x, nil 58 | } 59 | 60 | type WebhookService_WatchClient interface { 61 | Recv() (*HealthCheckResponse, error) 62 | grpc.ClientStream 63 | } 64 | 65 | type webhookServiceWatchClient struct { 66 | grpc.ClientStream 67 | } 68 | 69 | func (x *webhookServiceWatchClient) Recv() (*HealthCheckResponse, error) { 70 | m := new(HealthCheckResponse) 71 | if err := x.ClientStream.RecvMsg(m); err != nil { 72 | return nil, err 73 | } 74 | return m, nil 75 | } 76 | 77 | func (c *webhookServiceClient) ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) { 78 | out := new(ListWebhooksResponse) 79 | err := c.cc.Invoke(ctx, "/protobuf.WebhookService/ListWebhooks", in, out, opts...) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return out, nil 84 | } 85 | 86 | func (c *webhookServiceClient) GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*GetWebhookResponse, error) { 87 | out := new(GetWebhookResponse) 88 | err := c.cc.Invoke(ctx, "/protobuf.WebhookService/GetWebhook", in, out, opts...) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return out, nil 93 | } 94 | 95 | func (c *webhookServiceClient) SendWebhook(ctx context.Context, in *SendWebhookRequest, opts ...grpc.CallOption) (*SendWebhookResponse, error) { 96 | out := new(SendWebhookResponse) 97 | err := c.cc.Invoke(ctx, "/protobuf.WebhookService/SendWebhook", in, out, opts...) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return out, nil 102 | } 103 | 104 | // WebhookServiceServer is the server API for WebhookService service. 105 | // All implementations must embed UnimplementedWebhookServiceServer 106 | // for forward compatibility 107 | type WebhookServiceServer interface { 108 | Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) 109 | Watch(*HealthCheckRequest, WebhookService_WatchServer) error 110 | ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) 111 | GetWebhook(context.Context, *GetWebhookRequest) (*GetWebhookResponse, error) 112 | SendWebhook(context.Context, *SendWebhookRequest) (*SendWebhookResponse, error) 113 | mustEmbedUnimplementedWebhookServiceServer() 114 | } 115 | 116 | // UnimplementedWebhookServiceServer must be embedded to have forward compatible implementations. 117 | type UnimplementedWebhookServiceServer struct { 118 | } 119 | 120 | func (UnimplementedWebhookServiceServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { 121 | return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") 122 | } 123 | func (UnimplementedWebhookServiceServer) Watch(*HealthCheckRequest, WebhookService_WatchServer) error { 124 | return status.Errorf(codes.Unimplemented, "method Watch not implemented") 125 | } 126 | func (UnimplementedWebhookServiceServer) ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) { 127 | return nil, status.Errorf(codes.Unimplemented, "method ListWebhooks not implemented") 128 | } 129 | func (UnimplementedWebhookServiceServer) GetWebhook(context.Context, *GetWebhookRequest) (*GetWebhookResponse, error) { 130 | return nil, status.Errorf(codes.Unimplemented, "method GetWebhook not implemented") 131 | } 132 | func (UnimplementedWebhookServiceServer) SendWebhook(context.Context, *SendWebhookRequest) (*SendWebhookResponse, error) { 133 | return nil, status.Errorf(codes.Unimplemented, "method SendWebhook not implemented") 134 | } 135 | func (UnimplementedWebhookServiceServer) mustEmbedUnimplementedWebhookServiceServer() {} 136 | 137 | // UnsafeWebhookServiceServer may be embedded to opt out of forward compatibility for this service. 138 | // Use of this interface is not recommended, as added methods to WebhookServiceServer will 139 | // result in compilation errors. 140 | type UnsafeWebhookServiceServer interface { 141 | mustEmbedUnimplementedWebhookServiceServer() 142 | } 143 | 144 | func RegisterWebhookServiceServer(s grpc.ServiceRegistrar, srv WebhookServiceServer) { 145 | s.RegisterService(&WebhookService_ServiceDesc, srv) 146 | } 147 | 148 | func _WebhookService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 149 | in := new(HealthCheckRequest) 150 | if err := dec(in); err != nil { 151 | return nil, err 152 | } 153 | if interceptor == nil { 154 | return srv.(WebhookServiceServer).Check(ctx, in) 155 | } 156 | info := &grpc.UnaryServerInfo{ 157 | Server: srv, 158 | FullMethod: "/protobuf.WebhookService/Check", 159 | } 160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 161 | return srv.(WebhookServiceServer).Check(ctx, req.(*HealthCheckRequest)) 162 | } 163 | return interceptor(ctx, in, info, handler) 164 | } 165 | 166 | func _WebhookService_Watch_Handler(srv interface{}, stream grpc.ServerStream) error { 167 | m := new(HealthCheckRequest) 168 | if err := stream.RecvMsg(m); err != nil { 169 | return err 170 | } 171 | return srv.(WebhookServiceServer).Watch(m, &webhookServiceWatchServer{stream}) 172 | } 173 | 174 | type WebhookService_WatchServer interface { 175 | Send(*HealthCheckResponse) error 176 | grpc.ServerStream 177 | } 178 | 179 | type webhookServiceWatchServer struct { 180 | grpc.ServerStream 181 | } 182 | 183 | func (x *webhookServiceWatchServer) Send(m *HealthCheckResponse) error { 184 | return x.ServerStream.SendMsg(m) 185 | } 186 | 187 | func _WebhookService_ListWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 188 | in := new(ListWebhooksRequest) 189 | if err := dec(in); err != nil { 190 | return nil, err 191 | } 192 | if interceptor == nil { 193 | return srv.(WebhookServiceServer).ListWebhooks(ctx, in) 194 | } 195 | info := &grpc.UnaryServerInfo{ 196 | Server: srv, 197 | FullMethod: "/protobuf.WebhookService/ListWebhooks", 198 | } 199 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 200 | return srv.(WebhookServiceServer).ListWebhooks(ctx, req.(*ListWebhooksRequest)) 201 | } 202 | return interceptor(ctx, in, info, handler) 203 | } 204 | 205 | func _WebhookService_GetWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 206 | in := new(GetWebhookRequest) 207 | if err := dec(in); err != nil { 208 | return nil, err 209 | } 210 | if interceptor == nil { 211 | return srv.(WebhookServiceServer).GetWebhook(ctx, in) 212 | } 213 | info := &grpc.UnaryServerInfo{ 214 | Server: srv, 215 | FullMethod: "/protobuf.WebhookService/GetWebhook", 216 | } 217 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 218 | return srv.(WebhookServiceServer).GetWebhook(ctx, req.(*GetWebhookRequest)) 219 | } 220 | return interceptor(ctx, in, info, handler) 221 | } 222 | 223 | func _WebhookService_SendWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 224 | in := new(SendWebhookRequest) 225 | if err := dec(in); err != nil { 226 | return nil, err 227 | } 228 | if interceptor == nil { 229 | return srv.(WebhookServiceServer).SendWebhook(ctx, in) 230 | } 231 | info := &grpc.UnaryServerInfo{ 232 | Server: srv, 233 | FullMethod: "/protobuf.WebhookService/SendWebhook", 234 | } 235 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 236 | return srv.(WebhookServiceServer).SendWebhook(ctx, req.(*SendWebhookRequest)) 237 | } 238 | return interceptor(ctx, in, info, handler) 239 | } 240 | 241 | // WebhookService_ServiceDesc is the grpc.ServiceDesc for WebhookService service. 242 | // It's only intended for direct use with grpc.RegisterService, 243 | // and not to be introspected or modified (even as a copy) 244 | var WebhookService_ServiceDesc = grpc.ServiceDesc{ 245 | ServiceName: "protobuf.WebhookService", 246 | HandlerType: (*WebhookServiceServer)(nil), 247 | Methods: []grpc.MethodDesc{ 248 | { 249 | MethodName: "Check", 250 | Handler: _WebhookService_Check_Handler, 251 | }, 252 | { 253 | MethodName: "ListWebhooks", 254 | Handler: _WebhookService_ListWebhooks_Handler, 255 | }, 256 | { 257 | MethodName: "GetWebhook", 258 | Handler: _WebhookService_GetWebhook_Handler, 259 | }, 260 | { 261 | MethodName: "SendWebhook", 262 | Handler: _WebhookService_SendWebhook_Handler, 263 | }, 264 | }, 265 | Streams: []grpc.StreamDesc{ 266 | { 267 | StreamName: "Watch", 268 | Handler: _WebhookService_Watch_Handler, 269 | ServerStreams: true, 270 | }, 271 | }, 272 | Metadata: "webhook.proto", 273 | } 274 | -------------------------------------------------------------------------------- /protobuf/webhook.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.3 5 | // source: webhook.proto 6 | 7 | package protobuf 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type HealthCheckResponse_ServingStatus int32 25 | 26 | const ( 27 | HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0 28 | HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 29 | HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 30 | HealthCheckResponse_SERVICE_UNKNOWN HealthCheckResponse_ServingStatus = 3 // Used only by the Watch method. 31 | ) 32 | 33 | // Enum value maps for HealthCheckResponse_ServingStatus. 34 | var ( 35 | HealthCheckResponse_ServingStatus_name = map[int32]string{ 36 | 0: "UNKNOWN", 37 | 1: "SERVING", 38 | 2: "NOT_SERVING", 39 | 3: "SERVICE_UNKNOWN", 40 | } 41 | HealthCheckResponse_ServingStatus_value = map[string]int32{ 42 | "UNKNOWN": 0, 43 | "SERVING": 1, 44 | "NOT_SERVING": 2, 45 | "SERVICE_UNKNOWN": 3, 46 | } 47 | ) 48 | 49 | func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus { 50 | p := new(HealthCheckResponse_ServingStatus) 51 | *p = x 52 | return p 53 | } 54 | 55 | func (x HealthCheckResponse_ServingStatus) String() string { 56 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 57 | } 58 | 59 | func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { 60 | return file_webhook_proto_enumTypes[0].Descriptor() 61 | } 62 | 63 | func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { 64 | return &file_webhook_proto_enumTypes[0] 65 | } 66 | 67 | func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { 68 | return protoreflect.EnumNumber(x) 69 | } 70 | 71 | // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. 72 | func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { 73 | return file_webhook_proto_rawDescGZIP(), []int{1, 0} 74 | } 75 | 76 | type SendWebhookRequest_HttpMethod int32 77 | 78 | const ( 79 | SendWebhookRequest_GET SendWebhookRequest_HttpMethod = 0 80 | SendWebhookRequest_POST SendWebhookRequest_HttpMethod = 1 81 | ) 82 | 83 | // Enum value maps for SendWebhookRequest_HttpMethod. 84 | var ( 85 | SendWebhookRequest_HttpMethod_name = map[int32]string{ 86 | 0: "GET", 87 | 1: "POST", 88 | } 89 | SendWebhookRequest_HttpMethod_value = map[string]int32{ 90 | "GET": 0, 91 | "POST": 1, 92 | } 93 | ) 94 | 95 | func (x SendWebhookRequest_HttpMethod) Enum() *SendWebhookRequest_HttpMethod { 96 | p := new(SendWebhookRequest_HttpMethod) 97 | *p = x 98 | return p 99 | } 100 | 101 | func (x SendWebhookRequest_HttpMethod) String() string { 102 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 103 | } 104 | 105 | func (SendWebhookRequest_HttpMethod) Descriptor() protoreflect.EnumDescriptor { 106 | return file_webhook_proto_enumTypes[1].Descriptor() 107 | } 108 | 109 | func (SendWebhookRequest_HttpMethod) Type() protoreflect.EnumType { 110 | return &file_webhook_proto_enumTypes[1] 111 | } 112 | 113 | func (x SendWebhookRequest_HttpMethod) Number() protoreflect.EnumNumber { 114 | return protoreflect.EnumNumber(x) 115 | } 116 | 117 | // Deprecated: Use SendWebhookRequest_HttpMethod.Descriptor instead. 118 | func (SendWebhookRequest_HttpMethod) EnumDescriptor() ([]byte, []int) { 119 | return file_webhook_proto_rawDescGZIP(), []int{6, 0} 120 | } 121 | 122 | type SendWebhookRequest_RetryStrategy int32 123 | 124 | const ( 125 | SendWebhookRequest_BACKOFF SendWebhookRequest_RetryStrategy = 0 126 | SendWebhookRequest_LINEAR SendWebhookRequest_RetryStrategy = 1 127 | ) 128 | 129 | // Enum value maps for SendWebhookRequest_RetryStrategy. 130 | var ( 131 | SendWebhookRequest_RetryStrategy_name = map[int32]string{ 132 | 0: "BACKOFF", 133 | 1: "LINEAR", 134 | } 135 | SendWebhookRequest_RetryStrategy_value = map[string]int32{ 136 | "BACKOFF": 0, 137 | "LINEAR": 1, 138 | } 139 | ) 140 | 141 | func (x SendWebhookRequest_RetryStrategy) Enum() *SendWebhookRequest_RetryStrategy { 142 | p := new(SendWebhookRequest_RetryStrategy) 143 | *p = x 144 | return p 145 | } 146 | 147 | func (x SendWebhookRequest_RetryStrategy) String() string { 148 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 149 | } 150 | 151 | func (SendWebhookRequest_RetryStrategy) Descriptor() protoreflect.EnumDescriptor { 152 | return file_webhook_proto_enumTypes[2].Descriptor() 153 | } 154 | 155 | func (SendWebhookRequest_RetryStrategy) Type() protoreflect.EnumType { 156 | return &file_webhook_proto_enumTypes[2] 157 | } 158 | 159 | func (x SendWebhookRequest_RetryStrategy) Number() protoreflect.EnumNumber { 160 | return protoreflect.EnumNumber(x) 161 | } 162 | 163 | // Deprecated: Use SendWebhookRequest_RetryStrategy.Descriptor instead. 164 | func (SendWebhookRequest_RetryStrategy) EnumDescriptor() ([]byte, []int) { 165 | return file_webhook_proto_rawDescGZIP(), []int{6, 1} 166 | } 167 | 168 | type HealthCheckRequest struct { 169 | state protoimpl.MessageState 170 | sizeCache protoimpl.SizeCache 171 | unknownFields protoimpl.UnknownFields 172 | 173 | Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` 174 | } 175 | 176 | func (x *HealthCheckRequest) Reset() { 177 | *x = HealthCheckRequest{} 178 | if protoimpl.UnsafeEnabled { 179 | mi := &file_webhook_proto_msgTypes[0] 180 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 181 | ms.StoreMessageInfo(mi) 182 | } 183 | } 184 | 185 | func (x *HealthCheckRequest) String() string { 186 | return protoimpl.X.MessageStringOf(x) 187 | } 188 | 189 | func (*HealthCheckRequest) ProtoMessage() {} 190 | 191 | func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { 192 | mi := &file_webhook_proto_msgTypes[0] 193 | if protoimpl.UnsafeEnabled && x != nil { 194 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 195 | if ms.LoadMessageInfo() == nil { 196 | ms.StoreMessageInfo(mi) 197 | } 198 | return ms 199 | } 200 | return mi.MessageOf(x) 201 | } 202 | 203 | // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. 204 | func (*HealthCheckRequest) Descriptor() ([]byte, []int) { 205 | return file_webhook_proto_rawDescGZIP(), []int{0} 206 | } 207 | 208 | func (x *HealthCheckRequest) GetService() string { 209 | if x != nil { 210 | return x.Service 211 | } 212 | return "" 213 | } 214 | 215 | type HealthCheckResponse struct { 216 | state protoimpl.MessageState 217 | sizeCache protoimpl.SizeCache 218 | unknownFields protoimpl.UnknownFields 219 | 220 | Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=protobuf.HealthCheckResponse_ServingStatus" json:"status,omitempty"` 221 | } 222 | 223 | func (x *HealthCheckResponse) Reset() { 224 | *x = HealthCheckResponse{} 225 | if protoimpl.UnsafeEnabled { 226 | mi := &file_webhook_proto_msgTypes[1] 227 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 228 | ms.StoreMessageInfo(mi) 229 | } 230 | } 231 | 232 | func (x *HealthCheckResponse) String() string { 233 | return protoimpl.X.MessageStringOf(x) 234 | } 235 | 236 | func (*HealthCheckResponse) ProtoMessage() {} 237 | 238 | func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { 239 | mi := &file_webhook_proto_msgTypes[1] 240 | if protoimpl.UnsafeEnabled && x != nil { 241 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 242 | if ms.LoadMessageInfo() == nil { 243 | ms.StoreMessageInfo(mi) 244 | } 245 | return ms 246 | } 247 | return mi.MessageOf(x) 248 | } 249 | 250 | // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. 251 | func (*HealthCheckResponse) Descriptor() ([]byte, []int) { 252 | return file_webhook_proto_rawDescGZIP(), []int{1} 253 | } 254 | 255 | func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { 256 | if x != nil { 257 | return x.Status 258 | } 259 | return HealthCheckResponse_UNKNOWN 260 | } 261 | 262 | type ListWebhooksRequest struct { 263 | state protoimpl.MessageState 264 | sizeCache protoimpl.SizeCache 265 | unknownFields protoimpl.UnknownFields 266 | 267 | // @gotags: validate:"omitempty,required" 268 | PageToken string `protobuf:"bytes,1,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty" validate:"omitempty,required"` 269 | // @gotags: validate:"omitempty,max=100" 270 | PageSize uint32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty" validate:"omitempty,max=100"` 271 | } 272 | 273 | func (x *ListWebhooksRequest) Reset() { 274 | *x = ListWebhooksRequest{} 275 | if protoimpl.UnsafeEnabled { 276 | mi := &file_webhook_proto_msgTypes[2] 277 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 278 | ms.StoreMessageInfo(mi) 279 | } 280 | } 281 | 282 | func (x *ListWebhooksRequest) String() string { 283 | return protoimpl.X.MessageStringOf(x) 284 | } 285 | 286 | func (*ListWebhooksRequest) ProtoMessage() {} 287 | 288 | func (x *ListWebhooksRequest) ProtoReflect() protoreflect.Message { 289 | mi := &file_webhook_proto_msgTypes[2] 290 | if protoimpl.UnsafeEnabled && x != nil { 291 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 292 | if ms.LoadMessageInfo() == nil { 293 | ms.StoreMessageInfo(mi) 294 | } 295 | return ms 296 | } 297 | return mi.MessageOf(x) 298 | } 299 | 300 | // Deprecated: Use ListWebhooksRequest.ProtoReflect.Descriptor instead. 301 | func (*ListWebhooksRequest) Descriptor() ([]byte, []int) { 302 | return file_webhook_proto_rawDescGZIP(), []int{2} 303 | } 304 | 305 | func (x *ListWebhooksRequest) GetPageToken() string { 306 | if x != nil { 307 | return x.PageToken 308 | } 309 | return "" 310 | } 311 | 312 | func (x *ListWebhooksRequest) GetPageSize() uint32 { 313 | if x != nil { 314 | return x.PageSize 315 | } 316 | return 0 317 | } 318 | 319 | type ListWebhooksResponse struct { 320 | state protoimpl.MessageState 321 | sizeCache protoimpl.SizeCache 322 | unknownFields protoimpl.UnknownFields 323 | 324 | Webhooks []*Webhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` 325 | NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` 326 | } 327 | 328 | func (x *ListWebhooksResponse) Reset() { 329 | *x = ListWebhooksResponse{} 330 | if protoimpl.UnsafeEnabled { 331 | mi := &file_webhook_proto_msgTypes[3] 332 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 333 | ms.StoreMessageInfo(mi) 334 | } 335 | } 336 | 337 | func (x *ListWebhooksResponse) String() string { 338 | return protoimpl.X.MessageStringOf(x) 339 | } 340 | 341 | func (*ListWebhooksResponse) ProtoMessage() {} 342 | 343 | func (x *ListWebhooksResponse) ProtoReflect() protoreflect.Message { 344 | mi := &file_webhook_proto_msgTypes[3] 345 | if protoimpl.UnsafeEnabled && x != nil { 346 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 347 | if ms.LoadMessageInfo() == nil { 348 | ms.StoreMessageInfo(mi) 349 | } 350 | return ms 351 | } 352 | return mi.MessageOf(x) 353 | } 354 | 355 | // Deprecated: Use ListWebhooksResponse.ProtoReflect.Descriptor instead. 356 | func (*ListWebhooksResponse) Descriptor() ([]byte, []int) { 357 | return file_webhook_proto_rawDescGZIP(), []int{3} 358 | } 359 | 360 | func (x *ListWebhooksResponse) GetWebhooks() []*Webhook { 361 | if x != nil { 362 | return x.Webhooks 363 | } 364 | return nil 365 | } 366 | 367 | func (x *ListWebhooksResponse) GetNextPageToken() string { 368 | if x != nil { 369 | return x.NextPageToken 370 | } 371 | return "" 372 | } 373 | 374 | type GetWebhookRequest struct { 375 | state protoimpl.MessageState 376 | sizeCache protoimpl.SizeCache 377 | unknownFields protoimpl.UnknownFields 378 | 379 | // @gotags: validate:"required" 380 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" validate:"required"` 381 | } 382 | 383 | func (x *GetWebhookRequest) Reset() { 384 | *x = GetWebhookRequest{} 385 | if protoimpl.UnsafeEnabled { 386 | mi := &file_webhook_proto_msgTypes[4] 387 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 388 | ms.StoreMessageInfo(mi) 389 | } 390 | } 391 | 392 | func (x *GetWebhookRequest) String() string { 393 | return protoimpl.X.MessageStringOf(x) 394 | } 395 | 396 | func (*GetWebhookRequest) ProtoMessage() {} 397 | 398 | func (x *GetWebhookRequest) ProtoReflect() protoreflect.Message { 399 | mi := &file_webhook_proto_msgTypes[4] 400 | if protoimpl.UnsafeEnabled && x != nil { 401 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 402 | if ms.LoadMessageInfo() == nil { 403 | ms.StoreMessageInfo(mi) 404 | } 405 | return ms 406 | } 407 | return mi.MessageOf(x) 408 | } 409 | 410 | // Deprecated: Use GetWebhookRequest.ProtoReflect.Descriptor instead. 411 | func (*GetWebhookRequest) Descriptor() ([]byte, []int) { 412 | return file_webhook_proto_rawDescGZIP(), []int{4} 413 | } 414 | 415 | func (x *GetWebhookRequest) GetId() string { 416 | if x != nil { 417 | return x.Id 418 | } 419 | return "" 420 | } 421 | 422 | type GetWebhookResponse struct { 423 | state protoimpl.MessageState 424 | sizeCache protoimpl.SizeCache 425 | unknownFields protoimpl.UnknownFields 426 | 427 | Webhook *Webhook `protobuf:"bytes,1,opt,name=webhook,proto3" json:"webhook,omitempty"` 428 | } 429 | 430 | func (x *GetWebhookResponse) Reset() { 431 | *x = GetWebhookResponse{} 432 | if protoimpl.UnsafeEnabled { 433 | mi := &file_webhook_proto_msgTypes[5] 434 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 435 | ms.StoreMessageInfo(mi) 436 | } 437 | } 438 | 439 | func (x *GetWebhookResponse) String() string { 440 | return protoimpl.X.MessageStringOf(x) 441 | } 442 | 443 | func (*GetWebhookResponse) ProtoMessage() {} 444 | 445 | func (x *GetWebhookResponse) ProtoReflect() protoreflect.Message { 446 | mi := &file_webhook_proto_msgTypes[5] 447 | if protoimpl.UnsafeEnabled && x != nil { 448 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 449 | if ms.LoadMessageInfo() == nil { 450 | ms.StoreMessageInfo(mi) 451 | } 452 | return ms 453 | } 454 | return mi.MessageOf(x) 455 | } 456 | 457 | // Deprecated: Use GetWebhookResponse.ProtoReflect.Descriptor instead. 458 | func (*GetWebhookResponse) Descriptor() ([]byte, []int) { 459 | return file_webhook_proto_rawDescGZIP(), []int{5} 460 | } 461 | 462 | func (x *GetWebhookResponse) GetWebhook() *Webhook { 463 | if x != nil { 464 | return x.Webhook 465 | } 466 | return nil 467 | } 468 | 469 | type SendWebhookRequest struct { 470 | state protoimpl.MessageState 471 | sizeCache protoimpl.SizeCache 472 | unknownFields protoimpl.UnknownFields 473 | 474 | Method SendWebhookRequest_HttpMethod `protobuf:"varint,1,opt,name=method,proto3,enum=protobuf.SendWebhookRequest_HttpMethod" json:"method,omitempty"` 475 | // @gotags: validate:"required,url,max=1000" 476 | Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty" validate:"required,url,max=1000"` 477 | Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 478 | // @gotags: validate:"max=2048" 479 | Body string `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty" validate:"max=2048"` 480 | // @gotags: validate:"omitempty,lte=10" 481 | Retry uint32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty" validate:"omitempty,lte=10"` 482 | RetryStrategy SendWebhookRequest_RetryStrategy `protobuf:"varint,6,opt,name=retry_strategy,json=retryStrategy,proto3,enum=protobuf.SendWebhookRequest_RetryStrategy" json:"retry_strategy,omitempty"` 483 | // @gotags: validate:"omitempty,lte=10000" 484 | Timeout uint32 `protobuf:"varint,7,opt,name=timeout,proto3" json:"timeout,omitempty" validate:"omitempty,lte=10000"` 485 | // @gotags: validate:"lte=3" 486 | Concurrent uint32 `protobuf:"varint,8,opt,name=concurrent,proto3" json:"concurrent,omitempty" validate:"lte=3"` 487 | } 488 | 489 | func (x *SendWebhookRequest) Reset() { 490 | *x = SendWebhookRequest{} 491 | if protoimpl.UnsafeEnabled { 492 | mi := &file_webhook_proto_msgTypes[6] 493 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 494 | ms.StoreMessageInfo(mi) 495 | } 496 | } 497 | 498 | func (x *SendWebhookRequest) String() string { 499 | return protoimpl.X.MessageStringOf(x) 500 | } 501 | 502 | func (*SendWebhookRequest) ProtoMessage() {} 503 | 504 | func (x *SendWebhookRequest) ProtoReflect() protoreflect.Message { 505 | mi := &file_webhook_proto_msgTypes[6] 506 | if protoimpl.UnsafeEnabled && x != nil { 507 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 508 | if ms.LoadMessageInfo() == nil { 509 | ms.StoreMessageInfo(mi) 510 | } 511 | return ms 512 | } 513 | return mi.MessageOf(x) 514 | } 515 | 516 | // Deprecated: Use SendWebhookRequest.ProtoReflect.Descriptor instead. 517 | func (*SendWebhookRequest) Descriptor() ([]byte, []int) { 518 | return file_webhook_proto_rawDescGZIP(), []int{6} 519 | } 520 | 521 | func (x *SendWebhookRequest) GetMethod() SendWebhookRequest_HttpMethod { 522 | if x != nil { 523 | return x.Method 524 | } 525 | return SendWebhookRequest_GET 526 | } 527 | 528 | func (x *SendWebhookRequest) GetUrl() string { 529 | if x != nil { 530 | return x.Url 531 | } 532 | return "" 533 | } 534 | 535 | func (x *SendWebhookRequest) GetHeaders() map[string]string { 536 | if x != nil { 537 | return x.Headers 538 | } 539 | return nil 540 | } 541 | 542 | func (x *SendWebhookRequest) GetBody() string { 543 | if x != nil { 544 | return x.Body 545 | } 546 | return "" 547 | } 548 | 549 | func (x *SendWebhookRequest) GetRetry() uint32 { 550 | if x != nil { 551 | return x.Retry 552 | } 553 | return 0 554 | } 555 | 556 | func (x *SendWebhookRequest) GetRetryStrategy() SendWebhookRequest_RetryStrategy { 557 | if x != nil { 558 | return x.RetryStrategy 559 | } 560 | return SendWebhookRequest_BACKOFF 561 | } 562 | 563 | func (x *SendWebhookRequest) GetTimeout() uint32 { 564 | if x != nil { 565 | return x.Timeout 566 | } 567 | return 0 568 | } 569 | 570 | func (x *SendWebhookRequest) GetConcurrent() uint32 { 571 | if x != nil { 572 | return x.Concurrent 573 | } 574 | return 0 575 | } 576 | 577 | type SendWebhookResponse struct { 578 | state protoimpl.MessageState 579 | sizeCache protoimpl.SizeCache 580 | unknownFields protoimpl.UnknownFields 581 | 582 | Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` 583 | } 584 | 585 | func (x *SendWebhookResponse) Reset() { 586 | *x = SendWebhookResponse{} 587 | if protoimpl.UnsafeEnabled { 588 | mi := &file_webhook_proto_msgTypes[7] 589 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 590 | ms.StoreMessageInfo(mi) 591 | } 592 | } 593 | 594 | func (x *SendWebhookResponse) String() string { 595 | return protoimpl.X.MessageStringOf(x) 596 | } 597 | 598 | func (*SendWebhookResponse) ProtoMessage() {} 599 | 600 | func (x *SendWebhookResponse) ProtoReflect() protoreflect.Message { 601 | mi := &file_webhook_proto_msgTypes[7] 602 | if protoimpl.UnsafeEnabled && x != nil { 603 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 604 | if ms.LoadMessageInfo() == nil { 605 | ms.StoreMessageInfo(mi) 606 | } 607 | return ms 608 | } 609 | return mi.MessageOf(x) 610 | } 611 | 612 | // Deprecated: Use SendWebhookResponse.ProtoReflect.Descriptor instead. 613 | func (*SendWebhookResponse) Descriptor() ([]byte, []int) { 614 | return file_webhook_proto_rawDescGZIP(), []int{7} 615 | } 616 | 617 | func (x *SendWebhookResponse) GetQuery() string { 618 | if x != nil { 619 | return x.Query 620 | } 621 | return "" 622 | } 623 | 624 | type Webhook struct { 625 | state protoimpl.MessageState 626 | sizeCache protoimpl.SizeCache 627 | unknownFields protoimpl.UnknownFields 628 | 629 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 630 | Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` 631 | Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` 632 | Retries uint32 `protobuf:"varint,4,opt,name=retries,proto3" json:"retries,omitempty"` 633 | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` 634 | UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` 635 | } 636 | 637 | func (x *Webhook) Reset() { 638 | *x = Webhook{} 639 | if protoimpl.UnsafeEnabled { 640 | mi := &file_webhook_proto_msgTypes[8] 641 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 642 | ms.StoreMessageInfo(mi) 643 | } 644 | } 645 | 646 | func (x *Webhook) String() string { 647 | return protoimpl.X.MessageStringOf(x) 648 | } 649 | 650 | func (*Webhook) ProtoMessage() {} 651 | 652 | func (x *Webhook) ProtoReflect() protoreflect.Message { 653 | mi := &file_webhook_proto_msgTypes[8] 654 | if protoimpl.UnsafeEnabled && x != nil { 655 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 656 | if ms.LoadMessageInfo() == nil { 657 | ms.StoreMessageInfo(mi) 658 | } 659 | return ms 660 | } 661 | return mi.MessageOf(x) 662 | } 663 | 664 | // Deprecated: Use Webhook.ProtoReflect.Descriptor instead. 665 | func (*Webhook) Descriptor() ([]byte, []int) { 666 | return file_webhook_proto_rawDescGZIP(), []int{8} 667 | } 668 | 669 | func (x *Webhook) GetId() string { 670 | if x != nil { 671 | return x.Id 672 | } 673 | return "" 674 | } 675 | 676 | func (x *Webhook) GetMethod() string { 677 | if x != nil { 678 | return x.Method 679 | } 680 | return "" 681 | } 682 | 683 | func (x *Webhook) GetBody() string { 684 | if x != nil { 685 | return x.Body 686 | } 687 | return "" 688 | } 689 | 690 | func (x *Webhook) GetRetries() uint32 { 691 | if x != nil { 692 | return x.Retries 693 | } 694 | return 0 695 | } 696 | 697 | func (x *Webhook) GetCreatedAt() *timestamppb.Timestamp { 698 | if x != nil { 699 | return x.CreatedAt 700 | } 701 | return nil 702 | } 703 | 704 | func (x *Webhook) GetUpdatedAt() *timestamppb.Timestamp { 705 | if x != nil { 706 | return x.UpdatedAt 707 | } 708 | return nil 709 | } 710 | 711 | var File_webhook_proto protoreflect.FileDescriptor 712 | 713 | var file_webhook_proto_rawDesc = []byte{ 714 | 0x0a, 0x0d, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 715 | 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 716 | 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 717 | 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 718 | 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 719 | 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 720 | 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 721 | 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 722 | 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 723 | 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x65, 724 | 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 725 | 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 726 | 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 727 | 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 728 | 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 729 | 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 730 | 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 731 | 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x51, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 732 | 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 733 | 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 734 | 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 735 | 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 736 | 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x6d, 0x0a, 0x14, 0x4c, 737 | 0x69, 0x73, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 738 | 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x18, 739 | 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 740 | 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x08, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 741 | 0x6b, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 742 | 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 743 | 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x23, 0x0a, 0x11, 0x47, 0x65, 744 | 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 745 | 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 746 | 0x41, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 747 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 748 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 749 | 0x66, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 750 | 0x6f, 0x6b, 0x22, 0xea, 0x03, 0x0a, 0x12, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 751 | 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 752 | 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x70, 0x72, 0x6f, 0x74, 753 | 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 754 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 755 | 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 756 | 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x43, 0x0a, 0x07, 757 | 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 758 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 759 | 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 760 | 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 761 | 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 762 | 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x74, 0x72, 0x79, 0x18, 0x05, 763 | 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x72, 0x65, 0x74, 0x72, 0x79, 0x12, 0x51, 0x0a, 0x0e, 0x72, 764 | 0x65, 0x74, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x06, 0x20, 765 | 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 766 | 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 767 | 0x74, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 768 | 0x0d, 0x72, 0x65, 0x74, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x18, 769 | 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 770 | 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x63, 771 | 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x63, 0x6f, 772 | 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x1a, 0x3a, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 773 | 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 774 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 775 | 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 776 | 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x4d, 0x65, 0x74, 0x68, 777 | 0x6f, 0x64, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x45, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 778 | 0x4f, 0x53, 0x54, 0x10, 0x01, 0x22, 0x28, 0x0a, 0x0d, 0x52, 0x65, 0x74, 0x72, 0x79, 0x53, 0x74, 779 | 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x41, 0x43, 0x4b, 0x4f, 0x46, 780 | 0x46, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x49, 0x4e, 0x45, 0x41, 0x52, 0x10, 0x01, 0x22, 781 | 0x2b, 0x0a, 0x13, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 782 | 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 783 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0xd5, 0x01, 0x0a, 784 | 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 785 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 786 | 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 787 | 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 788 | 0x62, 0x6f, 0x64, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 789 | 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x39, 790 | 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 791 | 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 792 | 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 793 | 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 794 | 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 795 | 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 796 | 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 797 | 0x65, 0x64, 0x41, 0x74, 0x32, 0x82, 0x03, 0x0a, 0x0e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 798 | 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 799 | 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x65, 0x61, 0x6c, 800 | 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 801 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 802 | 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 803 | 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 804 | 0x66, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 805 | 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 806 | 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 807 | 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x57, 0x65, 0x62, 808 | 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 809 | 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 810 | 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 811 | 0x4c, 0x69, 0x73, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 812 | 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 813 | 0x6f, 0x6b, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x47, 0x65, 814 | 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 815 | 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x47, 0x65, 0x74, 0x57, 0x65, 816 | 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 817 | 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x1c, 0x2e, 0x70, 818 | 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 819 | 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 820 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 821 | 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2f, 0x70, 0x72, 822 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 823 | } 824 | 825 | var ( 826 | file_webhook_proto_rawDescOnce sync.Once 827 | file_webhook_proto_rawDescData = file_webhook_proto_rawDesc 828 | ) 829 | 830 | func file_webhook_proto_rawDescGZIP() []byte { 831 | file_webhook_proto_rawDescOnce.Do(func() { 832 | file_webhook_proto_rawDescData = protoimpl.X.CompressGZIP(file_webhook_proto_rawDescData) 833 | }) 834 | return file_webhook_proto_rawDescData 835 | } 836 | 837 | var file_webhook_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 838 | var file_webhook_proto_msgTypes = make([]protoimpl.MessageInfo, 10) 839 | var file_webhook_proto_goTypes = []interface{}{ 840 | (HealthCheckResponse_ServingStatus)(0), // 0: protobuf.HealthCheckResponse.ServingStatus 841 | (SendWebhookRequest_HttpMethod)(0), // 1: protobuf.SendWebhookRequest.HttpMethod 842 | (SendWebhookRequest_RetryStrategy)(0), // 2: protobuf.SendWebhookRequest.RetryStrategy 843 | (*HealthCheckRequest)(nil), // 3: protobuf.HealthCheckRequest 844 | (*HealthCheckResponse)(nil), // 4: protobuf.HealthCheckResponse 845 | (*ListWebhooksRequest)(nil), // 5: protobuf.ListWebhooksRequest 846 | (*ListWebhooksResponse)(nil), // 6: protobuf.ListWebhooksResponse 847 | (*GetWebhookRequest)(nil), // 7: protobuf.GetWebhookRequest 848 | (*GetWebhookResponse)(nil), // 8: protobuf.GetWebhookResponse 849 | (*SendWebhookRequest)(nil), // 9: protobuf.SendWebhookRequest 850 | (*SendWebhookResponse)(nil), // 10: protobuf.SendWebhookResponse 851 | (*Webhook)(nil), // 11: protobuf.Webhook 852 | nil, // 12: protobuf.SendWebhookRequest.HeadersEntry 853 | (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp 854 | } 855 | var file_webhook_proto_depIdxs = []int32{ 856 | 0, // 0: protobuf.HealthCheckResponse.status:type_name -> protobuf.HealthCheckResponse.ServingStatus 857 | 11, // 1: protobuf.ListWebhooksResponse.webhooks:type_name -> protobuf.Webhook 858 | 11, // 2: protobuf.GetWebhookResponse.webhook:type_name -> protobuf.Webhook 859 | 1, // 3: protobuf.SendWebhookRequest.method:type_name -> protobuf.SendWebhookRequest.HttpMethod 860 | 12, // 4: protobuf.SendWebhookRequest.headers:type_name -> protobuf.SendWebhookRequest.HeadersEntry 861 | 2, // 5: protobuf.SendWebhookRequest.retry_strategy:type_name -> protobuf.SendWebhookRequest.RetryStrategy 862 | 13, // 6: protobuf.Webhook.created_at:type_name -> google.protobuf.Timestamp 863 | 13, // 7: protobuf.Webhook.updated_at:type_name -> google.protobuf.Timestamp 864 | 3, // 8: protobuf.WebhookService.Check:input_type -> protobuf.HealthCheckRequest 865 | 3, // 9: protobuf.WebhookService.Watch:input_type -> protobuf.HealthCheckRequest 866 | 5, // 10: protobuf.WebhookService.ListWebhooks:input_type -> protobuf.ListWebhooksRequest 867 | 7, // 11: protobuf.WebhookService.GetWebhook:input_type -> protobuf.GetWebhookRequest 868 | 9, // 12: protobuf.WebhookService.SendWebhook:input_type -> protobuf.SendWebhookRequest 869 | 4, // 13: protobuf.WebhookService.Check:output_type -> protobuf.HealthCheckResponse 870 | 4, // 14: protobuf.WebhookService.Watch:output_type -> protobuf.HealthCheckResponse 871 | 6, // 15: protobuf.WebhookService.ListWebhooks:output_type -> protobuf.ListWebhooksResponse 872 | 8, // 16: protobuf.WebhookService.GetWebhook:output_type -> protobuf.GetWebhookResponse 873 | 10, // 17: protobuf.WebhookService.SendWebhook:output_type -> protobuf.SendWebhookResponse 874 | 13, // [13:18] is the sub-list for method output_type 875 | 8, // [8:13] is the sub-list for method input_type 876 | 8, // [8:8] is the sub-list for extension type_name 877 | 8, // [8:8] is the sub-list for extension extendee 878 | 0, // [0:8] is the sub-list for field type_name 879 | } 880 | 881 | func init() { file_webhook_proto_init() } 882 | func file_webhook_proto_init() { 883 | if File_webhook_proto != nil { 884 | return 885 | } 886 | if !protoimpl.UnsafeEnabled { 887 | file_webhook_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 888 | switch v := v.(*HealthCheckRequest); i { 889 | case 0: 890 | return &v.state 891 | case 1: 892 | return &v.sizeCache 893 | case 2: 894 | return &v.unknownFields 895 | default: 896 | return nil 897 | } 898 | } 899 | file_webhook_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 900 | switch v := v.(*HealthCheckResponse); i { 901 | case 0: 902 | return &v.state 903 | case 1: 904 | return &v.sizeCache 905 | case 2: 906 | return &v.unknownFields 907 | default: 908 | return nil 909 | } 910 | } 911 | file_webhook_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 912 | switch v := v.(*ListWebhooksRequest); i { 913 | case 0: 914 | return &v.state 915 | case 1: 916 | return &v.sizeCache 917 | case 2: 918 | return &v.unknownFields 919 | default: 920 | return nil 921 | } 922 | } 923 | file_webhook_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 924 | switch v := v.(*ListWebhooksResponse); i { 925 | case 0: 926 | return &v.state 927 | case 1: 928 | return &v.sizeCache 929 | case 2: 930 | return &v.unknownFields 931 | default: 932 | return nil 933 | } 934 | } 935 | file_webhook_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 936 | switch v := v.(*GetWebhookRequest); i { 937 | case 0: 938 | return &v.state 939 | case 1: 940 | return &v.sizeCache 941 | case 2: 942 | return &v.unknownFields 943 | default: 944 | return nil 945 | } 946 | } 947 | file_webhook_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 948 | switch v := v.(*GetWebhookResponse); i { 949 | case 0: 950 | return &v.state 951 | case 1: 952 | return &v.sizeCache 953 | case 2: 954 | return &v.unknownFields 955 | default: 956 | return nil 957 | } 958 | } 959 | file_webhook_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 960 | switch v := v.(*SendWebhookRequest); i { 961 | case 0: 962 | return &v.state 963 | case 1: 964 | return &v.sizeCache 965 | case 2: 966 | return &v.unknownFields 967 | default: 968 | return nil 969 | } 970 | } 971 | file_webhook_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 972 | switch v := v.(*SendWebhookResponse); i { 973 | case 0: 974 | return &v.state 975 | case 1: 976 | return &v.sizeCache 977 | case 2: 978 | return &v.unknownFields 979 | default: 980 | return nil 981 | } 982 | } 983 | file_webhook_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 984 | switch v := v.(*Webhook); i { 985 | case 0: 986 | return &v.state 987 | case 1: 988 | return &v.sizeCache 989 | case 2: 990 | return &v.unknownFields 991 | default: 992 | return nil 993 | } 994 | } 995 | } 996 | type x struct{} 997 | out := protoimpl.TypeBuilder{ 998 | File: protoimpl.DescBuilder{ 999 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 1000 | RawDescriptor: file_webhook_proto_rawDesc, 1001 | NumEnums: 3, 1002 | NumMessages: 10, 1003 | NumExtensions: 0, 1004 | NumServices: 1, 1005 | }, 1006 | GoTypes: file_webhook_proto_goTypes, 1007 | DependencyIndexes: file_webhook_proto_depIdxs, 1008 | EnumInfos: file_webhook_proto_enumTypes, 1009 | MessageInfos: file_webhook_proto_msgTypes, 1010 | }.Build() 1011 | File_webhook_proto = out.File 1012 | file_webhook_proto_rawDesc = nil 1013 | file_webhook_proto_goTypes = nil 1014 | file_webhook_proto_depIdxs = nil 1015 | } 1016 | --------------------------------------------------------------------------------