package v1 import ( "context" "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" v1pb "github.com/usememos/memos/proto/gen/api/v1" "github.com/usememos/memos/store" ) func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWebhookRequest) (*v1pb.Webhook, error) { currentUser, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } if currentUser == nil { return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") } // Only host users can create webhooks if !isSuperUser(currentUser) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Validate required fields if request.Webhook == nil { return nil, status.Errorf(codes.InvalidArgument, "webhook is required") } if strings.TrimSpace(request.Webhook.Url) == "" { return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required") } // TODO: Handle webhook_id, validate_only, and request_id fields if request.ValidateOnly { // Perform validation checks without actually creating the webhook return &v1pb.Webhook{ DisplayName: request.Webhook.DisplayName, Url: request.Webhook.Url, Creator: fmt.Sprintf("users/%d", currentUser.ID), State: request.Webhook.State, }, nil } webhook, err := s.Store.CreateWebhook(ctx, &store.Webhook{ CreatorID: currentUser.ID, Name: 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 } func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *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) } if currentUser == nil { 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, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err) } response := &v1pb.ListWebhooksResponse{ Webhooks: []*v1pb.Webhook{}, TotalSize: int32(len(webhooks)), } for _, webhook := range webhooks { response.Webhooks = append(response.Webhooks, convertWebhookFromStore(webhook)) } return response, nil } func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) { webhookID, err := ExtractWebhookIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } currentUser, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } if currentUser == nil { 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") } webhookPb := convertWebhookFromStore(webhook) // TODO: Implement read_mask field filtering return webhookPb, nil } func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) { if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") } webhookID, err := ExtractWebhookIDFromName(request.Webhook.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } currentUser, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } if currentUser == nil { 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, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err) } if existingWebhook == nil { if request.AllowMissing { // Could create webhook if missing, but for now return not found return nil, status.Errorf(codes.NotFound, "webhook not found") } return nil, status.Errorf(codes.NotFound, "webhook not found") } update := &store.UpdateWebhook{ ID: webhookID, } for _, field := range request.UpdateMask.Paths { switch field { case "display_name": update.Name = &request.Webhook.DisplayName case "url": update.URL = &request.Webhook.Url default: return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) } } webhook, err := s.Store.UpdateWebhook(ctx, update) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update webhook, error: %+v", err) } return convertWebhookFromStore(webhook), nil } func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) { webhookID, err := ExtractWebhookIDFromName(request.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) } currentUser, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } if currentUser == nil { 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, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get webhook: %v", err) } if webhook == nil { return nil, status.Errorf(codes.NotFound, "webhook not found") } // TODO: Handle force field properly err = s.Store.DeleteWebhook(ctx, &store.DeleteWebhook{ ID: webhookID, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to delete webhook, error: %+v", err) } return &emptypb.Empty{}, nil } func convertWebhookFromStore(webhook *store.Webhook) *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)), } }