package v1 import ( "context" "fmt" "strings" "time" "github.com/mssola/useragent" "github.com/pkg/errors" "golang.org/x/exp/slices" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" v1pb "github.com/yourselfhosted/slash/proto/gen/api/v1" storepb "github.com/yourselfhosted/slash/proto/gen/store" "github.com/yourselfhosted/slash/server/service/license" "github.com/yourselfhosted/slash/store" ) func (s *APIV1Service) ListShortcuts(ctx context.Context, _ *v1pb.ListShortcutsRequest) (*v1pb.ListShortcutsResponse, error) { user, err := getCurrentUser(ctx, s.Store) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) } find := &store.FindShortcut{} find.VisibilityList = []store.Visibility{store.VisibilityWorkspace, store.VisibilityPublic} visibleShortcutList, err := s.Store.ListShortcuts(ctx, find) if err != nil { return nil, status.Errorf(codes.Internal, "failed to fetch visible shortcut list, err: %v", err) } find.VisibilityList = []store.Visibility{store.VisibilityPrivate} find.CreatorID = &user.ID shortcutList, err := s.Store.ListShortcuts(ctx, find) if err != nil { return nil, status.Errorf(codes.Internal, "failed to fetch private shortcut list, err: %v", err) } shortcutList = append(shortcutList, visibleShortcutList...) shortcuts := []*v1pb.Shortcut{} for _, shortcut := range shortcutList { composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) } shortcuts = append(shortcuts, composedShortcut) } response := &v1pb.ListShortcutsResponse{ Shortcuts: shortcuts, } return response, nil } func (s *APIV1Service) GetShortcut(ctx context.Context, request *v1pb.GetShortcutRequest) (*v1pb.Shortcut, error) { shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ ID: &request.Id, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut by id: %v", err) } if shortcut == nil { return nil, status.Errorf(codes.NotFound, "shortcut not found") } userID, ok := ctx.Value(userIDContextKey).(int32) if ok { if shortcut.Visibility == storepb.Visibility_PRIVATE && shortcut.CreatorId != userID { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } } else { if shortcut.Visibility != storepb.Visibility_PUBLIC { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } } composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) } return composedShortcut, nil } func (s *APIV1Service) GetShortcutByName(ctx context.Context, request *v1pb.GetShortcutByNameRequest) (*v1pb.Shortcut, error) { shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ Name: &request.Name, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut by name: %v", err) } if shortcut == nil { return nil, status.Errorf(codes.NotFound, "shortcut not found") } userID, ok := ctx.Value(userIDContextKey).(int32) if ok { if shortcut.Visibility == storepb.Visibility_PRIVATE && shortcut.CreatorId != userID { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } } else { if shortcut.Visibility != storepb.Visibility_PUBLIC { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } } composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) } return composedShortcut, nil } func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateShortcutRequest) (*v1pb.Shortcut, error) { if request.Shortcut.Name == "" || request.Shortcut.Link == "" { return nil, status.Errorf(codes.InvalidArgument, "name and link are required") } if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeUnlimitedShortcuts) { shortcuts, err := s.Store.ListShortcuts(ctx, &store.FindShortcut{}) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut list, err: %v", err) } shortcutsLimit := int(s.LicenseService.GetSubscription().ShortcutsLimit) if len(shortcuts) >= shortcutsLimit { return nil, status.Errorf(codes.PermissionDenied, "Maximum number of shortcuts %d reached", shortcutsLimit) } } user, err := getCurrentUser(ctx, s.Store) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) } shortcutCreate := &storepb.Shortcut{ CreatorId: user.ID, Name: request.Shortcut.Name, Link: request.Shortcut.Link, Title: request.Shortcut.Title, Tags: request.Shortcut.Tags, Description: request.Shortcut.Description, Visibility: storepb.Visibility(request.Shortcut.Visibility), OgMetadata: &storepb.OpenGraphMetadata{}, } if shortcutCreate.Visibility == storepb.Visibility_VISIBILITY_UNSPECIFIED { workspaceSetting, err := s.GetWorkspaceSetting(ctx, nil) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get workspace setting, err: %v", err) } visibility := v1pb.Visibility_PRIVATE if workspaceSetting.DefaultVisibility != v1pb.Visibility_VISIBILITY_UNSPECIFIED { visibility = workspaceSetting.DefaultVisibility } shortcutCreate.Visibility = storepb.Visibility(visibility) } if request.Shortcut.OgMetadata != nil { shortcutCreate.OgMetadata = &storepb.OpenGraphMetadata{ Title: request.Shortcut.OgMetadata.Title, Description: request.Shortcut.OgMetadata.Description, Image: request.Shortcut.OgMetadata.Image, } } shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create shortcut, err: %v", err) } if err := s.createShortcutCreateActivity(ctx, shortcut); err != nil { return nil, status.Errorf(codes.Internal, "failed to create activity, err: %v", err) } composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) } return composedShortcut, nil } func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateShortcutRequest) (*v1pb.Shortcut, error) { if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { return nil, status.Errorf(codes.InvalidArgument, "updateMask is required") } user, err := getCurrentUser(ctx, s.Store) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) } shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ ID: &request.Shortcut.Id, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut by id: %v", err) } if shortcut == nil { return nil, status.Errorf(codes.NotFound, "shortcut not found") } if shortcut.CreatorId != user.ID && user.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } update := &store.UpdateShortcut{ ID: shortcut.Id, } for _, path := range request.UpdateMask.Paths { switch path { case "name": update.Name = &request.Shortcut.Name case "link": update.Link = &request.Shortcut.Link case "title": update.Title = &request.Shortcut.Title case "description": update.Description = &request.Shortcut.Description case "tags": tag := strings.Join(request.Shortcut.Tags, " ") update.Tag = &tag case "visibility": visibility := store.Visibility(request.Shortcut.Visibility.String()) update.Visibility = &visibility case "og_metadata": if request.Shortcut.OgMetadata != nil { update.OpenGraphMetadata = &storepb.OpenGraphMetadata{ Title: request.Shortcut.OgMetadata.Title, Description: request.Shortcut.OgMetadata.Description, Image: request.Shortcut.OgMetadata.Image, } } } } shortcut, err = s.Store.UpdateShortcut(ctx, update) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update shortcut, err: %v", err) } composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) } return composedShortcut, nil } func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteShortcutRequest) (*emptypb.Empty, error) { user, err := getCurrentUser(ctx, s.Store) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) } shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ ID: &request.Id, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut by id: %v", err) } if shortcut == nil { return nil, status.Errorf(codes.NotFound, "shortcut not found") } if shortcut.CreatorId != user.ID && user.Role != store.RoleAdmin { return nil, status.Errorf(codes.PermissionDenied, "Permission denied") } err = s.Store.DeleteShortcut(ctx, &store.DeleteShortcut{ ID: shortcut.Id, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to delete shortcut, err: %v", err) } return &emptypb.Empty{}, nil } func (s *APIV1Service) GetShortcutAnalytics(ctx context.Context, request *v1pb.GetShortcutAnalyticsRequest) (*v1pb.GetShortcutAnalyticsResponse, error) { shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ ID: &request.Id, }) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get shortcut by id: %v", err) } if shortcut == nil { return nil, status.Errorf(codes.NotFound, "shortcut not found") } activityFind := &store.FindActivity{ Type: store.ActivityShortcutView, PayloadShortcutID: &shortcut.Id, } if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeAdvancedAnalytics) { createdTsAfter := time.Now().AddDate(0, 0, -14).Unix() activityFind.CreatedTsAfter = &createdTsAfter } activities, err := s.Store.ListActivities(ctx, activityFind) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get activities, err: %v", err) } referenceMap := make(map[string]int32) deviceMap := make(map[string]int32) browserMap := make(map[string]int32) for _, activity := range activities { payload := &storepb.ActivityShorcutViewPayload{} if err := protojson.Unmarshal([]byte(activity.Payload), payload); err != nil { return nil, status.Error(codes.Internal, fmt.Sprintf("failed to unmarshal payload, err: %v", err)) } if _, ok := referenceMap[payload.Referer]; !ok { referenceMap[payload.Referer] = 0 } referenceMap[payload.Referer]++ ua := useragent.New(payload.UserAgent) deviceName := ua.OSInfo().Name browserName, _ := ua.Browser() if _, ok := deviceMap[deviceName]; !ok { deviceMap[deviceName] = 0 } deviceMap[deviceName]++ if _, ok := browserMap[browserName]; !ok { browserMap[browserName] = 0 } browserMap[browserName]++ } response := &v1pb.GetShortcutAnalyticsResponse{ References: mapToAnalyticsSlice(referenceMap), Devices: mapToAnalyticsSlice(deviceMap), Browsers: mapToAnalyticsSlice(browserMap), } return response, nil } func mapToAnalyticsSlice(m map[string]int32) []*v1pb.GetShortcutAnalyticsResponse_AnalyticsItem { analyticsSlice := make([]*v1pb.GetShortcutAnalyticsResponse_AnalyticsItem, 0) for key, value := range m { analyticsSlice = append(analyticsSlice, &v1pb.GetShortcutAnalyticsResponse_AnalyticsItem{ Name: key, Count: value, }) } slices.SortFunc(analyticsSlice, func(i, j *v1pb.GetShortcutAnalyticsResponse_AnalyticsItem) int { return int(i.Count - j.Count) }) return analyticsSlice } func (s *APIV1Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error { payload := &storepb.ActivityShorcutCreatePayload{ ShortcutId: shortcut.Id, } payloadStr, err := protojson.Marshal(payload) if err != nil { return errors.Wrap(err, "Failed to marshal activity payload") } activity := &store.Activity{ CreatorID: shortcut.CreatorId, Type: store.ActivityShortcutCreate, Level: store.ActivityInfo, Payload: string(payloadStr), } _, err = s.Store.CreateActivity(ctx, activity) if err != nil { return errors.Wrap(err, "Failed to create activity") } return nil } func (s *APIV1Service) convertShortcutFromStorepb(ctx context.Context, shortcut *storepb.Shortcut) (*v1pb.Shortcut, error) { composedShortcut := &v1pb.Shortcut{ Id: shortcut.Id, CreatorId: shortcut.CreatorId, CreatedTime: timestamppb.New(time.Unix(shortcut.CreatedTs, 0)), UpdatedTime: timestamppb.New(time.Unix(shortcut.UpdatedTs, 0)), RowStatus: v1pb.RowStatus(shortcut.RowStatus), Name: shortcut.Name, Link: shortcut.Link, Title: shortcut.Title, Tags: shortcut.Tags, Description: shortcut.Description, Visibility: v1pb.Visibility(shortcut.Visibility), OgMetadata: &v1pb.Shortcut_OpenGraphMetadata{ Title: shortcut.OgMetadata.Title, Description: shortcut.OgMetadata.Description, Image: shortcut.OgMetadata.Image, }, } activityList, err := s.Store.ListActivities(ctx, &store.FindActivity{ Type: store.ActivityShortcutView, Level: store.ActivityInfo, PayloadShortcutID: &composedShortcut.Id, }) if err != nil { return nil, errors.Wrap(err, "Failed to list activities") } composedShortcut.ViewCount = int32(len(activityList)) return composedShortcut, nil }