diff --git a/plugin/webhook/webhook.go b/plugin/webhook/webhook.go index af1724ffe..28f650f9b 100644 --- a/plugin/webhook/webhook.go +++ b/plugin/webhook/webhook.go @@ -9,7 +9,6 @@ import ( "time" "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" v1pb "github.com/usememos/memos/proto/gen/api/v1" ) @@ -19,9 +18,20 @@ var ( timeout = 30 * time.Second ) +type WebhookRequestPayload struct { + // The target URL for the webhook request. + Url string `json:"url"` + // The type of activity that triggered this webhook. + ActivityType string `json:"activityType"` + // The resource name of the creator. Format: users/{user} + Creator string `json:"creator"` + // The memo that triggered this webhook (if applicable). + Memo *v1pb.Memo `json:"memo"` +} + // Post posts the message to webhook endpoint. -func Post(requestPayload *v1pb.WebhookRequestPayload) error { - body, err := protojson.Marshal(requestPayload) +func Post(requestPayload *WebhookRequestPayload) error { + body, err := json.Marshal(requestPayload) if err != nil { return errors.Wrapf(err, "failed to marshal webhook request to %s", requestPayload.Url) } @@ -67,7 +77,7 @@ func Post(requestPayload *v1pb.WebhookRequestPayload) error { // PostAsync posts the message to webhook endpoint asynchronously. // It spawns a new goroutine to handle the request and does not wait for the response. -func PostAsync(requestPayload *v1pb.WebhookRequestPayload) { +func PostAsync(requestPayload *WebhookRequestPayload) { go func() { if err := Post(requestPayload); err != nil { // Since we're in a goroutine, we can only log the error diff --git a/proto/api/v1/webhook_service.proto b/proto/api/v1/webhook_service.proto index 24b2fba98..e69d46a41 100644 --- a/proto/api/v1/webhook_service.proto +++ b/proto/api/v1/webhook_service.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package memos.api.v1; -import "api/v1/common.proto"; import "api/v1/memo_service.proto"; import "google/api/annotations.proto"; import "google/api/client.proto"; @@ -10,43 +9,43 @@ import "google/api/field_behavior.proto"; import "google/api/resource.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; -import "google/protobuf/timestamp.proto"; option go_package = "gen/api/v1"; service WebhookService { - // ListWebhooks returns a list of webhooks. + // ListWebhooks returns a list of webhooks for a user. rpc ListWebhooks(ListWebhooksRequest) returns (ListWebhooksResponse) { - option (google.api.http) = {get: "/api/v1/webhooks"}; + option (google.api.http) = {get: "/api/v1/{parent=users/*}/webhooks"}; + option (google.api.method_signature) = "parent"; } // GetWebhook gets a webhook by name. rpc GetWebhook(GetWebhookRequest) returns (Webhook) { - option (google.api.http) = {get: "/api/v1/{name=webhooks/*}"}; + option (google.api.http) = {get: "/api/v1/{name=users/*/webhooks/*}"}; option (google.api.method_signature) = "name"; } - // CreateWebhook creates a new webhook. + // CreateWebhook creates a new webhook for a user. rpc CreateWebhook(CreateWebhookRequest) returns (Webhook) { option (google.api.http) = { - post: "/api/v1/webhooks" + post: "/api/v1/{parent=users/*}/webhooks" body: "webhook" }; - option (google.api.method_signature) = "webhook"; + option (google.api.method_signature) = "parent,webhook"; } - // UpdateWebhook updates a webhook. + // UpdateWebhook updates a webhook for a user. rpc UpdateWebhook(UpdateWebhookRequest) returns (Webhook) { option (google.api.http) = { - patch: "/api/v1/{webhook.name=webhooks/*}" + patch: "/api/v1/{webhook.name=users/*/webhooks/*}" body: "webhook" }; option (google.api.method_signature) = "webhook,update_mask"; } - // DeleteWebhook deletes a webhook. + // DeleteWebhook deletes a webhook for a user. rpc DeleteWebhook(DeleteWebhookRequest) returns (google.protobuf.Empty) { - option (google.api.http) = {delete: "/api/v1/{name=webhooks/*}"}; + option (google.api.http) = {delete: "/api/v1/{name=users/*/webhooks/*}"}; option (google.api.method_signature) = "name"; } } @@ -54,46 +53,39 @@ service WebhookService { message Webhook { option (google.api.resource) = { type: "memos.api.v1/Webhook" - pattern: "webhooks/{webhook}" - name_field: "name" + pattern: "users/{user}/webhooks/{webhook}" singular: "webhook" plural: "webhooks" }; // The resource name of the webhook. - // Format: webhooks/{webhook} + // Format: users/{user}/webhooks/{webhook} string name = 1 [(google.api.field_behavior) = IDENTIFIER]; - // Required. The display name of the webhook. + // The display name of the webhook. string display_name = 2 [(google.api.field_behavior) = REQUIRED]; - // Required. The target URL for the webhook. + // The target URL for the webhook. string url = 3 [(google.api.field_behavior) = REQUIRED]; +} - // Output only. The resource name of the creator. +message ListWebhooksRequest { + // Required. The parent resource where webhooks are listed. // Format: users/{user} - string creator = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; - - // The state of the webhook. - State state = 5 [(google.api.field_behavior) = REQUIRED]; - - // Output only. The creation timestamp. - google.protobuf.Timestamp create_time = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; - - // Output only. The last update timestamp. - google.protobuf.Timestamp update_time = 7 [(google.api.field_behavior) = OUTPUT_ONLY]; + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {child_type: "memos.api.v1/Webhook"} + ]; } -message ListWebhooksRequest {} - message ListWebhooksResponse { // The list of webhooks. repeated Webhook webhooks = 1; } message GetWebhookRequest { - // Required. The resource name of the webhook. - // Format: webhooks/{webhook} + // Required. The resource name of the webhook to retrieve. + // Format: users/{user}/webhooks/{webhook} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/Webhook"} @@ -101,55 +93,33 @@ message GetWebhookRequest { } message CreateWebhookRequest { - // Required. The webhook to create. - Webhook webhook = 1 [ + // Required. The parent resource where this webhook will be created. + // Format: users/{user} + string parent = 1 [ (google.api.field_behavior) = REQUIRED, - (google.api.field_behavior) = INPUT_ONLY + (google.api.resource_reference) = {child_type: "memos.api.v1/Webhook"} ]; - // 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]; + // Required. The webhook to create. + Webhook webhook = 2 [(google.api.field_behavior) = REQUIRED]; - // Optional. If set, validate the request but don't actually create the webhook. + // Optional. If set, validate the request, but do not actually create the webhook. bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL]; } message UpdateWebhookRequest { - // Required. The webhook to update. + // Required. The webhook resource which replaces the resource on the server. 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]; + // Optional. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; } message DeleteWebhookRequest { // Required. The resource name of the webhook to delete. - // Format: webhooks/{webhook} + // Format: users/{user}/webhooks/{webhook} string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = {type: "memos.api.v1/Webhook"} ]; } - -message WebhookRequestPayload { - // The target URL for the webhook request. - string url = 1 [(google.api.field_behavior) = REQUIRED]; - - // The type of activity that triggered this webhook. - string activity_type = 2 [(google.api.field_behavior) = REQUIRED]; - - // The resource name of the creator. - // Format: users/{user} - string creator = 3 [ - (google.api.field_behavior) = OUTPUT_ONLY, - (google.api.resource_reference) = {type: "memos.api.v1/User"} - ]; - - // The creation timestamp of the activity. - google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; - - // The memo that triggered this webhook (if applicable). - Memo memo = 5 [(google.api.field_behavior) = OPTIONAL]; -} diff --git a/proto/gen/api/v1/webhook_service.pb.go b/proto/gen/api/v1/webhook_service.pb.go index e877b5769..dfa0730eb 100644 --- a/proto/gen/api/v1/webhook_service.pb.go +++ b/proto/gen/api/v1/webhook_service.pb.go @@ -12,7 +12,6 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -28,21 +27,12 @@ const ( type Webhook struct { state protoimpl.MessageState `protogen:"open.v1"` // The resource name of the webhook. - // Format: webhooks/{webhook} + // Format: users/{user}/webhooks/{webhook} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Required. The display name of the webhook. + // The display name of the webhook. DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - // Required. The target URL for the webhook. - Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` - // Output only. The resource name of the creator. - // Format: users/{user} - Creator string `protobuf:"bytes,4,opt,name=creator,proto3" json:"creator,omitempty"` - // The state of the webhook. - State State `protobuf:"varint,5,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` - // Output only. The creation timestamp. - CreateTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - // Output only. The last update timestamp. - UpdateTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + // The target URL for the webhook. + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -98,36 +88,11 @@ func (x *Webhook) GetUrl() string { return "" } -func (x *Webhook) GetCreator() string { - if x != nil { - return x.Creator - } - return "" -} - -func (x *Webhook) GetState() State { - if x != nil { - return x.State - } - return State_STATE_UNSPECIFIED -} - -func (x *Webhook) GetCreateTime() *timestamppb.Timestamp { - if x != nil { - return x.CreateTime - } - return nil -} - -func (x *Webhook) GetUpdateTime() *timestamppb.Timestamp { - if x != nil { - return x.UpdateTime - } - return nil -} - type ListWebhooksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where webhooks are listed. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -162,6 +127,13 @@ func (*ListWebhooksRequest) Descriptor() ([]byte, []int) { return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{1} } +func (x *ListWebhooksRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + type ListWebhooksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of webhooks. @@ -209,8 +181,8 @@ func (x *ListWebhooksResponse) GetWebhooks() []*Webhook { type GetWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Required. The resource name of the webhook. - // Format: webhooks/{webhook} + // Required. The resource name of the webhook to retrieve. + // Format: users/{user}/webhooks/{webhook} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -255,13 +227,12 @@ func (x *GetWebhookRequest) GetName() string { type CreateWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where this webhook will be created. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` // Required. The webhook to create. - Webhook *Webhook `protobuf:"bytes,1,opt,name=webhook,proto3" json:"webhook,omitempty"` - // Optional. The webhook ID to use for this webhook. - // If empty, a unique ID will be generated. - // Must match the pattern [a-z0-9-]+ - WebhookId string `protobuf:"bytes,2,opt,name=webhook_id,json=webhookId,proto3" json:"webhook_id,omitempty"` - // Optional. If set, validate the request but don't actually create the webhook. + Webhook *Webhook `protobuf:"bytes,2,opt,name=webhook,proto3" json:"webhook,omitempty"` + // Optional. If set, validate the request, but do not actually create the webhook. ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -297,18 +268,18 @@ func (*CreateWebhookRequest) Descriptor() ([]byte, []int) { return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{4} } -func (x *CreateWebhookRequest) GetWebhook() *Webhook { +func (x *CreateWebhookRequest) GetParent() string { if x != nil { - return x.Webhook + return x.Parent } - return nil + return "" } -func (x *CreateWebhookRequest) GetWebhookId() string { +func (x *CreateWebhookRequest) GetWebhook() *Webhook { if x != nil { - return x.WebhookId + return x.Webhook } - return "" + return nil } func (x *CreateWebhookRequest) GetValidateOnly() bool { @@ -320,9 +291,9 @@ func (x *CreateWebhookRequest) GetValidateOnly() bool { type UpdateWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Required. The webhook to update. + // Required. The webhook resource which replaces the resource on the server. Webhook *Webhook `protobuf:"bytes,1,opt,name=webhook,proto3" json:"webhook,omitempty"` - // Required. The list of fields to update. + // Optional. The list of fields to update. UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -375,7 +346,7 @@ func (x *UpdateWebhookRequest) GetUpdateMask() *fieldmaskpb.FieldMask { type DeleteWebhookRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Required. The resource name of the webhook to delete. - // Format: webhooks/{webhook} + // Format: users/{user}/webhooks/{webhook} Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -418,137 +389,41 @@ func (x *DeleteWebhookRequest) GetName() string { return "" } -type WebhookRequestPayload struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The target URL for the webhook request. - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - // The type of activity that triggered this webhook. - ActivityType string `protobuf:"bytes,2,opt,name=activity_type,json=activityType,proto3" json:"activity_type,omitempty"` - // The resource name of the creator. - // Format: users/{user} - Creator string `protobuf:"bytes,3,opt,name=creator,proto3" json:"creator,omitempty"` - // The creation timestamp of the activity. - CreateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` - // The memo that triggered this webhook (if applicable). - Memo *Memo `protobuf:"bytes,5,opt,name=memo,proto3" json:"memo,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *WebhookRequestPayload) Reset() { - *x = WebhookRequestPayload{} - mi := &file_api_v1_webhook_service_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *WebhookRequestPayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*WebhookRequestPayload) ProtoMessage() {} - -func (x *WebhookRequestPayload) ProtoReflect() protoreflect.Message { - mi := &file_api_v1_webhook_service_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use WebhookRequestPayload.ProtoReflect.Descriptor instead. -func (*WebhookRequestPayload) Descriptor() ([]byte, []int) { - return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{7} -} - -func (x *WebhookRequestPayload) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -func (x *WebhookRequestPayload) GetActivityType() string { - if x != nil { - return x.ActivityType - } - return "" -} - -func (x *WebhookRequestPayload) GetCreator() string { - if x != nil { - return x.Creator - } - return "" -} - -func (x *WebhookRequestPayload) GetCreateTime() *timestamppb.Timestamp { - if x != nil { - return x.CreateTime - } - return nil -} - -func (x *WebhookRequestPayload) GetMemo() *Memo { - if x != nil { - return x.Memo - } - return nil -} - var File_api_v1_webhook_service_proto protoreflect.FileDescriptor const file_api_v1_webhook_service_proto_rawDesc = "" + "\n" + - "\x1capi/v1/webhook_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x19api/v1/memo_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfc\x02\n" + + "\x1capi/v1/webhook_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/memo_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\xb0\x01\n" + "\aWebhook\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12&\n" + "\fdisplay_name\x18\x02 \x01(\tB\x03\xe0A\x02R\vdisplayName\x12\x15\n" + - "\x03url\x18\x03 \x01(\tB\x03\xe0A\x02R\x03url\x12\x1d\n" + - "\acreator\x18\x04 \x01(\tB\x03\xe0A\x03R\acreator\x12.\n" + - "\x05state\x18\x05 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x02R\x05state\x12@\n" + - "\vcreate_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + - "createTime\x12@\n" + - "\vupdate_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + - "updateTime:F\xeaAC\n" + - "\x14memos.api.v1/Webhook\x12\x12webhooks/{webhook}\x1a\x04name*\bwebhooks2\awebhook\"\x15\n" + - "\x13ListWebhooksRequest\"I\n" + + "\x03url\x18\x03 \x01(\tB\x03\xe0A\x02R\x03url:M\xeaAJ\n" + + "\x14memos.api.v1/Webhook\x12\x1fusers/{user}/webhooks/{webhook}*\bwebhooks2\awebhook\"K\n" + + "\x13ListWebhooksRequest\x124\n" + + "\x06parent\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\x12\x14memos.api.v1/WebhookR\x06parent\"I\n" + "\x14ListWebhooksResponse\x121\n" + "\bwebhooks\x18\x01 \x03(\v2\x15.memos.api.v1.WebhookR\bwebhooks\"E\n" + "\x11GetWebhookRequest\x120\n" + "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + - "\x14memos.api.v1/WebhookR\x04name\"\x9d\x01\n" + - "\x14CreateWebhookRequest\x127\n" + - "\awebhook\x18\x01 \x01(\v2\x15.memos.api.v1.WebhookB\x06\xe0A\x02\xe0A\x04R\awebhook\x12\"\n" + - "\n" + - "webhook_id\x18\x02 \x01(\tB\x03\xe0A\x01R\twebhookId\x12(\n" + + "\x14memos.api.v1/WebhookR\x04name\"\xac\x01\n" + + "\x14CreateWebhookRequest\x124\n" + + "\x06parent\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\x12\x14memos.api.v1/WebhookR\x06parent\x124\n" + + "\awebhook\x18\x02 \x01(\v2\x15.memos.api.v1.WebhookB\x03\xe0A\x02R\awebhook\x12(\n" + "\rvalidate_only\x18\x03 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\"\x8e\x01\n" + "\x14UpdateWebhookRequest\x124\n" + "\awebhook\x18\x01 \x01(\v2\x15.memos.api.v1.WebhookB\x03\xe0A\x02R\awebhook\x12@\n" + - "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + "updateMask\"H\n" + "\x14DeleteWebhookRequest\x120\n" + "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + - "\x14memos.api.v1/WebhookR\x04name\"\xfc\x01\n" + - "\x15WebhookRequestPayload\x12\x15\n" + - "\x03url\x18\x01 \x01(\tB\x03\xe0A\x02R\x03url\x12(\n" + - "\ractivity_type\x18\x02 \x01(\tB\x03\xe0A\x02R\factivityType\x123\n" + - "\acreator\x18\x03 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + - "\x11memos.api.v1/UserR\acreator\x12@\n" + - "\vcreate_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + - "createTime\x12+\n" + - "\x04memo\x18\x05 \x01(\v2\x12.memos.api.v1.MemoB\x03\xe0A\x01R\x04memo2\xf8\x04\n" + - "\x0eWebhookService\x12o\n" + - "\fListWebhooks\x12!.memos.api.v1.ListWebhooksRequest\x1a\".memos.api.v1.ListWebhooksResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/v1/webhooks\x12n\n" + + "\x14memos.api.v1/WebhookR\x04name2\xc4\x05\n" + + "\x0eWebhookService\x12\x89\x01\n" + + "\fListWebhooks\x12!.memos.api.v1.ListWebhooksRequest\x1a\".memos.api.v1.ListWebhooksResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/webhooks\x12v\n" + "\n" + - "GetWebhook\x12\x1f.memos.api.v1.GetWebhookRequest\x1a\x15.memos.api.v1.Webhook\"(\xdaA\x04name\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/{name=webhooks/*}\x12w\n" + - "\rCreateWebhook\x12\".memos.api.v1.CreateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"+\xdaA\awebhook\x82\xd3\xe4\x93\x02\x1b:\awebhook\"\x10/api/v1/webhooks\x12\x94\x01\n" + - "\rUpdateWebhook\x12\".memos.api.v1.UpdateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"H\xdaA\x13webhook,update_mask\x82\xd3\xe4\x93\x02,:\awebhook2!/api/v1/{webhook.name=webhooks/*}\x12u\n" + - "\rDeleteWebhook\x12\".memos.api.v1.DeleteWebhookRequest\x1a\x16.google.protobuf.Empty\"(\xdaA\x04name\x82\xd3\xe4\x93\x02\x1b*\x19/api/v1/{name=webhooks/*}B\xab\x01\n" + + "GetWebhook\x12\x1f.memos.api.v1.GetWebhookRequest\x1a\x15.memos.api.v1.Webhook\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*/webhooks/*}\x12\x8f\x01\n" + + "\rCreateWebhook\x12\".memos.api.v1.CreateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"C\xdaA\x0eparent,webhook\x82\xd3\xe4\x93\x02,:\awebhook\"!/api/v1/{parent=users/*}/webhooks\x12\x9c\x01\n" + + "\rUpdateWebhook\x12\".memos.api.v1.UpdateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"P\xdaA\x13webhook,update_mask\x82\xd3\xe4\x93\x024:\awebhook2)/api/v1/{webhook.name=users/*/webhooks/*}\x12}\n" + + "\rDeleteWebhook\x12\".memos.api.v1.DeleteWebhookRequest\x1a\x16.google.protobuf.Empty\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#*!/api/v1/{name=users/*/webhooks/*}B\xab\x01\n" + "\x10com.memos.api.v1B\x13WebhookServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" var ( @@ -563,7 +438,7 @@ func file_api_v1_webhook_service_proto_rawDescGZIP() []byte { return file_api_v1_webhook_service_proto_rawDescData } -var file_api_v1_webhook_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_api_v1_webhook_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_api_v1_webhook_service_proto_goTypes = []any{ (*Webhook)(nil), // 0: memos.api.v1.Webhook (*ListWebhooksRequest)(nil), // 1: memos.api.v1.ListWebhooksRequest @@ -572,38 +447,29 @@ var file_api_v1_webhook_service_proto_goTypes = []any{ (*CreateWebhookRequest)(nil), // 4: memos.api.v1.CreateWebhookRequest (*UpdateWebhookRequest)(nil), // 5: memos.api.v1.UpdateWebhookRequest (*DeleteWebhookRequest)(nil), // 6: memos.api.v1.DeleteWebhookRequest - (*WebhookRequestPayload)(nil), // 7: memos.api.v1.WebhookRequestPayload - (State)(0), // 8: memos.api.v1.State - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp - (*fieldmaskpb.FieldMask)(nil), // 10: google.protobuf.FieldMask - (*Memo)(nil), // 11: memos.api.v1.Memo - (*emptypb.Empty)(nil), // 12: google.protobuf.Empty + (*fieldmaskpb.FieldMask)(nil), // 7: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 8: google.protobuf.Empty } var file_api_v1_webhook_service_proto_depIdxs = []int32{ - 8, // 0: memos.api.v1.Webhook.state:type_name -> memos.api.v1.State - 9, // 1: memos.api.v1.Webhook.create_time:type_name -> google.protobuf.Timestamp - 9, // 2: memos.api.v1.Webhook.update_time:type_name -> google.protobuf.Timestamp - 0, // 3: memos.api.v1.ListWebhooksResponse.webhooks:type_name -> memos.api.v1.Webhook - 0, // 4: memos.api.v1.CreateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook - 0, // 5: memos.api.v1.UpdateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook - 10, // 6: memos.api.v1.UpdateWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask - 9, // 7: memos.api.v1.WebhookRequestPayload.create_time:type_name -> google.protobuf.Timestamp - 11, // 8: memos.api.v1.WebhookRequestPayload.memo:type_name -> memos.api.v1.Memo - 1, // 9: memos.api.v1.WebhookService.ListWebhooks:input_type -> memos.api.v1.ListWebhooksRequest - 3, // 10: memos.api.v1.WebhookService.GetWebhook:input_type -> memos.api.v1.GetWebhookRequest - 4, // 11: memos.api.v1.WebhookService.CreateWebhook:input_type -> memos.api.v1.CreateWebhookRequest - 5, // 12: memos.api.v1.WebhookService.UpdateWebhook:input_type -> memos.api.v1.UpdateWebhookRequest - 6, // 13: memos.api.v1.WebhookService.DeleteWebhook:input_type -> memos.api.v1.DeleteWebhookRequest - 2, // 14: memos.api.v1.WebhookService.ListWebhooks:output_type -> memos.api.v1.ListWebhooksResponse - 0, // 15: memos.api.v1.WebhookService.GetWebhook:output_type -> memos.api.v1.Webhook - 0, // 16: memos.api.v1.WebhookService.CreateWebhook:output_type -> memos.api.v1.Webhook - 0, // 17: memos.api.v1.WebhookService.UpdateWebhook:output_type -> memos.api.v1.Webhook - 12, // 18: memos.api.v1.WebhookService.DeleteWebhook:output_type -> google.protobuf.Empty - 14, // [14:19] is the sub-list for method output_type - 9, // [9:14] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 0, // 0: memos.api.v1.ListWebhooksResponse.webhooks:type_name -> memos.api.v1.Webhook + 0, // 1: memos.api.v1.CreateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook + 0, // 2: memos.api.v1.UpdateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook + 7, // 3: memos.api.v1.UpdateWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 4: memos.api.v1.WebhookService.ListWebhooks:input_type -> memos.api.v1.ListWebhooksRequest + 3, // 5: memos.api.v1.WebhookService.GetWebhook:input_type -> memos.api.v1.GetWebhookRequest + 4, // 6: memos.api.v1.WebhookService.CreateWebhook:input_type -> memos.api.v1.CreateWebhookRequest + 5, // 7: memos.api.v1.WebhookService.UpdateWebhook:input_type -> memos.api.v1.UpdateWebhookRequest + 6, // 8: memos.api.v1.WebhookService.DeleteWebhook:input_type -> memos.api.v1.DeleteWebhookRequest + 2, // 9: memos.api.v1.WebhookService.ListWebhooks:output_type -> memos.api.v1.ListWebhooksResponse + 0, // 10: memos.api.v1.WebhookService.GetWebhook:output_type -> memos.api.v1.Webhook + 0, // 11: memos.api.v1.WebhookService.CreateWebhook:output_type -> memos.api.v1.Webhook + 0, // 12: memos.api.v1.WebhookService.UpdateWebhook:output_type -> memos.api.v1.Webhook + 8, // 13: memos.api.v1.WebhookService.DeleteWebhook:output_type -> google.protobuf.Empty + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_api_v1_webhook_service_proto_init() } @@ -611,7 +477,6 @@ func file_api_v1_webhook_service_proto_init() { if File_api_v1_webhook_service_proto != nil { return } - file_api_v1_common_proto_init() file_api_v1_memo_service_proto_init() type x struct{} out := protoimpl.TypeBuilder{ @@ -619,7 +484,7 @@ func file_api_v1_webhook_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_webhook_service_proto_rawDesc), len(file_api_v1_webhook_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/gen/api/v1/webhook_service.pb.gw.go b/proto/gen/api/v1/webhook_service.pb.gw.go index 1b3572ca9..e8f938ea5 100644 --- a/proto/gen/api/v1/webhook_service.pb.gw.go +++ b/proto/gen/api/v1/webhook_service.pb.gw.go @@ -39,10 +39,19 @@ func request_WebhookService_ListWebhooks_0(ctx context.Context, marshaler runtim var ( protoReq ListWebhooksRequest metadata runtime.ServerMetadata + err error ) if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } msg, err := client.ListWebhooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } @@ -51,7 +60,16 @@ func local_request_WebhookService_ListWebhooks_0(ctx context.Context, marshaler var ( protoReq ListWebhooksRequest metadata runtime.ServerMetadata + err error ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } msg, err := server.ListWebhooks(ctx, &protoReq) return msg, metadata, err } @@ -95,12 +113,13 @@ func local_request_WebhookService_GetWebhook_0(ctx context.Context, marshaler ru return msg, metadata, err } -var filter_WebhookService_CreateWebhook_0 = &utilities.DoubleArray{Encoding: map[string]int{"webhook": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +var filter_WebhookService_CreateWebhook_0 = &utilities.DoubleArray{Encoding: map[string]int{"webhook": 0, "parent": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} func request_WebhookService_CreateWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq CreateWebhookRequest metadata runtime.ServerMetadata + err error ) if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) @@ -108,6 +127,14 @@ func request_WebhookService_CreateWebhook_0(ctx context.Context, marshaler runti if req.Body != nil { _, _ = io.Copy(io.Discard, req.Body) } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -122,10 +149,19 @@ func local_request_WebhookService_CreateWebhook_0(ctx context.Context, marshaler var ( protoReq CreateWebhookRequest metadata runtime.ServerMetadata + err error ) if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -268,7 +304,7 @@ func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.Serve var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/webhooks")) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -288,7 +324,7 @@ func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.Serve var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -308,7 +344,7 @@ func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.Serve var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/webhooks")) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -328,7 +364,7 @@ func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.Serve var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -348,7 +384,7 @@ func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.Serve var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -406,7 +442,7 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/webhooks")) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -423,7 +459,7 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -440,7 +476,7 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/webhooks")) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -457,7 +493,7 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -474,7 +510,7 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=webhooks/*}")) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -491,11 +527,11 @@ func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.Serve } var ( - pattern_WebhookService_ListWebhooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "webhooks"}, "")) - pattern_WebhookService_GetWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "webhooks", "name"}, "")) - pattern_WebhookService_CreateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "webhooks"}, "")) - pattern_WebhookService_UpdateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "webhooks", "webhook.name"}, "")) - pattern_WebhookService_DeleteWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "webhooks", "name"}, "")) + pattern_WebhookService_ListWebhooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, "")) + pattern_WebhookService_GetWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, "")) + pattern_WebhookService_CreateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, "")) + pattern_WebhookService_UpdateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "webhook.name"}, "")) + pattern_WebhookService_DeleteWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, "")) ) var ( diff --git a/proto/gen/api/v1/webhook_service_grpc.pb.go b/proto/gen/api/v1/webhook_service_grpc.pb.go index a59cce50e..e81076b7b 100644 --- a/proto/gen/api/v1/webhook_service_grpc.pb.go +++ b/proto/gen/api/v1/webhook_service_grpc.pb.go @@ -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. type WebhookServiceClient interface { - // ListWebhooks returns a list of webhooks. + // ListWebhooks returns a list of webhooks for a user. 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 creates a new webhook for a user. CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) - // UpdateWebhook updates a webhook. + // UpdateWebhook updates a webhook for a user. UpdateWebhook(ctx context.Context, in *UpdateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) - // DeleteWebhook deletes a webhook. + // DeleteWebhook deletes a webhook for a user. DeleteWebhook(ctx context.Context, in *DeleteWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } @@ -105,15 +105,15 @@ func (c *webhookServiceClient) DeleteWebhook(ctx context.Context, in *DeleteWebh // All implementations must embed UnimplementedWebhookServiceServer // for forward compatibility. type WebhookServiceServer interface { - // ListWebhooks returns a list of webhooks. + // ListWebhooks returns a list of webhooks for a user. ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) // GetWebhook gets a webhook by name. GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error) - // CreateWebhook creates a new webhook. + // CreateWebhook creates a new webhook for a user. CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error) - // UpdateWebhook updates a webhook. + // UpdateWebhook updates a webhook for a user. UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) - // DeleteWebhook deletes a webhook. + // DeleteWebhook deletes a webhook for a user. DeleteWebhook(context.Context, *DeleteWebhookRequest) (*emptypb.Empty, error) mustEmbedUnimplementedWebhookServiceServer() } diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml index 4b952d281..916e6c2fa 100644 --- a/proto/gen/apidocs.swagger.yaml +++ b/proto/gen/apidocs.swagger.yaml @@ -602,57 +602,6 @@ paths: type: string tags: - UserService - /api/v1/webhooks: - get: - summary: ListWebhooks returns a list of webhooks. - operationId: WebhookService_ListWebhooks - responses: - "200": - description: A successful response. - schema: - $ref: '#/definitions/v1ListWebhooksResponse' - default: - description: An unexpected error response. - schema: - $ref: '#/definitions/googlerpcStatus' - tags: - - WebhookService - post: - summary: CreateWebhook creates a new webhook. - operationId: WebhookService_CreateWebhook - responses: - "200": - description: A successful response. - schema: - $ref: '#/definitions/v1Webhook' - default: - description: An unexpected error response. - schema: - $ref: '#/definitions/googlerpcStatus' - parameters: - - name: webhook - description: Required. The webhook to create. - in: body - required: true - schema: - $ref: '#/definitions/v1Webhook' - required: - - webhook - - name: webhookId - description: |- - Optional. The webhook ID to use for this webhook. - If empty, a unique ID will be generated. - Must match the pattern [a-z0-9-]+ - in: query - required: false - type: string - - name: validateOnly - description: Optional. If set, validate the request but don't actually create the webhook. - in: query - required: false - type: boolean - tags: - - WebhookService /api/v1/workspace/profile: get: summary: Gets the workspace profile. @@ -1242,7 +1191,7 @@ paths: "200": description: A successful response. schema: - $ref: '#/definitions/v1Webhook' + $ref: '#/definitions/apiv1Webhook' default: description: An unexpected error response. schema: @@ -1250,12 +1199,12 @@ paths: parameters: - name: name_6 description: |- - Required. The resource name of the webhook. - Format: webhooks/{webhook} + Required. The resource name of the webhook to retrieve. + Format: users/{user}/webhooks/{webhook} in: path required: true type: string - pattern: webhooks/[^/]+ + pattern: users/[^/]+/webhooks/[^/]+ tags: - WebhookService delete: @@ -1362,7 +1311,7 @@ paths: - ShortcutService /api/v1/{name_9}: delete: - summary: DeleteWebhook deletes a webhook. + summary: DeleteWebhook deletes a webhook for a user. operationId: WebhookService_DeleteWebhook responses: "200": @@ -1378,11 +1327,11 @@ paths: - name: name_9 description: |- Required. The resource name of the webhook to delete. - Format: webhooks/{webhook} + Format: users/{user}/webhooks/{webhook} in: path required: true type: string - pattern: webhooks/[^/]+ + pattern: users/[^/]+/webhooks/[^/]+ tags: - WebhookService /api/v1/{name}: @@ -2131,6 +2080,66 @@ paths: $ref: '#/definitions/MemoServiceRenameMemoTagBody' tags: - MemoService + /api/v1/{parent}/webhooks: + get: + summary: ListWebhooks returns a list of webhooks for a user. + operationId: WebhookService_ListWebhooks + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListWebhooksResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Required. The parent resource where webhooks are listed. + Format: users/{user} + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - WebhookService + post: + summary: CreateWebhook creates a new webhook for a user. + operationId: WebhookService_CreateWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Webhook' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Required. The parent resource where this webhook will be created. + Format: users/{user} + in: path + required: true + type: string + pattern: users/[^/]+ + - name: webhook + description: Required. The webhook to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1Webhook' + required: + - webhook + - name: validateOnly + description: Optional. If set, validate the request, but do not actually create the webhook. + in: query + required: false + type: boolean + tags: + - WebhookService /api/v1/{setting.name}: patch: summary: Updates a workspace setting. @@ -2333,13 +2342,13 @@ paths: - UserService /api/v1/{webhook.name}: patch: - summary: UpdateWebhook updates a webhook. + summary: UpdateWebhook updates a webhook for a user. operationId: WebhookService_UpdateWebhook responses: "200": description: A successful response. schema: - $ref: '#/definitions/v1Webhook' + $ref: '#/definitions/apiv1Webhook' default: description: An unexpected error response. schema: @@ -2348,13 +2357,13 @@ paths: - name: webhook.name description: |- The resource name of the webhook. - Format: webhooks/{webhook} + Format: users/{user}/webhooks/{webhook} in: path required: true type: string - pattern: webhooks/[^/]+ + pattern: users/[^/]+/webhooks/[^/]+ - name: webhook - description: Required. The webhook to update. + description: Required. The webhook resource which replaces the resource on the server. in: body required: true schema: @@ -2362,34 +2371,14 @@ paths: properties: displayName: type: string - description: Required. The display name of the webhook. + description: The display name of the webhook. url: type: string - description: Required. The target URL for the webhook. - creator: - type: string - title: |- - Output only. The resource name of the creator. - Format: users/{user} - readOnly: true - state: - $ref: '#/definitions/v1State' - description: The state of the webhook. - createTime: - type: string - format: date-time - description: Output only. The creation timestamp. - readOnly: true - updateTime: - type: string - format: date-time - description: Output only. The last update timestamp. - readOnly: true - title: Required. The webhook to update. + description: The target URL for the webhook. + title: Required. The webhook resource which replaces the resource on the server. required: - displayName - url - - state - webhook tags: - WebhookService @@ -2878,6 +2867,23 @@ definitions: type: string description: The default visibility of the memo. title: User settings message + apiv1Webhook: + type: object + properties: + name: + type: string + title: |- + The resource name of the webhook. + Format: users/{user}/webhooks/{webhook} + displayName: + type: string + description: The display name of the webhook. + url: + type: string + description: The target URL for the webhook. + required: + - displayName + - url apiv1WorkspaceCustomProfile: type: object properties: @@ -3658,7 +3664,7 @@ definitions: type: array items: type: object - $ref: '#/definitions/v1Webhook' + $ref: '#/definitions/apiv1Webhook' description: The list of webhooks. v1MathBlockNode: type: object @@ -4200,43 +4206,6 @@ definitions: - PROTECTED - PUBLIC default: VISIBILITY_UNSPECIFIED - v1Webhook: - type: object - properties: - name: - type: string - title: |- - The resource name of the webhook. - Format: webhooks/{webhook} - displayName: - type: string - description: Required. The display name of the webhook. - url: - type: string - description: Required. The target URL for the webhook. - creator: - type: string - title: |- - Output only. The resource name of the creator. - Format: users/{user} - readOnly: true - state: - $ref: '#/definitions/v1State' - description: The state of the webhook. - createTime: - type: string - format: date-time - description: Output only. The creation timestamp. - readOnly: true - updateTime: - type: string - format: date-time - description: Output only. The last update timestamp. - readOnly: true - required: - - displayName - - url - - state v1WorkspaceProfile: type: object properties: diff --git a/proto/gen/store/user_setting.pb.go b/proto/gen/store/user_setting.pb.go index e710a9faa..9c34a4ed3 100644 --- a/proto/gen/store/user_setting.pb.go +++ b/proto/gen/store/user_setting.pb.go @@ -34,6 +34,8 @@ const ( UserSetting_ACCESS_TOKENS UserSetting_Key = 3 // The shortcuts of the user. UserSetting_SHORTCUTS UserSetting_Key = 4 + // The webhooks of the user. + UserSetting_WEBHOOKS UserSetting_Key = 5 ) // Enum value maps for UserSetting_Key. @@ -44,6 +46,7 @@ var ( 2: "SESSIONS", 3: "ACCESS_TOKENS", 4: "SHORTCUTS", + 5: "WEBHOOKS", } UserSetting_Key_value = map[string]int32{ "KEY_UNSPECIFIED": 0, @@ -51,6 +54,7 @@ var ( "SESSIONS": 2, "ACCESS_TOKENS": 3, "SHORTCUTS": 4, + "WEBHOOKS": 5, } ) @@ -91,6 +95,7 @@ type UserSetting struct { // *UserSetting_Sessions // *UserSetting_AccessTokens // *UserSetting_Shortcuts + // *UserSetting_Webhooks Value isUserSetting_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -183,6 +188,15 @@ func (x *UserSetting) GetShortcuts() *ShortcutsUserSetting { return nil } +func (x *UserSetting) GetWebhooks() *WebhooksUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_Webhooks); ok { + return x.Webhooks + } + } + return nil +} + type isUserSetting_Value interface { isUserSetting_Value() } @@ -203,6 +217,10 @@ type UserSetting_Shortcuts struct { Shortcuts *ShortcutsUserSetting `protobuf:"bytes,6,opt,name=shortcuts,proto3,oneof"` } +type UserSetting_Webhooks struct { + Webhooks *WebhooksUserSetting `protobuf:"bytes,7,opt,name=webhooks,proto3,oneof"` +} + func (*UserSetting_General) isUserSetting_Value() {} func (*UserSetting_Sessions) isUserSetting_Value() {} @@ -211,6 +229,8 @@ func (*UserSetting_AccessTokens) isUserSetting_Value() {} func (*UserSetting_Shortcuts) isUserSetting_Value() {} +func (*UserSetting_Webhooks) isUserSetting_Value() {} + type GeneralUserSetting struct { state protoimpl.MessageState `protogen:"open.v1"` // The user's locale. @@ -406,6 +426,50 @@ func (x *ShortcutsUserSetting) GetShortcuts() []*ShortcutsUserSetting_Shortcut { return nil } +type WebhooksUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Webhooks []*WebhooksUserSetting_Webhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebhooksUserSetting) Reset() { + *x = WebhooksUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebhooksUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhooksUserSetting) ProtoMessage() {} + +func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebhooksUserSetting.ProtoReflect.Descriptor instead. +func (*WebhooksUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{5} +} + +func (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook { + if x != nil { + return x.Webhooks + } + return nil +} + type SessionsUserSetting_Session struct { state protoimpl.MessageState `protogen:"open.v1"` // Unique session identifier. @@ -424,7 +488,7 @@ type SessionsUserSetting_Session struct { func (x *SessionsUserSetting_Session) Reset() { *x = SessionsUserSetting_Session{} - mi := &file_store_user_setting_proto_msgTypes[5] + mi := &file_store_user_setting_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -436,7 +500,7 @@ func (x *SessionsUserSetting_Session) String() string { func (*SessionsUserSetting_Session) ProtoMessage() {} func (x *SessionsUserSetting_Session) ProtoReflect() protoreflect.Message { - mi := &file_store_user_setting_proto_msgTypes[5] + mi := &file_store_user_setting_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -505,7 +569,7 @@ type SessionsUserSetting_ClientInfo struct { func (x *SessionsUserSetting_ClientInfo) Reset() { *x = SessionsUserSetting_ClientInfo{} - mi := &file_store_user_setting_proto_msgTypes[6] + mi := &file_store_user_setting_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -517,7 +581,7 @@ func (x *SessionsUserSetting_ClientInfo) String() string { func (*SessionsUserSetting_ClientInfo) ProtoMessage() {} func (x *SessionsUserSetting_ClientInfo) ProtoReflect() protoreflect.Message { - mi := &file_store_user_setting_proto_msgTypes[6] + mi := &file_store_user_setting_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -581,7 +645,7 @@ type AccessTokensUserSetting_AccessToken struct { func (x *AccessTokensUserSetting_AccessToken) Reset() { *x = AccessTokensUserSetting_AccessToken{} - mi := &file_store_user_setting_proto_msgTypes[7] + mi := &file_store_user_setting_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -593,7 +657,7 @@ func (x *AccessTokensUserSetting_AccessToken) String() string { func (*AccessTokensUserSetting_AccessToken) ProtoMessage() {} func (x *AccessTokensUserSetting_AccessToken) ProtoReflect() protoreflect.Message { - mi := &file_store_user_setting_proto_msgTypes[7] + mi := &file_store_user_setting_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -634,7 +698,7 @@ type ShortcutsUserSetting_Shortcut struct { func (x *ShortcutsUserSetting_Shortcut) Reset() { *x = ShortcutsUserSetting_Shortcut{} - mi := &file_store_user_setting_proto_msgTypes[8] + mi := &file_store_user_setting_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -646,7 +710,7 @@ func (x *ShortcutsUserSetting_Shortcut) String() string { func (*ShortcutsUserSetting_Shortcut) ProtoMessage() {} func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message { - mi := &file_store_user_setting_proto_msgTypes[8] + mi := &file_store_user_setting_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -683,24 +747,89 @@ func (x *ShortcutsUserSetting_Shortcut) GetFilter() string { return "" } +type WebhooksUserSetting_Webhook struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique identifier for the webhook + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Descriptive title for the webhook + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // The webhook URL endpoint + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebhooksUserSetting_Webhook) Reset() { + *x = WebhooksUserSetting_Webhook{} + mi := &file_store_user_setting_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebhooksUserSetting_Webhook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhooksUserSetting_Webhook) ProtoMessage() {} + +func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebhooksUserSetting_Webhook.ProtoReflect.Descriptor instead. +func (*WebhooksUserSetting_Webhook) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *WebhooksUserSetting_Webhook) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WebhooksUserSetting_Webhook) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *WebhooksUserSetting_Webhook) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + var File_store_user_setting_proto protoreflect.FileDescriptor const file_store_user_setting_proto_rawDesc = "" + "\n" + - "\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc5\x03\n" + + "\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\x93\x04\n" + "\vUserSetting\x12\x17\n" + "\auser_id\x18\x01 \x01(\x05R\x06userId\x12.\n" + "\x03key\x18\x02 \x01(\x0e2\x1c.memos.store.UserSetting.KeyR\x03key\x12;\n" + "\ageneral\x18\x03 \x01(\v2\x1f.memos.store.GeneralUserSettingH\x00R\ageneral\x12>\n" + "\bsessions\x18\x04 \x01(\v2 .memos.store.SessionsUserSettingH\x00R\bsessions\x12K\n" + "\raccess_tokens\x18\x05 \x01(\v2$.memos.store.AccessTokensUserSettingH\x00R\faccessTokens\x12A\n" + - "\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\"W\n" + + "\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" + + "\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\"e\n" + "\x03Key\x12\x13\n" + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + "\aGENERAL\x10\x01\x12\f\n" + "\bSESSIONS\x10\x02\x12\x11\n" + "\rACCESS_TOKENS\x10\x03\x12\r\n" + - "\tSHORTCUTS\x10\x04B\a\n" + + "\tSHORTCUTS\x10\x04\x12\f\n" + + "\bWEBHOOKS\x10\x05B\a\n" + "\x05value\"u\n" + "\x12GeneralUserSetting\x12\x16\n" + "\x06locale\x18\x01 \x01(\tR\x06locale\x12\x1e\n" + @@ -740,7 +869,13 @@ const file_store_user_setting_proto_rawDesc = "" + "\bShortcut\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + - "\x06filter\x18\x03 \x01(\tR\x06filterB\x9b\x01\n" + + "\x06filter\x18\x03 \x01(\tR\x06filter\"\x9e\x01\n" + + "\x13WebhooksUserSetting\x12D\n" + + "\bwebhooks\x18\x01 \x03(\v2(.memos.store.WebhooksUserSetting.WebhookR\bwebhooks\x1aA\n" + + "\aWebhook\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x10\n" + + "\x03url\x18\x03 \x01(\tR\x03urlB\x9b\x01\n" + "\x0fcom.memos.storeB\x10UserSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" var ( @@ -756,7 +891,7 @@ func file_store_user_setting_proto_rawDescGZIP() []byte { } var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_store_user_setting_proto_goTypes = []any{ (UserSetting_Key)(0), // 0: memos.store.UserSetting.Key (*UserSetting)(nil), // 1: memos.store.UserSetting @@ -764,11 +899,13 @@ var file_store_user_setting_proto_goTypes = []any{ (*SessionsUserSetting)(nil), // 3: memos.store.SessionsUserSetting (*AccessTokensUserSetting)(nil), // 4: memos.store.AccessTokensUserSetting (*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting - (*SessionsUserSetting_Session)(nil), // 6: memos.store.SessionsUserSetting.Session - (*SessionsUserSetting_ClientInfo)(nil), // 7: memos.store.SessionsUserSetting.ClientInfo - (*AccessTokensUserSetting_AccessToken)(nil), // 8: memos.store.AccessTokensUserSetting.AccessToken - (*ShortcutsUserSetting_Shortcut)(nil), // 9: memos.store.ShortcutsUserSetting.Shortcut - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting + (*SessionsUserSetting_Session)(nil), // 7: memos.store.SessionsUserSetting.Session + (*SessionsUserSetting_ClientInfo)(nil), // 8: memos.store.SessionsUserSetting.ClientInfo + (*AccessTokensUserSetting_AccessToken)(nil), // 9: memos.store.AccessTokensUserSetting.AccessToken + (*ShortcutsUserSetting_Shortcut)(nil), // 10: memos.store.ShortcutsUserSetting.Shortcut + (*WebhooksUserSetting_Webhook)(nil), // 11: memos.store.WebhooksUserSetting.Webhook + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp } var file_store_user_setting_proto_depIdxs = []int32{ 0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key @@ -776,18 +913,20 @@ var file_store_user_setting_proto_depIdxs = []int32{ 3, // 2: memos.store.UserSetting.sessions:type_name -> memos.store.SessionsUserSetting 4, // 3: memos.store.UserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting 5, // 4: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting - 6, // 5: memos.store.SessionsUserSetting.sessions:type_name -> memos.store.SessionsUserSetting.Session - 8, // 6: memos.store.AccessTokensUserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting.AccessToken - 9, // 7: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut - 10, // 8: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp - 10, // 9: memos.store.SessionsUserSetting.Session.expire_time:type_name -> google.protobuf.Timestamp - 10, // 10: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp - 7, // 11: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 6, // 5: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting + 7, // 6: memos.store.SessionsUserSetting.sessions:type_name -> memos.store.SessionsUserSetting.Session + 9, // 7: memos.store.AccessTokensUserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting.AccessToken + 10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut + 11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook + 12, // 10: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp + 12, // 11: memos.store.SessionsUserSetting.Session.expire_time:type_name -> google.protobuf.Timestamp + 12, // 12: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp + 8, // 13: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_store_user_setting_proto_init() } @@ -800,6 +939,7 @@ func file_store_user_setting_proto_init() { (*UserSetting_Sessions)(nil), (*UserSetting_AccessTokens)(nil), (*UserSetting_Shortcuts)(nil), + (*UserSetting_Webhooks)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -807,7 +947,7 @@ func file_store_user_setting_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)), NumEnums: 1, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/store/user_setting.proto b/proto/store/user_setting.proto index 4f519909b..6a71fe23b 100644 --- a/proto/store/user_setting.proto +++ b/proto/store/user_setting.proto @@ -17,17 +17,19 @@ message UserSetting { ACCESS_TOKENS = 3; // The shortcuts of the user. SHORTCUTS = 4; + // The webhooks of the user. + WEBHOOKS = 5; } int32 user_id = 1; Key key = 2; - oneof value { GeneralUserSetting general = 3; SessionsUserSetting sessions = 4; AccessTokensUserSetting access_tokens = 5; ShortcutsUserSetting shortcuts = 6; + WebhooksUserSetting webhooks = 7; } } @@ -89,3 +91,15 @@ message ShortcutsUserSetting { } repeated Shortcut shortcuts = 1; } + +message WebhooksUserSetting { + message Webhook { + // Unique identifier for the webhook + string id = 1; + // Descriptive title for the webhook + string title = 2; + // The webhook URL endpoint + string url = 3; + } + repeated Webhook webhooks = 1; +} diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 19ddbcb07..9bc0a25ce 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -18,7 +18,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/usememos/memos/plugin/webhook" v1pb "github.com/usememos/memos/proto/gen/api/v1" @@ -689,9 +688,7 @@ func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1p if err != nil { return status.Errorf(codes.InvalidArgument, "invalid memo creator") } - webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ - CreatorID: &creatorID, - }) + webhooks, err := s.Store.GetUserWebhooks(ctx, creatorID) if err != nil { return err } @@ -701,7 +698,7 @@ func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1p return errors.Wrap(err, "failed to convert memo to webhook payload") } payload.ActivityType = activityType - payload.Url = hook.URL + payload.Url = hook.Url // Use asynchronous webhook dispatch webhook.PostAsync(payload) @@ -709,15 +706,14 @@ func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1p return nil } -func convertMemoToWebhookPayload(memo *v1pb.Memo) (*v1pb.WebhookRequestPayload, error) { +func convertMemoToWebhookPayload(memo *v1pb.Memo) (*webhook.WebhookRequestPayload, error) { creatorID, err := ExtractUserIDFromName(memo.Creator) if err != nil { return nil, errors.Wrap(err, "invalid memo creator") } - return &v1pb.WebhookRequestPayload{ - Creator: fmt.Sprintf("%s%d", UserNamePrefix, creatorID), - CreateTime: timestamppb.New(time.Now()), - Memo: memo, + return &webhook.WebhookRequestPayload{ + Creator: fmt.Sprintf("%s%d", UserNamePrefix, creatorID), + Memo: memo, }, nil } diff --git a/server/router/api/v1/resource_name.go b/server/router/api/v1/resource_name.go index 45ad21058..98e692b31 100644 --- a/server/router/api/v1/resource_name.go +++ b/server/router/api/v1/resource_name.go @@ -146,14 +146,18 @@ func ExtractActivityIDFromName(name string) (int32, error) { } // ExtractWebhookIDFromName returns the webhook ID from a resource name. -func ExtractWebhookIDFromName(name string) (int32, error) { - tokens, err := GetNameParentTokens(name, WebhookNamePrefix) +// Expected format: users/{user}/webhooks/{webhook} +func ExtractWebhookIDFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, UserNamePrefix, WebhookNamePrefix) if err != nil { - return 0, err + return "", err } - id, err := util.ConvertStringToInt32(tokens[0]) - if err != nil { - return 0, errors.Errorf("invalid webhook ID %q", tokens[0]) + if len(tokens) != 2 { + return "", errors.Errorf("invalid webhook name format: %q", name) } - return id, nil + webhookID := tokens[1] + if webhookID == "" { + return "", errors.Errorf("invalid webhook ID %q", webhookID) + } + return webhookID, nil } diff --git a/server/router/api/v1/test/webhook_service_test.go b/server/router/api/v1/test/webhook_service_test.go index 4ea2cdf15..8960f94de 100644 --- a/server/router/api/v1/test/webhook_service_test.go +++ b/server/router/api/v1/test/webhook_service_test.go @@ -13,7 +13,6 @@ import ( func TestCreateWebhook(t *testing.T) { ctx := context.Background() - t.Run("CreateWebhook with host user", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) @@ -27,6 +26,7 @@ func TestCreateWebhook(t *testing.T) { // Create a webhook req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -41,16 +41,16 @@ func TestCreateWebhook(t *testing.T) { require.Equal(t, "Test Webhook", resp.DisplayName) require.Equal(t, "https://example.com/webhook", resp.Url) require.Contains(t, resp.Name, "webhooks/") - require.Equal(t, fmt.Sprintf("users/%d", hostUser.ID), resp.Creator) + require.Contains(t, resp.Name, fmt.Sprintf("users/%d", hostUser.ID)) }) t.Run("CreateWebhook fails without authentication", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() - // Try to create webhook without authentication req := &v1pb.CreateWebhookRequest{ + Parent: "users/1", // Dummy parent since we don't have a real user Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -73,9 +73,9 @@ func TestCreateWebhook(t *testing.T) { require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, regularUser.ID) - // Try to create webhook as regular user req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", regularUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -99,9 +99,9 @@ func TestCreateWebhook(t *testing.T) { require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Try to create webhook with missing URL req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", // URL missing @@ -128,9 +128,10 @@ func TestListWebhooks(t *testing.T) { require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // List webhooks - req := &v1pb.ListWebhooksRequest{} + req := &v1pb.ListWebhooksRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + } resp, err := ts.Service.ListWebhooks(userCtx, req) // Verify response @@ -148,9 +149,9 @@ func TestListWebhooks(t *testing.T) { hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Create a webhook createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -160,7 +161,9 @@ func TestListWebhooks(t *testing.T) { require.NoError(t, err) // List webhooks - listReq := &v1pb.ListWebhooksRequest{} + listReq := &v1pb.ListWebhooksRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + } resp, err := ts.Service.ListWebhooks(userCtx, listReq) // Verify response @@ -175,9 +178,10 @@ func TestListWebhooks(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() - // Try to list webhooks without authentication - req := &v1pb.ListWebhooksRequest{} + req := &v1pb.ListWebhooksRequest{ + Parent: "users/1", // Dummy parent since we don't have a real user + } _, err := ts.Service.ListWebhooks(ctx, req) // Should fail with permission denied or unauthenticated @@ -197,9 +201,9 @@ func TestGetWebhook(t *testing.T) { hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Create a webhook createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -213,13 +217,11 @@ func TestGetWebhook(t *testing.T) { Name: createdWebhook.Name, } resp, err := ts.Service.GetWebhook(userCtx, getReq) - // Verify response require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, createdWebhook.Name, resp.Name) require.Equal(t, createdWebhook.Url, resp.Url) - require.Equal(t, createdWebhook.Creator, resp.Creator) }) t.Run("GetWebhook fails with invalid name", func(t *testing.T) { @@ -251,10 +253,9 @@ func TestGetWebhook(t *testing.T) { hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Try to get non-existent webhook req := &v1pb.GetWebhookRequest{ - Name: "webhooks/999", + Name: fmt.Sprintf("users/%d/webhooks/999", hostUser.ID), } _, err = ts.Service.GetWebhook(userCtx, req) @@ -276,12 +277,12 @@ func TestUpdateWebhook(t *testing.T) { hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Create a webhook createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ - Name: "Original Webhook", - Url: "https://example.com/webhook", + DisplayName: "Original Webhook", + Url: "https://example.com/webhook", }, } createdWebhook, err := ts.Service.CreateWebhook(userCtx, createReq) @@ -310,11 +311,10 @@ func TestUpdateWebhook(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() - // Try to update webhook without authentication req := &v1pb.UpdateWebhookRequest{ Webhook: &v1pb.Webhook{ - Name: "webhooks/1", + Name: "users/1/webhooks/1", Url: "https://updated.example.com/webhook", }, } @@ -328,7 +328,6 @@ func TestUpdateWebhook(t *testing.T) { func TestDeleteWebhook(t *testing.T) { ctx := context.Background() - t.Run("DeleteWebhook removes webhook", func(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) @@ -341,6 +340,7 @@ func TestDeleteWebhook(t *testing.T) { // Create a webhook createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), Webhook: &v1pb.Webhook{ DisplayName: "Test Webhook", Url: "https://example.com/webhook", @@ -373,10 +373,9 @@ func TestDeleteWebhook(t *testing.T) { // Create test service for this specific test ts := NewTestService(t) defer ts.Cleanup() - // Try to delete webhook without authentication req := &v1pb.DeleteWebhookRequest{ - Name: "webhooks/1", + Name: "users/1/webhooks/1", } _, err := ts.Service.DeleteWebhook(ctx, req) @@ -394,10 +393,9 @@ func TestDeleteWebhook(t *testing.T) { hostUser, err := ts.CreateHostUser(ctx, "admin") require.NoError(t, err) userCtx := ts.CreateUserContext(ctx, hostUser.ID) - // Try to delete non-existent webhook req := &v1pb.DeleteWebhookRequest{ - Name: "webhooks/999", + Name: fmt.Sprintf("users/%d/webhooks/999", hostUser.ID), } _, err = ts.Service.DeleteWebhook(userCtx, req) diff --git a/server/router/api/v1/webhook_service.go b/server/router/api/v1/webhook_service.go index 379ee27bc..9b6768f99 100644 --- a/server/router/api/v1/webhook_service.go +++ b/server/router/api/v1/webhook_service.go @@ -2,17 +2,18 @@ package v1 import ( "context" + "crypto/rand" + "encoding/hex" "fmt" "strings" - "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" + "github.com/usememos/memos/internal/util" v1pb "github.com/usememos/memos/proto/gen/api/v1" - "github.com/usememos/memos/store" + storepb "github.com/usememos/memos/proto/gen/store" ) func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWebhookRequest) (*v1pb.Webhook, error) { @@ -24,6 +25,17 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } + // Extract user ID from parent (format: users/{user}) + parentUserID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) + } + + // Users can only create webhooks for themselves + if parentUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + // Only host users can create webhooks if !isSuperUser(currentUser) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") @@ -37,29 +49,42 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required") } - // TODO: Handle webhook_id, validate_only, and request_id fields + // Handle validate_only field if request.ValidateOnly { // Perform validation checks without actually creating the webhook return &v1pb.Webhook{ + Name: fmt.Sprintf("users/%d/webhooks/validate", currentUser.ID), 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{ - CreatorID: currentUser.ID, - Name: request.Webhook.DisplayName, - URL: strings.TrimSpace(request.Webhook.Url), + err = s.Store.AddUserWebhook(ctx, currentUser.ID, &storepb.WebhooksUserSetting_Webhook{ + Id: generateWebhookID(), + Title: request.Webhook.DisplayName, + Url: strings.TrimSpace(request.Webhook.Url), }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err) } - return convertWebhookFromStore(webhook), nil + + // Return the newly created webhook + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user webhooks, error: %+v", err) + } + + // Find the webhook we just created + for _, webhook := range webhooks { + if webhook.Title == request.Webhook.DisplayName && webhook.Url == strings.TrimSpace(request.Webhook.Url) { + return convertWebhookFromUserSetting(webhook, currentUser.ID), nil + } + } + + return nil, status.Errorf(codes.Internal, "failed to find created webhook") } -func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) { +func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) { currentUser, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) @@ -68,11 +93,18 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksReq return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } - // TODO: Implement proper filtering, ordering, and pagination - // For now, list webhooks for the current user - webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{ - CreatorID: ¤tUser.ID, - }) + // Extract user ID from parent (format: users/{user}) + parentUserID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) + } + + // Users can only list their own webhooks + if parentUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err) } @@ -81,16 +113,28 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksReq Webhooks: []*v1pb.Webhook{}, } for _, webhook := range webhooks { - response.Webhooks = append(response.Webhooks, convertWebhookFromStore(webhook)) + response.Webhooks = append(response.Webhooks, convertWebhookFromUserSetting(webhook, currentUser.ID)) } return response, nil } func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) { - webhookID, err := ExtractWebhookIDFromName(request.Name) + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } currentUser, err := s.GetCurrentUser(ctx) if err != nil { @@ -100,22 +144,23 @@ func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookR return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } - webhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{ - ID: &webhookID, - CreatorID: ¤tUser.ID, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get webhook, error: %+v", err) - } - if webhook == nil { - return nil, status.Errorf(codes.NotFound, "webhook not found") + // Users can only access their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") } - webhookPb := convertWebhookFromStore(webhook) - - // TODO: Implement read_mask field filtering + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get webhooks, error: %+v", err) + } - return webhookPb, nil + // Find webhook by ID + for _, webhook := range webhooks { + if webhook.Id == webhookID { + return convertWebhookFromUserSetting(webhook, currentUser.ID), nil + } + } + return nil, status.Errorf(codes.NotFound, "webhook not found") } func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) { @@ -123,10 +168,22 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") } - webhookID, err := ExtractWebhookIDFromName(request.Webhook.Name) + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Webhook.Name, UserNamePrefix, WebhookNamePrefix) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } currentUser, err := s.GetCurrentUser(ctx) if err != nil { @@ -136,44 +193,75 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } - // Check if webhook exists and user has permission - existingWebhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{ - ID: &webhookID, - CreatorID: ¤tUser.ID, - }) + // Users can only update their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Get existing webhooks from user settings + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err) + } + + // Find the webhook to update + var existingWebhook *storepb.WebhooksUserSetting_Webhook + for _, webhook := range webhooks { + if webhook.Id == webhookID { + existingWebhook = webhook + break + } } + if existingWebhook == nil { return nil, status.Errorf(codes.NotFound, "webhook not found") } - update := &store.UpdateWebhook{ - ID: webhookID, + // Create updated webhook + updatedWebhook := &storepb.WebhooksUserSetting_Webhook{ + Id: existingWebhook.Id, + Title: existingWebhook.Title, + Url: existingWebhook.Url, } + + // Apply updates based on update mask for _, field := range request.UpdateMask.Paths { switch field { case "display_name": - update.Name = &request.Webhook.DisplayName + updatedWebhook.Title = request.Webhook.DisplayName case "url": - update.URL = &request.Webhook.Url + updatedWebhook.Url = request.Webhook.Url default: return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) } } - webhook, err := s.Store.UpdateWebhook(ctx, update) + // Update the webhook in user settings + err = s.Store.UpdateUserWebhook(ctx, currentUser.ID, updatedWebhook) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to update webhook, error: %+v", err) + return nil, status.Errorf(codes.Internal, "failed to update webhook: %v", err) } - return convertWebhookFromStore(webhook), nil + + return convertWebhookFromUserSetting(updatedWebhook, currentUser.ID), nil } func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) { - webhookID, err := ExtractWebhookIDFromName(request.Name) + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } currentUser, err := s.GetCurrentUser(ctx) if err != nil { @@ -183,37 +271,47 @@ func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWe return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } - // Check if webhook exists and user has permission - webhook, err := s.Store.GetWebhook(ctx, &store.FindWebhook{ - ID: &webhookID, - CreatorID: ¤tUser.ID, - }) + // Users can only delete their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Get existing webhooks from user settings to verify it exists + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err) } - if webhook == nil { - return nil, status.Errorf(codes.NotFound, "webhook not found") + + // Check if webhook exists + webhookExists := false + for _, webhook := range webhooks { + if webhook.Id == webhookID { + webhookExists = true + break + } } - // TODO: Handle force field properly + if !webhookExists { + return nil, status.Errorf(codes.NotFound, "webhook not found") + } - err = s.Store.DeleteWebhook(ctx, &store.DeleteWebhook{ - ID: webhookID, - }) + err = s.Store.RemoveUserWebhook(ctx, currentUser.ID, webhookID) 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: %v", err) } return &emptypb.Empty{}, nil } -func convertWebhookFromStore(webhook *store.Webhook) *v1pb.Webhook { +func convertWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, userID int32) *v1pb.Webhook { return &v1pb.Webhook{ - Name: fmt.Sprintf("webhooks/%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)), - UpdateTime: timestamppb.New(time.Unix(webhook.UpdatedTs, 0)), + Name: fmt.Sprintf("users/%d/webhooks/%s", userID, webhook.Id), + DisplayName: webhook.Title, + Url: webhook.Url, } } + +func generateWebhookID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/store/db/mysql/webhook.go b/store/db/mysql/webhook.go deleted file mode 100644 index ec750efdb..000000000 --- a/store/db/mysql/webhook.go +++ /dev/null @@ -1,108 +0,0 @@ -package mysql - -import ( - "context" - "strings" - - "github.com/usememos/memos/store" -) - -func (d *DB) CreateWebhook(ctx context.Context, create *store.Webhook) (*store.Webhook, error) { - fields := []string{"`name`", "`url`", "`creator_id`"} - placeholder := []string{"?", "?", "?"} - args := []any{create.Name, create.URL, create.CreatorID} - - stmt := "INSERT INTO `webhook` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" - result, err := d.db.ExecContext(ctx, stmt, args...) - if err != nil { - return nil, err - } - - id, err := result.LastInsertId() - if err != nil { - return nil, err - } - - create.ID = int32(id) - return d.GetWebhook(ctx, &store.FindWebhook{ID: &create.ID}) -} - -func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*store.Webhook, error) { - where, args := []string{"1 = 1"}, []any{} - if find.ID != nil { - where, args = append(where, "`id` = ?"), append(args, *find.ID) - } - if find.CreatorID != nil { - where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID) - } - - rows, err := d.db.QueryContext(ctx, "SELECT `id`, UNIX_TIMESTAMP(`created_ts`), UNIX_TIMESTAMP(`updated_ts`), `creator_id`, `name`, `url` FROM `webhook` WHERE "+strings.Join(where, " AND ")+" ORDER BY `id` DESC", - args..., - ) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Webhook{} - for rows.Next() { - webhook := &store.Webhook{} - if err := rows.Scan( - &webhook.ID, - &webhook.CreatedTs, - &webhook.UpdatedTs, - &webhook.CreatorID, - &webhook.Name, - &webhook.URL, - ); err != nil { - return nil, err - } - list = append(list, webhook) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -func (d *DB) GetWebhook(ctx context.Context, find *store.FindWebhook) (*store.Webhook, error) { - list, err := d.ListWebhooks(ctx, find) - if err != nil { - return nil, err - } - if len(list) == 0 { - return nil, nil - } - return list[0], nil -} - -func (d *DB) UpdateWebhook(ctx context.Context, update *store.UpdateWebhook) (*store.Webhook, error) { - set, args := []string{}, []any{} - if update.Name != nil { - set, args = append(set, "`name` = ?"), append(args, *update.Name) - } - if update.URL != nil { - set, args = append(set, "`url` = ?"), append(args, *update.URL) - } - args = append(args, update.ID) - - stmt := "UPDATE `webhook` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" - _, err := d.db.ExecContext(ctx, stmt, args...) - if err != nil { - return nil, err - } - - webhook, err := d.GetWebhook(ctx, &store.FindWebhook{ID: &update.ID}) - if err != nil { - return nil, err - } - - return webhook, nil -} - -func (d *DB) DeleteWebhook(ctx context.Context, delete *store.DeleteWebhook) error { - _, err := d.db.ExecContext(ctx, "DELETE FROM `webhook` WHERE `id` = ?", delete.ID) - return err -} diff --git a/store/db/postgres/webhook.go b/store/db/postgres/webhook.go deleted file mode 100644 index 7f2b49ed2..000000000 --- a/store/db/postgres/webhook.go +++ /dev/null @@ -1,103 +0,0 @@ -package postgres - -import ( - "context" - "strings" - - "github.com/usememos/memos/store" -) - -func (d *DB) CreateWebhook(ctx context.Context, create *store.Webhook) (*store.Webhook, error) { - fields := []string{"name", "url", "creator_id"} - args := []any{create.Name, create.URL, create.CreatorID} - stmt := "INSERT INTO webhook (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts, updated_ts" - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &create.ID, - &create.CreatedTs, - &create.UpdatedTs, - ); err != nil { - return nil, err - } - webhook := create - return webhook, nil -} - -func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*store.Webhook, error) { - where, args := []string{"1 = 1"}, []any{} - if find.ID != nil { - where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) - } - if find.CreatorID != nil { - where, args = append(where, "creator_id = "+placeholder(len(args)+1)), append(args, *find.CreatorID) - } - - rows, err := d.db.QueryContext(ctx, ` - SELECT - id, - created_ts, - updated_ts, - creator_id, - name, - url - FROM webhook - WHERE `+strings.Join(where, " AND ")+` - ORDER BY id DESC`, - args..., - ) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Webhook{} - for rows.Next() { - webhook := &store.Webhook{} - if err := rows.Scan( - &webhook.ID, - &webhook.CreatedTs, - &webhook.UpdatedTs, - &webhook.CreatorID, - &webhook.Name, - &webhook.URL, - ); err != nil { - return nil, err - } - list = append(list, webhook) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -func (d *DB) UpdateWebhook(ctx context.Context, update *store.UpdateWebhook) (*store.Webhook, error) { - set, args := []string{}, []any{} - if update.Name != nil { - set, args = append(set, "name = "+placeholder(len(args)+1)), append(args, *update.Name) - } - if update.URL != nil { - set, args = append(set, "url = "+placeholder(len(args)+1)), append(args, *update.URL) - } - - stmt := "UPDATE webhook SET " + strings.Join(set, ", ") + " WHERE id = " + placeholder(len(args)+1) + " RETURNING id, created_ts, updated_ts, creator_id, name, url" - args = append(args, update.ID) - webhook := &store.Webhook{} - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &webhook.ID, - &webhook.CreatedTs, - &webhook.UpdatedTs, - &webhook.CreatorID, - &webhook.Name, - &webhook.URL, - ); err != nil { - return nil, err - } - return webhook, nil -} - -func (d *DB) DeleteWebhook(ctx context.Context, delete *store.DeleteWebhook) error { - _, err := d.db.ExecContext(ctx, "DELETE FROM webhook WHERE id = $1", delete.ID) - return err -} diff --git a/store/db/sqlite/webhook.go b/store/db/sqlite/webhook.go deleted file mode 100644 index 228f97a36..000000000 --- a/store/db/sqlite/webhook.go +++ /dev/null @@ -1,104 +0,0 @@ -package sqlite - -import ( - "context" - "strings" - - "github.com/usememos/memos/store" -) - -func (d *DB) CreateWebhook(ctx context.Context, create *store.Webhook) (*store.Webhook, error) { - fields := []string{"`name`", "`url`", "`creator_id`"} - placeholder := []string{"?", "?", "?"} - args := []any{create.Name, create.URL, create.CreatorID} - stmt := "INSERT INTO `webhook` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`" - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &create.ID, - &create.CreatedTs, - &create.UpdatedTs, - ); err != nil { - return nil, err - } - webhook := create - return webhook, nil -} - -func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*store.Webhook, error) { - where, args := []string{"1 = 1"}, []any{} - if find.ID != nil { - where, args = append(where, "`id` = ?"), append(args, *find.ID) - } - if find.CreatorID != nil { - where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID) - } - - rows, err := d.db.QueryContext(ctx, ` - SELECT - id, - created_ts, - updated_ts, - creator_id, - name, - url - FROM webhook - WHERE `+strings.Join(where, " AND ")+` - ORDER BY id DESC`, - args..., - ) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Webhook{} - for rows.Next() { - webhook := &store.Webhook{} - if err := rows.Scan( - &webhook.ID, - &webhook.CreatedTs, - &webhook.UpdatedTs, - &webhook.CreatorID, - &webhook.Name, - &webhook.URL, - ); err != nil { - return nil, err - } - list = append(list, webhook) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} - -func (d *DB) UpdateWebhook(ctx context.Context, update *store.UpdateWebhook) (*store.Webhook, error) { - set, args := []string{}, []any{} - if update.Name != nil { - set, args = append(set, "name = ?"), append(args, *update.Name) - } - if update.URL != nil { - set, args = append(set, "url = ?"), append(args, *update.URL) - } - args = append(args, update.ID) - - stmt := "UPDATE `webhook` SET " + strings.Join(set, ", ") + " WHERE `id` = ? RETURNING `id`, `created_ts`, `updated_ts`, `creator_id`, `name`, `url`" - webhook := &store.Webhook{} - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &webhook.ID, - &webhook.CreatedTs, - &webhook.UpdatedTs, - &webhook.CreatorID, - &webhook.Name, - &webhook.URL, - ); err != nil { - return nil, err - } - return webhook, nil -} - -func (d *DB) DeleteWebhook(ctx context.Context, delete *store.DeleteWebhook) error { - _, err := d.db.ExecContext(ctx, "DELETE FROM `webhook` WHERE `id` = ?", delete.ID) - return err -} diff --git a/store/driver.go b/store/driver.go index da893e744..48fd6287e 100644 --- a/store/driver.go +++ b/store/driver.go @@ -69,12 +69,6 @@ type Driver interface { UpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error) DeleteInbox(ctx context.Context, delete *DeleteInbox) error - // Webhook model related methods. - CreateWebhook(ctx context.Context, create *Webhook) (*Webhook, error) - ListWebhooks(ctx context.Context, find *FindWebhook) ([]*Webhook, error) - UpdateWebhook(ctx context.Context, update *UpdateWebhook) (*Webhook, error) - DeleteWebhook(ctx context.Context, delete *DeleteWebhook) error - // Reaction model related methods. UpsertReaction(ctx context.Context, create *Reaction) (*Reaction, error) ListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error) diff --git a/store/test/webhook_test.go b/store/test/webhook_test.go deleted file mode 100644 index 614919329..000000000 --- a/store/test/webhook_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package teststore - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/usememos/memos/store" -) - -func TestWebhookStore(t *testing.T) { - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - webhook, err := ts.CreateWebhook(ctx, &store.Webhook{ - CreatorID: user.ID, - Name: "test_webhook", - URL: "https://example.com", - }) - require.NoError(t, err) - require.Equal(t, "test_webhook", webhook.Name) - require.Equal(t, user.ID, webhook.CreatorID) - webhooks, err := ts.ListWebhooks(ctx, &store.FindWebhook{ - CreatorID: &user.ID, - }) - require.NoError(t, err) - require.Equal(t, 1, len(webhooks)) - require.Equal(t, webhook, webhooks[0]) - newName := "test_webhook_new" - updatedWebhook, err := ts.UpdateWebhook(ctx, &store.UpdateWebhook{ - ID: webhook.ID, - Name: &newName, - }) - require.NoError(t, err) - require.Equal(t, newName, updatedWebhook.Name) - require.Equal(t, webhook.CreatorID, updatedWebhook.CreatorID) - err = ts.DeleteWebhook(ctx, &store.DeleteWebhook{ - ID: webhook.ID, - }) - require.NoError(t, err) - webhooks, err = ts.ListWebhooks(ctx, &store.FindWebhook{ - CreatorID: &user.ID, - }) - require.NoError(t, err) - require.Equal(t, 0, len(webhooks)) - ts.Close() -} diff --git a/store/user_setting.go b/store/user_setting.go index 3cfbdfca3..3c48b971c 100644 --- a/store/user_setting.go +++ b/store/user_setting.go @@ -241,6 +241,114 @@ func (s *Store) UpdateUserSessionLastAccessed(ctx context.Context, userID int32, return err } +// GetUserWebhooks returns the webhooks of the user. +func (s *Store) GetUserWebhooks(ctx context.Context, userID int32) ([]*storepb.WebhooksUserSetting_Webhook, error) { + userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_WEBHOOKS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return []*storepb.WebhooksUserSetting_Webhook{}, nil + } + + webhooksUserSetting := userSetting.GetWebhooks() + return webhooksUserSetting.Webhooks, nil +} + +// AddUserWebhook adds a new webhook for the user. +func (s *Store) AddUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error { + existingWebhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + // Check if webhook already exists, update if it does + var updatedWebhooks []*storepb.WebhooksUserSetting_Webhook + webhookExists := false + for _, existing := range existingWebhooks { + if existing.Id == webhook.Id { + updatedWebhooks = append(updatedWebhooks, webhook) + webhookExists = true + } else { + updatedWebhooks = append(updatedWebhooks, existing) + } + } + + // If webhook doesn't exist, add it + if !webhookExists { + updatedWebhooks = append(updatedWebhooks, webhook) + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: updatedWebhooks, + }, + }, + }) + + return err +} + +// RemoveUserWebhook removes the webhook of the user. +func (s *Store) RemoveUserWebhook(ctx context.Context, userID int32, webhookID string) error { + oldWebhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + newWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(oldWebhooks)) + for _, webhook := range oldWebhooks { + if webhookID != webhook.Id { + newWebhooks = append(newWebhooks, webhook) + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: newWebhooks, + }, + }, + }) + + return err +} + +// UpdateUserWebhook updates an existing webhook for the user. +func (s *Store) UpdateUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error { + webhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + for i, existing := range webhooks { + if existing.Id == webhook.Id { + webhooks[i] = webhook + break + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: webhooks, + }, + }, + }) + + return err +} + func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) { userSetting := &storepb.UserSetting{ UserId: raw.UserID, @@ -272,6 +380,12 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) { return nil, err } userSetting.Value = &storepb.UserSetting_General{General: generalUserSetting} + case storepb.UserSetting_WEBHOOKS: + webhooksUserSetting := &storepb.WebhooksUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), webhooksUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_Webhooks{Webhooks: webhooksUserSetting} default: return nil, nil } @@ -313,6 +427,13 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er return nil, err } raw.Value = string(value) + case storepb.UserSetting_WEBHOOKS: + webhooksUserSetting := userSetting.GetWebhooks() + value, err := protojson.Marshal(webhooksUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) default: return nil, errors.Errorf("unsupported user setting key: %v", userSetting.Key) } diff --git a/store/webhook.go b/store/webhook.go deleted file mode 100644 index deffd077b..000000000 --- a/store/webhook.go +++ /dev/null @@ -1,56 +0,0 @@ -package store - -import ( - "context" -) - -type Webhook struct { - ID int32 - CreatedTs int64 - UpdatedTs int64 - CreatorID int32 - Name string - URL string -} - -type FindWebhook struct { - ID *int32 - CreatorID *int32 -} - -type UpdateWebhook struct { - ID int32 - Name *string - URL *string -} - -type DeleteWebhook struct { - ID int32 -} - -func (s *Store) CreateWebhook(ctx context.Context, create *Webhook) (*Webhook, error) { - return s.driver.CreateWebhook(ctx, create) -} - -func (s *Store) ListWebhooks(ctx context.Context, find *FindWebhook) ([]*Webhook, error) { - return s.driver.ListWebhooks(ctx, find) -} - -func (s *Store) GetWebhook(ctx context.Context, find *FindWebhook) (*Webhook, error) { - list, err := s.ListWebhooks(ctx, find) - if err != nil { - return nil, err - } - if len(list) == 0 { - return nil, nil - } - return list[0], nil -} - -func (s *Store) UpdateWebhook(ctx context.Context, update *UpdateWebhook) (*Webhook, error) { - return s.driver.UpdateWebhook(ctx, update) -} - -func (s *Store) DeleteWebhook(ctx context.Context, delete *DeleteWebhook) error { - return s.driver.DeleteWebhook(ctx, delete) -} diff --git a/web/src/components/CreateWebhookDialog.tsx b/web/src/components/CreateWebhookDialog.tsx index dfb6f4e85..aa73f13ee 100644 --- a/web/src/components/CreateWebhookDialog.tsx +++ b/web/src/components/CreateWebhookDialog.tsx @@ -3,6 +3,7 @@ import { XIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { webhookServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; import { useTranslate } from "@/utils/i18n"; import { generateDialog } from "./Dialog"; @@ -20,6 +21,7 @@ interface State { const CreateWebhookDialog: React.FC = (props: Props) => { const { webhookName, destroy, onConfirm } = props; const t = useTranslate(); + const currentUser = useCurrentUser(); const [state, setState] = useState({ displayName: "", url: "", @@ -67,9 +69,15 @@ const CreateWebhookDialog: React.FC = (props: Props) => { return; } + if (!currentUser) { + toast.error("User not authenticated"); + return; + } + try { if (isCreating) { await webhookServiceClient.createWebhook({ + parent: currentUser.name, webhook: { displayName: state.displayName, url: state.url, diff --git a/web/src/components/Settings/WebhookSection.tsx b/web/src/components/Settings/WebhookSection.tsx index 5bd118087..80715eb11 100644 --- a/web/src/components/Settings/WebhookSection.tsx +++ b/web/src/components/Settings/WebhookSection.tsx @@ -3,26 +3,31 @@ import { ExternalLinkIcon, TrashIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { webhookServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; import { Webhook } from "@/types/proto/api/v1/webhook_service"; import { useTranslate } from "@/utils/i18n"; import showCreateWebhookDialog from "../CreateWebhookDialog"; -const listWebhooks = async () => { - const { webhooks } = await webhookServiceClient.listWebhooks({}); - return webhooks; -}; - const WebhookSection = () => { const t = useTranslate(); + const currentUser = useCurrentUser(); const [webhooks, setWebhooks] = useState([]); + const listWebhooks = async () => { + if (!currentUser) return []; + const { webhooks } = await webhookServiceClient.listWebhooks({ + parent: currentUser.name, + }); + return webhooks; + }; + useEffect(() => { listWebhooks().then((webhooks) => { setWebhooks(webhooks); }); - }, []); + }, [currentUser]); - const handleCreateAccessTokenDialogConfirm = async () => { + const handleCreateWebhookDialogConfirm = async () => { const webhooks = await listWebhooks(); setWebhooks(webhooks); }; @@ -47,7 +52,7 @@ const WebhookSection = () => {