//go:build !noserver package cmd import ( "errors" "fmt" "github.com/urfave/cli/v2" "heckel.io/ntfy/user" "heckel.io/ntfy/util" "time" ) func init() { commands = append(commands, cmdTier) } const ( defaultMessageLimit = 5000 defaultMessageExpiryDuration = 12 * time.Hour defaultEmailLimit = 20 defaultReservationLimit = 3 defaultAttachmentFileSizeLimit = "15M" defaultAttachmentTotalSizeLimit = "100M" defaultAttachmentExpiryDuration = 6 * time.Hour defaultAttachmentBandwidthLimit = "1G" ) var ( flagsTier = append([]cli.Flag{}, flagsUser...) ) var cmdTier = &cli.Command{ Name: "tier", Usage: "Manage/show tiers", UsageText: "ntfy tier [list|add|remove] ...", Flags: flagsTier, Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc), Category: categoryServer, Subcommands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "Adds a new tier", UsageText: "ntfy tier add [OPTIONS] CODE", Action: execTierAdd, Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Usage: "tier name"}, &cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"}, &cli.DurationFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"}, &cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"}, &cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"}, &cli.DurationFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"}, &cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"}, &cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"}, }, Description: ` FIXME `, }, { Name: "change", Aliases: []string{"ch"}, Usage: "Change a tier", UsageText: "ntfy tier change [OPTIONS] CODE", Action: execTierChange, Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Usage: "tier name"}, &cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"}, &cli.DurationFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"}, &cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"}, &cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"}, &cli.DurationFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"}, &cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"}, &cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"}, }, Description: ` FIXME `, }, { Name: "remove", Aliases: []string{"del", "rm"}, Usage: "Removes a tier", UsageText: "ntfy tier remove CODE", Action: execTierDel, Description: ` FIXME `, }, { Name: "list", Aliases: []string{"l"}, Usage: "Shows a list of tiers", Action: execTierList, Description: ` FIXME `, }, }, Description: `Manage tier of the ntfy server. The command allows you to add/remove/change tier in the ntfy user database. Tiers are used to grant users higher limits based on their tier. This is a server-only command. It directly manages the user.db as defined in the server config file server.yml. The command only works if 'auth-file' is properly defined. Please also refer to the related command 'ntfy access'. FIXME `, } func execTierAdd(c *cli.Context) error { code := c.Args().Get(0) if code == "" { return errors.New("tier code expected, type 'ntfy tier add --help' for help") } else if !user.AllowedTier(code) { return errors.New("tier code must consist only of numbers and letters") } manager, err := createUserManager(c) if err != nil { return err } if tier, _ := manager.Tier(code); tier != nil { return fmt.Errorf("tier %s already exists", code) } name := c.String("name") if name == "" { name = code } attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit")) if err != nil { return err } attachmentTotalSizeLimit, err := util.ParseSize(c.String("attachment-total-size-limit")) if err != nil { return err } attachmentBandwidthLimit, err := util.ParseSize(c.String("attachment-bandwidth-limit")) if err != nil { return err } tier := &user.Tier{ ID: "", // Generated Code: code, Name: name, MessageLimit: c.Int64("message-limit"), MessageExpiryDuration: c.Duration("message-expiry-duration"), EmailLimit: c.Int64("email-limit"), ReservationLimit: c.Int64("reservation-limit"), AttachmentFileSizeLimit: attachmentFileSizeLimit, AttachmentTotalSizeLimit: attachmentTotalSizeLimit, AttachmentExpiryDuration: c.Duration("attachment-expiry-duration"), AttachmentBandwidthLimit: attachmentBandwidthLimit, StripePriceID: c.String("stripe-price-id"), } if err := manager.AddTier(tier); err != nil { return err } tier, err = manager.Tier(code) if err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "tier added\n\n") printTier(c, tier) return nil } func execTierChange(c *cli.Context) error { code := c.Args().Get(0) if code == "" { return errors.New("tier code expected, type 'ntfy tier change --help' for help") } else if !user.AllowedTier(code) { return errors.New("tier code must consist only of numbers and letters") } manager, err := createUserManager(c) if err != nil { return err } tier, err := manager.Tier(code) if err == user.ErrTierNotFound { return fmt.Errorf("tier %s does not exist", code) } else if err != nil { return err } if c.IsSet("name") { tier.Name = c.String("name") } if c.IsSet("message-limit") { tier.MessageLimit = c.Int64("message-limit") } if c.IsSet("message-expiry-duration") { tier.MessageExpiryDuration = c.Duration("message-expiry-duration") } if c.IsSet("email-limit") { tier.EmailLimit = c.Int64("email-limit") } if c.IsSet("reservation-limit") { tier.ReservationLimit = c.Int64("reservation-limit") } if c.IsSet("attachment-file-size-limit") { tier.AttachmentFileSizeLimit, err = util.ParseSize(c.String("attachment-file-size-limit")) if err != nil { return err } } if c.IsSet("attachment-total-size-limit") { tier.AttachmentTotalSizeLimit, err = util.ParseSize(c.String("attachment-total-size-limit")) if err != nil { return err } } if c.IsSet("attachment-expiry-duration") { tier.AttachmentExpiryDuration = c.Duration("attachment-expiry-duration") } if c.IsSet("attachment-bandwidth-limit") { tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit")) if err != nil { return err } } if c.IsSet("stripe-price-id") { tier.StripePriceID = c.String("stripe-price-id") } if err := manager.UpdateTier(tier); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "tier updated\n\n") printTier(c, tier) return nil } func execTierDel(c *cli.Context) error { code := c.Args().Get(0) if code == "" { return errors.New("tier code expected, type 'ntfy tier del --help' for help") } manager, err := createUserManager(c) if err != nil { return err } if _, err := manager.Tier(code); err == user.ErrTierNotFound { return fmt.Errorf("tier %s does not exist", code) } if err := manager.RemoveTier(code); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "tier %s removed\n", code) return nil } func execTierList(c *cli.Context) error { manager, err := createUserManager(c) if err != nil { return err } tiers, err := manager.Tiers() if err != nil { return err } for _, tier := range tiers { printTier(c, tier) } return nil } func printTier(c *cli.Context, tier *user.Tier) { stripePriceID := tier.StripePriceID if stripePriceID == "" { stripePriceID = "(none)" } fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID) fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name) fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit) fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds())) fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit) fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit) fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit)) fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit)) fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds())) fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit)) fmt.Fprintf(c.App.ErrWriter, "- Stripe price: %s\n", stripePriceID) }