tier.go 14 KB

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