Commit 3b0c8759 authored by Steven's avatar Steven

refactor: webhook service

parent c9ab03e1
...@@ -2,9 +2,12 @@ syntax = "proto3"; ...@@ -2,9 +2,12 @@ syntax = "proto3";
package memos.api.v1; package memos.api.v1;
import "api/v1/common.proto";
import "api/v1/memo_service.proto"; import "api/v1/memo_service.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/client.proto"; import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/resource.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto"; import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
...@@ -12,91 +15,191 @@ import "google/protobuf/timestamp.proto"; ...@@ -12,91 +15,191 @@ import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v1"; option go_package = "gen/api/v1";
service WebhookService { service WebhookService {
// ListWebhooks returns a list of webhooks.
rpc ListWebhooks(ListWebhooksRequest) returns (ListWebhooksResponse) {
option (google.api.http) = {get: "/api/v1/webhooks"};
}
// GetWebhook gets a webhook by name.
rpc GetWebhook(GetWebhookRequest) returns (Webhook) {
option (google.api.http) = {get: "/api/v1/{name=webhooks/*}"};
option (google.api.method_signature) = "name";
}
// CreateWebhook creates a new webhook. // CreateWebhook creates a new webhook.
rpc CreateWebhook(CreateWebhookRequest) returns (Webhook) { rpc CreateWebhook(CreateWebhookRequest) returns (Webhook) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/v1/webhooks" post: "/api/v1/webhooks"
body: "*" body: "webhook"
}; };
option (google.api.method_signature) = "webhook";
} }
// GetWebhook returns a webhook by id.
rpc GetWebhook(GetWebhookRequest) returns (Webhook) {
option (google.api.http) = {get: "/api/v1/webhooks/{id}"};
option (google.api.method_signature) = "id";
}
// ListWebhooks returns a list of webhooks.
rpc ListWebhooks(ListWebhooksRequest) returns (ListWebhooksResponse) {
option (google.api.http) = {get: "/api/v1/webhooks"};
}
// UpdateWebhook updates a webhook. // UpdateWebhook updates a webhook.
rpc UpdateWebhook(UpdateWebhookRequest) returns (Webhook) { rpc UpdateWebhook(UpdateWebhookRequest) returns (Webhook) {
option (google.api.http) = { option (google.api.http) = {
patch: "/api/v1/webhooks/{webhook.id}" patch: "/api/v1/{webhook.name=webhooks/*}"
body: "webhook" body: "webhook"
}; };
option (google.api.method_signature) = "webhook,update_mask"; option (google.api.method_signature) = "webhook,update_mask";
} }
// DeleteWebhook deletes a webhook by id.
// DeleteWebhook deletes a webhook.
rpc DeleteWebhook(DeleteWebhookRequest) returns (google.protobuf.Empty) { rpc DeleteWebhook(DeleteWebhookRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/webhooks/{id}"}; option (google.api.http) = {delete: "/api/v1/{name=webhooks/*}"};
option (google.api.method_signature) = "id"; option (google.api.method_signature) = "name";
} }
} }
message Webhook { message Webhook {
int32 id = 1; option (google.api.resource) = {
type: "memos.api.v1/Webhook"
pattern: "webhooks/{webhook}"
name_field: "name"
singular: "webhook"
plural: "webhooks"
};
// The name of the creator. // The resource name of the webhook.
string creator = 2; // Format: webhooks/{webhook}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
google.protobuf.Timestamp create_time = 3; // Output only. The system generated unique identifier.
string uid = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
google.protobuf.Timestamp update_time = 4; // Required. The display name of the webhook.
string display_name = 3 [(google.api.field_behavior) = REQUIRED];
string name = 5; // Required. The target URL for the webhook.
string url = 4 [(google.api.field_behavior) = REQUIRED];
string url = 6; // Output only. The resource name of the creator.
} // Format: users/{user}
string creator = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
message CreateWebhookRequest { // The state of the webhook.
string name = 1; State state = 6 [(google.api.field_behavior) = REQUIRED];
string url = 2; // Output only. The creation timestamp.
} google.protobuf.Timestamp create_time = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
message GetWebhookRequest { // Output only. The last update timestamp.
int32 id = 1; google.protobuf.Timestamp update_time = 8 [(google.api.field_behavior) = OUTPUT_ONLY];
// Output only. The etag for this resource.
string etag = 9 [(google.api.field_behavior) = OUTPUT_ONLY];
} }
message ListWebhooksRequest { message ListWebhooksRequest {
// The name of the creator. // Optional. The maximum number of webhooks to return.
string creator = 2; // The service may return fewer than this value.
// If unspecified, at most 50 webhooks will be returned.
// The maximum value is 1000; values above 1000 will be coerced to 1000.
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];
// Optional. A page token, received from a previous `ListWebhooks` call.
// Provide this to retrieve the subsequent page.
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filter to apply to the list results.
// Example: "state=ACTIVE" or "creator=users/123"
// Supported operators: =, !=, <, <=, >, >=, :
// Supported fields: display_name, url, creator, state, create_time, update_time
string filter = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. The order to sort results by.
// Example: "create_time desc" or "display_name asc"
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
// Optional. If true, show deleted webhooks in the response.
bool show_deleted = 5 [(google.api.field_behavior) = OPTIONAL];
} }
message ListWebhooksResponse { message ListWebhooksResponse {
// The list of webhooks.
repeated Webhook webhooks = 1; repeated Webhook webhooks = 1;
// A token that can be sent as `page_token` to retrieve the next page.
// If this field is omitted, there are no subsequent pages.
string next_page_token = 2;
// The total count of webhooks (may be approximate).
int32 total_size = 3;
}
message GetWebhookRequest {
// Required. The resource name of the webhook.
// Format: webhooks/{webhook}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Webhook"}
];
// Optional. The fields to return in the response.
// If not specified, all fields are returned.
google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL];
}
message CreateWebhookRequest {
// Required. The webhook to create.
Webhook webhook = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.field_behavior) = INPUT_ONLY
];
// Optional. The webhook ID to use for this webhook.
// If empty, a unique ID will be generated.
// Must match the pattern [a-z0-9-]+
string webhook_id = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. If set, validate the request but don't actually create the webhook.
bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. An idempotency token that can be used to ensure that multiple
// requests to create a webhook have the same result.
string request_id = 4 [(google.api.field_behavior) = OPTIONAL];
} }
message UpdateWebhookRequest { message UpdateWebhookRequest {
Webhook webhook = 1; // Required. The webhook to update.
Webhook webhook = 1 [(google.api.field_behavior) = REQUIRED];
// Required. The list of fields to update.
google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];
google.protobuf.FieldMask update_mask = 2; // Optional. If set to true, allows updating sensitive fields.
bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];
} }
message DeleteWebhookRequest { message DeleteWebhookRequest {
int32 id = 1; // Required. The resource name of the webhook to delete.
// Format: webhooks/{webhook}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Webhook"}
];
// Optional. If set to true, the webhook will be deleted even if it has associated data.
bool force = 2 [(google.api.field_behavior) = OPTIONAL];
} }
message WebhookRequestPayload { message WebhookRequestPayload {
string url = 1; // The target URL for the webhook request.
string url = 1 [(google.api.field_behavior) = REQUIRED];
string activity_type = 2; // The type of activity that triggered this webhook.
string activity_type = 2 [(google.api.field_behavior) = REQUIRED];
// The name of the creator. // The resource name of the creator.
// Format: users/{user} // Format: users/{user}
string creator = 3; string creator = 3 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
google.protobuf.Timestamp create_time = 4; // The creation timestamp of the activity.
google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
Memo memo = 5; // The memo that triggered this webhook (if applicable).
Memo memo = 5 [(google.api.field_behavior) = OPTIONAL];
} }
This diff is collapsed.
This diff is collapsed.
...@@ -20,9 +20,9 @@ import ( ...@@ -20,9 +20,9 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
WebhookService_CreateWebhook_FullMethodName = "/memos.api.v1.WebhookService/CreateWebhook"
WebhookService_GetWebhook_FullMethodName = "/memos.api.v1.WebhookService/GetWebhook"
WebhookService_ListWebhooks_FullMethodName = "/memos.api.v1.WebhookService/ListWebhooks" WebhookService_ListWebhooks_FullMethodName = "/memos.api.v1.WebhookService/ListWebhooks"
WebhookService_GetWebhook_FullMethodName = "/memos.api.v1.WebhookService/GetWebhook"
WebhookService_CreateWebhook_FullMethodName = "/memos.api.v1.WebhookService/CreateWebhook"
WebhookService_UpdateWebhook_FullMethodName = "/memos.api.v1.WebhookService/UpdateWebhook" WebhookService_UpdateWebhook_FullMethodName = "/memos.api.v1.WebhookService/UpdateWebhook"
WebhookService_DeleteWebhook_FullMethodName = "/memos.api.v1.WebhookService/DeleteWebhook" WebhookService_DeleteWebhook_FullMethodName = "/memos.api.v1.WebhookService/DeleteWebhook"
) )
...@@ -31,15 +31,15 @@ const ( ...@@ -31,15 +31,15 @@ const (
// //
// 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. // 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.
type WebhookServiceClient interface { type WebhookServiceClient interface {
// CreateWebhook creates a new webhook.
CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error)
// GetWebhook returns a webhook by id.
GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*Webhook, error)
// ListWebhooks returns a list of webhooks. // ListWebhooks returns a list of webhooks.
ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error)
// GetWebhook gets a webhook by name.
GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*Webhook, error)
// CreateWebhook creates a new webhook.
CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error)
// UpdateWebhook updates a webhook. // UpdateWebhook updates a webhook.
UpdateWebhook(ctx context.Context, in *UpdateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) UpdateWebhook(ctx context.Context, in *UpdateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error)
// DeleteWebhook deletes a webhook by id. // DeleteWebhook deletes a webhook.
DeleteWebhook(ctx context.Context, in *DeleteWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DeleteWebhook(ctx context.Context, in *DeleteWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
} }
...@@ -51,10 +51,10 @@ func NewWebhookServiceClient(cc grpc.ClientConnInterface) WebhookServiceClient { ...@@ -51,10 +51,10 @@ func NewWebhookServiceClient(cc grpc.ClientConnInterface) WebhookServiceClient {
return &webhookServiceClient{cc} return &webhookServiceClient{cc}
} }
func (c *webhookServiceClient) CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) { func (c *webhookServiceClient) ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Webhook) out := new(ListWebhooksResponse)
err := c.cc.Invoke(ctx, WebhookService_CreateWebhook_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, WebhookService_ListWebhooks_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -71,10 +71,10 @@ func (c *webhookServiceClient) GetWebhook(ctx context.Context, in *GetWebhookReq ...@@ -71,10 +71,10 @@ func (c *webhookServiceClient) GetWebhook(ctx context.Context, in *GetWebhookReq
return out, nil return out, nil
} }
func (c *webhookServiceClient) ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) { func (c *webhookServiceClient) CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListWebhooksResponse) out := new(Webhook)
err := c.cc.Invoke(ctx, WebhookService_ListWebhooks_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, WebhookService_CreateWebhook_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -105,15 +105,15 @@ func (c *webhookServiceClient) DeleteWebhook(ctx context.Context, in *DeleteWebh ...@@ -105,15 +105,15 @@ func (c *webhookServiceClient) DeleteWebhook(ctx context.Context, in *DeleteWebh
// All implementations must embed UnimplementedWebhookServiceServer // All implementations must embed UnimplementedWebhookServiceServer
// for forward compatibility. // for forward compatibility.
type WebhookServiceServer interface { type WebhookServiceServer interface {
// CreateWebhook creates a new webhook.
CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error)
// GetWebhook returns a webhook by id.
GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error)
// ListWebhooks returns a list of webhooks. // ListWebhooks returns a list of webhooks.
ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error)
// GetWebhook gets a webhook by name.
GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error)
// CreateWebhook creates a new webhook.
CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error)
// UpdateWebhook updates a webhook. // UpdateWebhook updates a webhook.
UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error)
// DeleteWebhook deletes a webhook by id. // DeleteWebhook deletes a webhook.
DeleteWebhook(context.Context, *DeleteWebhookRequest) (*emptypb.Empty, error) DeleteWebhook(context.Context, *DeleteWebhookRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedWebhookServiceServer() mustEmbedUnimplementedWebhookServiceServer()
} }
...@@ -125,14 +125,14 @@ type WebhookServiceServer interface { ...@@ -125,14 +125,14 @@ type WebhookServiceServer interface {
// pointer dereference when methods are called. // pointer dereference when methods are called.
type UnimplementedWebhookServiceServer struct{} type UnimplementedWebhookServiceServer struct{}
func (UnimplementedWebhookServiceServer) CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error) { func (UnimplementedWebhookServiceServer) ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateWebhook not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListWebhooks not implemented")
} }
func (UnimplementedWebhookServiceServer) GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error) { func (UnimplementedWebhookServiceServer) GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetWebhook not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetWebhook not implemented")
} }
func (UnimplementedWebhookServiceServer) ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) { func (UnimplementedWebhookServiceServer) CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListWebhooks not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateWebhook not implemented")
} }
func (UnimplementedWebhookServiceServer) UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) { func (UnimplementedWebhookServiceServer) UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateWebhook not implemented") return nil, status.Errorf(codes.Unimplemented, "method UpdateWebhook not implemented")
...@@ -161,20 +161,20 @@ func RegisterWebhookServiceServer(s grpc.ServiceRegistrar, srv WebhookServiceSer ...@@ -161,20 +161,20 @@ func RegisterWebhookServiceServer(s grpc.ServiceRegistrar, srv WebhookServiceSer
s.RegisterService(&WebhookService_ServiceDesc, srv) s.RegisterService(&WebhookService_ServiceDesc, srv)
} }
func _WebhookService_CreateWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _WebhookService_ListWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateWebhookRequest) in := new(ListWebhooksRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(WebhookServiceServer).CreateWebhook(ctx, in) return srv.(WebhookServiceServer).ListWebhooks(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: WebhookService_CreateWebhook_FullMethodName, FullMethod: WebhookService_ListWebhooks_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebhookServiceServer).CreateWebhook(ctx, req.(*CreateWebhookRequest)) return srv.(WebhookServiceServer).ListWebhooks(ctx, req.(*ListWebhooksRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
...@@ -197,20 +197,20 @@ func _WebhookService_GetWebhook_Handler(srv interface{}, ctx context.Context, de ...@@ -197,20 +197,20 @@ func _WebhookService_GetWebhook_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _WebhookService_ListWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _WebhookService_CreateWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListWebhooksRequest) in := new(CreateWebhookRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(WebhookServiceServer).ListWebhooks(ctx, in) return srv.(WebhookServiceServer).CreateWebhook(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: WebhookService_ListWebhooks_FullMethodName, FullMethod: WebhookService_CreateWebhook_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebhookServiceServer).ListWebhooks(ctx, req.(*ListWebhooksRequest)) return srv.(WebhookServiceServer).CreateWebhook(ctx, req.(*CreateWebhookRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
...@@ -259,16 +259,16 @@ var WebhookService_ServiceDesc = grpc.ServiceDesc{ ...@@ -259,16 +259,16 @@ var WebhookService_ServiceDesc = grpc.ServiceDesc{
HandlerType: (*WebhookServiceServer)(nil), HandlerType: (*WebhookServiceServer)(nil),
Methods: []grpc.MethodDesc{ Methods: []grpc.MethodDesc{
{ {
MethodName: "CreateWebhook", MethodName: "ListWebhooks",
Handler: _WebhookService_CreateWebhook_Handler, Handler: _WebhookService_ListWebhooks_Handler,
}, },
{ {
MethodName: "GetWebhook", MethodName: "GetWebhook",
Handler: _WebhookService_GetWebhook_Handler, Handler: _WebhookService_GetWebhook_Handler,
}, },
{ {
MethodName: "ListWebhooks", MethodName: "CreateWebhook",
Handler: _WebhookService_ListWebhooks_Handler, Handler: _WebhookService_CreateWebhook_Handler,
}, },
{ {
MethodName: "UpdateWebhook", MethodName: "UpdateWebhook",
......
This diff is collapsed.
...@@ -17,6 +17,7 @@ const ( ...@@ -17,6 +17,7 @@ const (
InboxNamePrefix = "inboxes/" InboxNamePrefix = "inboxes/"
IdentityProviderNamePrefix = "identityProviders/" IdentityProviderNamePrefix = "identityProviders/"
ActivityNamePrefix = "activities/" ActivityNamePrefix = "activities/"
WebhookNamePrefix = "webhooks/"
) )
// GetNameParentTokens returns the tokens from a resource name. // GetNameParentTokens returns the tokens from a resource name.
...@@ -117,3 +118,16 @@ func ExtractActivityIDFromName(name string) (int32, error) { ...@@ -117,3 +118,16 @@ func ExtractActivityIDFromName(name string) (int32, error) {
} }
return id, nil return id, nil
} }
// ExtractWebhookIDFromName returns the webhook ID from a resource name.
func ExtractWebhookIDFromName(name string) (int32, error) {
tokens, err := GetNameParentTokens(name, WebhookNamePrefix)
if err != nil {
return 0, err
}
id, err := util.ConvertStringToInt32(tokens[0])
if err != nil {
return 0, errors.Errorf("invalid webhook ID %q", tokens[0])
}
return id, nil
}
...@@ -2,6 +2,7 @@ package v1 ...@@ -2,6 +2,7 @@ package v1
import ( import (
"context" "context"
"crypto/md5"
"fmt" "fmt"
"strings" "strings"
"time" "time"
...@@ -21,10 +22,21 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe ...@@ -21,10 +22,21 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
// TODO: Handle webhook_id, validate_only, and request_id fields
if request.ValidateOnly {
// Perform validation checks without actually creating the webhook
return &v1pb.Webhook{
DisplayName: request.Webhook.DisplayName,
Url: request.Webhook.Url,
Creator: fmt.Sprintf("users/%d", currentUser.ID),
State: request.Webhook.State,
}, nil
}
webhook, err := s.Store.CreateWebhook(ctx, &store.Webhook{ webhook, err := s.Store.CreateWebhook(ctx, &store.Webhook{
CreatorID: currentUser.ID, CreatorID: currentUser.ID,
Name: request.Name, Name: request.Webhook.DisplayName,
URL: strings.TrimSpace(request.Url), URL: strings.TrimSpace(request.Webhook.Url),
}) })
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err) return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err)
...@@ -32,14 +44,16 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe ...@@ -32,14 +44,16 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe
return convertWebhookFromStore(webhook), nil return convertWebhookFromStore(webhook), nil
} }
func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) { func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) {
creatorID, err := ExtractUserIDFromName(request.Creator) currentUser, err := s.GetCurrentUser(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid creator name: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
// TODO: Implement proper filtering, ordering, and pagination
// For now, list webhooks for the current user
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
CreatorID: &creatorID, CreatorID: &currentUser.ID,
}) })
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err) return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err)
...@@ -47,6 +61,7 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebho ...@@ -47,6 +61,7 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebho
response := &v1pb.ListWebhooksResponse{ response := &v1pb.ListWebhooksResponse{
Webhooks: []*v1pb.Webhook{}, Webhooks: []*v1pb.Webhook{},
TotalSize: int32(len(webhooks)),
} }
for _, webhook := range webhooks { for _, webhook := range webhooks {
response.Webhooks = append(response.Webhooks, convertWebhookFromStore(webhook)) response.Webhooks = append(response.Webhooks, convertWebhookFromStore(webhook))
...@@ -55,13 +70,18 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebho ...@@ -55,13 +70,18 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebho
} }
func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) { func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) {
webhookID, err := ExtractWebhookIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx) currentUser, err := s.GetCurrentUser(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
webhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{ webhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{
ID: &request.Id, ID: &webhookID,
CreatorID: &currentUser.ID, CreatorID: &currentUser.ID,
}) })
if err != nil { if err != nil {
...@@ -70,7 +90,12 @@ func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookR ...@@ -70,7 +90,12 @@ func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookR
if webhook == nil { if webhook == nil {
return nil, status.Errorf(codes.NotFound, "webhook not found") return nil, status.Errorf(codes.NotFound, "webhook not found")
} }
return convertWebhookFromStore(webhook), nil
webhookPb := convertWebhookFromStore(webhook)
// TODO: Implement read_mask field filtering
return webhookPb, nil
} }
func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) { func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) {
...@@ -78,13 +103,43 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe ...@@ -78,13 +103,43 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe
return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") return nil, status.Errorf(codes.InvalidArgument, "update_mask is required")
} }
update := &store.UpdateWebhook{} webhookID, err := ExtractWebhookIDFromName(request.Webhook.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
// Check if webhook exists and user has permission
existingWebhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{
ID: &webhookID,
CreatorID: &currentUser.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err)
}
if existingWebhook == nil {
if request.AllowMissing {
// Could create webhook if missing, but for now return not found
return nil, status.Errorf(codes.NotFound, "webhook not found")
}
return nil, status.Errorf(codes.NotFound, "webhook not found")
}
update := &store.UpdateWebhook{
ID: webhookID,
}
for _, field := range request.UpdateMask.Paths { for _, field := range request.UpdateMask.Paths {
switch field { switch field {
case "name": case "display_name":
update.Name = &request.Webhook.Name update.Name = &request.Webhook.DisplayName
case "url": case "url":
update.URL = &request.Webhook.Url update.URL = &request.Webhook.Url
default:
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
} }
} }
...@@ -96,8 +151,32 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe ...@@ -96,8 +151,32 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe
} }
func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) { func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) {
err := s.Store.DeleteWebhook(ctx, &store.DeleteWebhook{ webhookID, err := ExtractWebhookIDFromName(request.Name)
ID: request.Id, if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
// Check if webhook exists and user has permission
webhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{
ID: &webhookID,
CreatorID: &currentUser.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err)
}
if webhook == nil {
return nil, status.Errorf(codes.NotFound, "webhook not found")
}
// TODO: Handle force field properly
err = s.Store.DeleteWebhook(ctx, &store.DeleteWebhook{
ID: webhookID,
}) })
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete webhook, error: %+v", err) return nil, status.Errorf(codes.Internal, "failed to delete webhook, error: %+v", err)
...@@ -106,12 +185,19 @@ func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWe ...@@ -106,12 +185,19 @@ func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWe
} }
func convertWebhookFromStore(webhook *store.Webhook) *v1pb.Webhook { func convertWebhookFromStore(webhook *store.Webhook) *v1pb.Webhook {
// Generate etag using MD5 hash of webhook data
etag := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%d-%s-%s",
webhook.ID, webhook.UpdatedTs, webhook.Name, webhook.URL))))
return &v1pb.Webhook{ return &v1pb.Webhook{
Id: webhook.ID, Name: fmt.Sprintf("webhooks/%d", webhook.ID),
Uid: fmt.Sprintf("%d", webhook.ID),
DisplayName: webhook.Name,
Url: webhook.URL,
Creator: fmt.Sprintf("users/%d", webhook.CreatorID),
State: v1pb.State_NORMAL, // Default to NORMAL state for webhooks
CreateTime: timestamppb.New(time.Unix(webhook.CreatedTs, 0)), CreateTime: timestamppb.New(time.Unix(webhook.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(webhook.UpdatedTs, 0)), UpdateTime: timestamppb.New(time.Unix(webhook.UpdatedTs, 0)),
Creator: fmt.Sprintf("%s%d", UserNamePrefix, webhook.CreatorID), Etag: etag,
Name: webhook.Name,
Url: webhook.URL,
} }
} }
...@@ -8,34 +8,34 @@ import { useTranslate } from "@/utils/i18n"; ...@@ -8,34 +8,34 @@ import { useTranslate } from "@/utils/i18n";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
interface Props extends DialogProps { interface Props extends DialogProps {
webhookId?: number; webhookName?: string;
onConfirm: () => void; onConfirm: () => void;
} }
interface State { interface State {
name: string; displayName: string;
url: string; url: string;
} }
const CreateWebhookDialog: React.FC<Props> = (props: Props) => { const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
const { webhookId, destroy, onConfirm } = props; const { webhookName, destroy, onConfirm } = props;
const t = useTranslate(); const t = useTranslate();
const [state, setState] = useState({ const [state, setState] = useState({
name: "", displayName: "",
url: "", url: "",
}); });
const requestState = useLoading(false); const requestState = useLoading(false);
const isCreating = webhookId === undefined; const isCreating = webhookName === undefined;
useEffect(() => { useEffect(() => {
if (webhookId) { if (webhookName) {
webhookServiceClient webhookServiceClient
.getWebhook({ .getWebhook({
id: webhookId, name: webhookName,
}) })
.then((webhook) => { .then((webhook) => {
setState({ setState({
name: webhook.name, displayName: webhook.displayName,
url: webhook.url, url: webhook.url,
}); });
}); });
...@@ -51,7 +51,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => { ...@@ -51,7 +51,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({ setPartialState({
name: e.target.value, displayName: e.target.value,
}); });
}; };
...@@ -62,7 +62,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => { ...@@ -62,7 +62,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
}; };
const handleSaveBtnClick = async () => { const handleSaveBtnClick = async () => {
if (!state.name || !state.url) { if (!state.displayName || !state.url) {
toast.error(t("message.fill-all-required-fields")); toast.error(t("message.fill-all-required-fields"));
return; return;
} }
...@@ -70,17 +70,19 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => { ...@@ -70,17 +70,19 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
try { try {
if (isCreating) { if (isCreating) {
await webhookServiceClient.createWebhook({ await webhookServiceClient.createWebhook({
name: state.name, webhook: {
displayName: state.displayName,
url: state.url, url: state.url,
},
}); });
} else { } else {
await webhookServiceClient.updateWebhook({ await webhookServiceClient.updateWebhook({
webhook: { webhook: {
id: webhookId, name: webhookName,
name: state.name, displayName: state.displayName,
url: state.url, url: state.url,
}, },
updateMask: ["name", "url"], updateMask: ["display_name", "url"],
}); });
} }
...@@ -112,7 +114,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => { ...@@ -112,7 +114,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
className="w-full" className="w-full"
type="text" type="text"
placeholder={t("setting.webhook-section.create-dialog.an-easy-to-remember-name")} placeholder={t("setting.webhook-section.create-dialog.an-easy-to-remember-name")}
value={state.name} value={state.displayName}
onChange={handleTitleInputChange} onChange={handleTitleInputChange}
/> />
</div> </div>
......
...@@ -3,39 +3,35 @@ import { ExternalLinkIcon, TrashIcon } from "lucide-react"; ...@@ -3,39 +3,35 @@ import { ExternalLinkIcon, TrashIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { webhookServiceClient } from "@/grpcweb"; import { webhookServiceClient } from "@/grpcweb";
import useCurrentUser from "@/hooks/useCurrentUser";
import { Webhook } from "@/types/proto/api/v1/webhook_service"; import { Webhook } from "@/types/proto/api/v1/webhook_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import showCreateWebhookDialog from "../CreateWebhookDialog"; import showCreateWebhookDialog from "../CreateWebhookDialog";
const listWebhooks = async (user: string) => { const listWebhooks = async () => {
const { webhooks } = await webhookServiceClient.listWebhooks({ const { webhooks } = await webhookServiceClient.listWebhooks({});
creator: user,
});
return webhooks; return webhooks;
}; };
const WebhookSection = () => { const WebhookSection = () => {
const t = useTranslate(); const t = useTranslate();
const currentUser = useCurrentUser();
const [webhooks, setWebhooks] = useState<Webhook[]>([]); const [webhooks, setWebhooks] = useState<Webhook[]>([]);
useEffect(() => { useEffect(() => {
listWebhooks(currentUser.name).then((webhooks) => { listWebhooks().then((webhooks) => {
setWebhooks(webhooks); setWebhooks(webhooks);
}); });
}, []); }, []);
const handleCreateAccessTokenDialogConfirm = async () => { const handleCreateAccessTokenDialogConfirm = async () => {
const webhooks = await listWebhooks(currentUser.name); const webhooks = await listWebhooks();
setWebhooks(webhooks); setWebhooks(webhooks);
}; };
const handleDeleteWebhook = async (webhook: Webhook) => { const handleDeleteWebhook = async (webhook: Webhook) => {
const confirmed = window.confirm(`Are you sure to delete webhook \`${webhook.name}\`? You cannot undo this action.`); const confirmed = window.confirm(`Are you sure to delete webhook \`${webhook.displayName}\`? You cannot undo this action.`);
if (confirmed) { if (confirmed) {
await webhookServiceClient.deleteWebhook({ id: webhook.id }); await webhookServiceClient.deleteWebhook({ name: webhook.name });
setWebhooks(webhooks.filter((item) => item.id !== webhook.id)); setWebhooks(webhooks.filter((item) => item.name !== webhook.name));
} }
}; };
...@@ -77,8 +73,8 @@ const WebhookSection = () => { ...@@ -77,8 +73,8 @@ const WebhookSection = () => {
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-500"> <tbody className="divide-y divide-gray-200 dark:divide-gray-500">
{webhooks.map((webhook) => ( {webhooks.map((webhook) => (
<tr key={webhook.id}> <tr key={webhook.name}>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400">{webhook.name}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400">{webhook.displayName}</td>
<td className="max-w-[200px] px-3 py-2 text-sm text-gray-900 dark:text-gray-400 truncate" title={webhook.url}> <td className="max-w-[200px] px-3 py-2 text-sm text-gray-900 dark:text-gray-400 truncate" title={webhook.url}>
{webhook.url} {webhook.url}
</td> </td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment