tier.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. //go:build !noserver
  2. package cmd
  3. import (
  4. "errors"
  5. "fmt"
  6. "github.com/urfave/cli/v2"
  7. "heckel.io/ntfy/user"
  8. "heckel.io/ntfy/util"
  9. "time"
  10. )
  11. func init() {
  12. commands = append(commands, cmdTier)
  13. }
  14. const (
  15. defaultMessageLimit = 5000
  16. defaultMessageExpiryDuration = 12 * time.Hour
  17. defaultEmailLimit = 20
  18. defaultReservationLimit = 3
  19. defaultAttachmentFileSizeLimit = "15M"
  20. defaultAttachmentTotalSizeLimit = "100M"
  21. defaultAttachmentExpiryDuration = 6 * time.Hour
  22. defaultAttachmentBandwidthLimit = "1G"
  23. )
  24. var (
  25. flagsTier = append([]cli.Flag{}, flagsUser...)
  26. )
  27. var cmdTier = &cli.Command{
  28. Name: "tier",
  29. Usage: "Manage/show tiers",
  30. UsageText: "ntfy tier [list|add|change|remove] ...",
  31. Flags: flagsTier,
  32. Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc),
  33. Category: categoryServer,
  34. Subcommands: []*cli.Command{
  35. {
  36. Name: "add",
  37. Aliases: []string{"a"},
  38. Usage: "Adds a new tier",
  39. UsageText: "ntfy tier add [OPTIONS] CODE",
  40. Action: execTierAdd,
  41. Flags: []cli.Flag{
  42. &cli.StringFlag{Name: "name", Usage: "tier name"},
  43. &cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
  44. &cli.DurationFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
  45. &cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
  46. &cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
  47. &cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
  48. &cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
  49. &cli.DurationFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
  50. &cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
  51. &cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
  52. },
  53. Description: `Add a new tier to the ntfy user database.
  54. Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or
  55. make it possible for users to reserve topics.
  56. This is a server-only command. It directly reads from user.db as defined in the server config
  57. file server.yml. The command only works if 'auth-file' is properly defined.
  58. Examples:
  59. ntfy tier add pro # Add tier with code "pro", using the defaults
  60. ntfy tier add \ # Add a tier with custom limits
  61. --name="Pro" \
  62. --message-limit=10000 \
  63. --message-expiry-duration=24h \
  64. --email-limit=50 \
  65. --reservation-limit=10 \
  66. --attachment-file-size-limit=100M \
  67. --attachment-total-size-limit=1G \
  68. --attachment-expiry-duration=12h \
  69. --attachment-bandwidth-limit=5G \
  70. pro
  71. `,
  72. },
  73. {
  74. Name: "change",
  75. Aliases: []string{"ch"},
  76. Usage: "Change a tier",
  77. UsageText: "ntfy tier change [OPTIONS] CODE",
  78. Action: execTierChange,
  79. Flags: []cli.Flag{
  80. &cli.StringFlag{Name: "name", Usage: "tier name"},
  81. &cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
  82. &cli.DurationFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
  83. &cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
  84. &cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
  85. &cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
  86. &cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
  87. &cli.DurationFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
  88. &cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
  89. &cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
  90. },
  91. Description: `Updates a tier to change the limits.
  92. After updating a tier, you may have to restart the ntfy server to apply them
  93. to all visitors.
  94. This is a server-only command. It directly reads from user.db as defined in the server config
  95. file server.yml. The command only works if 'auth-file' is properly defined.
  96. Examples:
  97. ntfy tier change --name="Pro" pro # Update the name of an existing tier
  98. ntfy tier change \ # Update multiple limits and fields
  99. --message-expiry-duration=24h \
  100. --stripe-price-id=price_1234 \
  101. pro
  102. `,
  103. },
  104. {
  105. Name: "remove",
  106. Aliases: []string{"del", "rm"},
  107. Usage: "Removes a tier",
  108. UsageText: "ntfy tier remove CODE",
  109. Action: execTierDel,
  110. Description: `Remove a tier from the ntfy user database.
  111. You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
  112. to remove or switch their tier first.
  113. This is a server-only command. It directly reads from user.db as defined in the server config
  114. file server.yml. The command only works if 'auth-file' is properly defined.
  115. Example:
  116. ntfy tier del pro
  117. `,
  118. },
  119. {
  120. Name: "list",
  121. Aliases: []string{"l"},
  122. Usage: "Shows a list of tiers",
  123. Action: execTierList,
  124. Description: `Shows a list of all configured tiers.
  125. This is a server-only command. It directly reads from user.db as defined in the server config
  126. file server.yml. The command only works if 'auth-file' is properly defined.
  127. `,
  128. },
  129. },
  130. Description: `Manage tiers of the ntfy server.
  131. The command allows you to add/remove/change tiers in the ntfy user database. Tiers are used
  132. to grant users higher limits, such as daily message limits, attachment size, or make it
  133. possible for users to reserve topics.
  134. This is a server-only command. It directly manages the user.db as defined in the server config
  135. file server.yml. The command only works if 'auth-file' is properly defined.
  136. Examples:
  137. ntfy tier add pro # Add tier with code "pro", using the defaults
  138. ntfy tier change --name="Pro" pro # Update the name of an existing tier
  139. ntfy tier del pro # Delete an existing tier
  140. `,
  141. }
  142. func execTierAdd(c *cli.Context) error {
  143. code := c.Args().Get(0)
  144. if code == "" {
  145. return errors.New("tier code expected, type 'ntfy tier add --help' for help")
  146. } else if !user.AllowedTier(code) {
  147. return errors.New("tier code must consist only of numbers and letters")
  148. }
  149. manager, err := createUserManager(c)
  150. if err != nil {
  151. return err
  152. }
  153. if tier, _ := manager.Tier(code); tier != nil {
  154. return fmt.Errorf("tier %s already exists", code)
  155. }
  156. name := c.String("name")
  157. if name == "" {
  158. name = code
  159. }
  160. attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit"))
  161. if err != nil {
  162. return err
  163. }
  164. attachmentTotalSizeLimit, err := util.ParseSize(c.String("attachment-total-size-limit"))
  165. if err != nil {
  166. return err
  167. }
  168. attachmentBandwidthLimit, err := util.ParseSize(c.String("attachment-bandwidth-limit"))
  169. if err != nil {
  170. return err
  171. }
  172. tier := &user.Tier{
  173. ID: "", // Generated
  174. Code: code,
  175. Name: name,
  176. MessageLimit: c.Int64("message-limit"),
  177. MessageExpiryDuration: c.Duration("message-expiry-duration"),
  178. EmailLimit: c.Int64("email-limit"),
  179. ReservationLimit: c.Int64("reservation-limit"),
  180. AttachmentFileSizeLimit: attachmentFileSizeLimit,
  181. AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
  182. AttachmentExpiryDuration: c.Duration("attachment-expiry-duration"),
  183. AttachmentBandwidthLimit: attachmentBandwidthLimit,
  184. StripePriceID: c.String("stripe-price-id"),
  185. }
  186. if err := manager.AddTier(tier); err != nil {
  187. return err
  188. }
  189. tier, err = manager.Tier(code)
  190. if err != nil {
  191. return err
  192. }
  193. fmt.Fprintf(c.App.ErrWriter, "tier added\n\n")
  194. printTier(c, tier)
  195. return nil
  196. }
  197. func execTierChange(c *cli.Context) error {
  198. code := c.Args().Get(0)
  199. if code == "" {
  200. return errors.New("tier code expected, type 'ntfy tier change --help' for help")
  201. } else if !user.AllowedTier(code) {
  202. return errors.New("tier code must consist only of numbers and letters")
  203. }
  204. manager, err := createUserManager(c)
  205. if err != nil {
  206. return err
  207. }
  208. tier, err := manager.Tier(code)
  209. if err == user.ErrTierNotFound {
  210. return fmt.Errorf("tier %s does not exist", code)
  211. } else if err != nil {
  212. return err
  213. }
  214. if c.IsSet("name") {
  215. tier.Name = c.String("name")
  216. }
  217. if c.IsSet("message-limit") {
  218. tier.MessageLimit = c.Int64("message-limit")
  219. }
  220. if c.IsSet("message-expiry-duration") {
  221. tier.MessageExpiryDuration = c.Duration("message-expiry-duration")
  222. }
  223. if c.IsSet("email-limit") {
  224. tier.EmailLimit = c.Int64("email-limit")
  225. }
  226. if c.IsSet("reservation-limit") {
  227. tier.ReservationLimit = c.Int64("reservation-limit")
  228. }
  229. if c.IsSet("attachment-file-size-limit") {
  230. tier.AttachmentFileSizeLimit, err = util.ParseSize(c.String("attachment-file-size-limit"))
  231. if err != nil {
  232. return err
  233. }
  234. }
  235. if c.IsSet("attachment-total-size-limit") {
  236. tier.AttachmentTotalSizeLimit, err = util.ParseSize(c.String("attachment-total-size-limit"))
  237. if err != nil {
  238. return err
  239. }
  240. }
  241. if c.IsSet("attachment-expiry-duration") {
  242. tier.AttachmentExpiryDuration = c.Duration("attachment-expiry-duration")
  243. }
  244. if c.IsSet("attachment-bandwidth-limit") {
  245. tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit"))
  246. if err != nil {
  247. return err
  248. }
  249. }
  250. if c.IsSet("stripe-price-id") {
  251. tier.StripePriceID = c.String("stripe-price-id")
  252. }
  253. if err := manager.UpdateTier(tier); err != nil {
  254. return err
  255. }
  256. fmt.Fprintf(c.App.ErrWriter, "tier updated\n\n")
  257. printTier(c, tier)
  258. return nil
  259. }
  260. func execTierDel(c *cli.Context) error {
  261. code := c.Args().Get(0)
  262. if code == "" {
  263. return errors.New("tier code expected, type 'ntfy tier del --help' for help")
  264. }
  265. manager, err := createUserManager(c)
  266. if err != nil {
  267. return err
  268. }
  269. if _, err := manager.Tier(code); err == user.ErrTierNotFound {
  270. return fmt.Errorf("tier %s does not exist", code)
  271. }
  272. if err := manager.RemoveTier(code); err != nil {
  273. return err
  274. }
  275. fmt.Fprintf(c.App.ErrWriter, "tier %s removed\n", code)
  276. return nil
  277. }
  278. func execTierList(c *cli.Context) error {
  279. manager, err := createUserManager(c)
  280. if err != nil {
  281. return err
  282. }
  283. tiers, err := manager.Tiers()
  284. if err != nil {
  285. return err
  286. }
  287. for _, tier := range tiers {
  288. printTier(c, tier)
  289. }
  290. return nil
  291. }
  292. func printTier(c *cli.Context, tier *user.Tier) {
  293. stripePriceID := tier.StripePriceID
  294. if stripePriceID == "" {
  295. stripePriceID = "(none)"
  296. }
  297. fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
  298. fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
  299. fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
  300. fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
  301. fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
  302. fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
  303. fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
  304. fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
  305. fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
  306. fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
  307. fmt.Fprintf(c.App.ErrWriter, "- Stripe price: %s\n", stripePriceID)
  308. }