mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-14 11:47:33 +00:00
"ntfy tier" CLI command
This commit is contained in:
parent
e3b39f670f
commit
a32e8abc12
8 changed files with 140 additions and 40 deletions
|
@ -8,20 +8,27 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||
testMessage := util.RandomString(10)
|
||||
|
||||
app, _, _, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
||||
time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
|
||||
|
||||
app2, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
||||
require.Contains(t, stdout.String(), testMessage)
|
||||
_, err := util.Retry(func() (*int, error) {
|
||||
app2, _, stdout, _ := newTestApp()
|
||||
if err := app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.Contains(stdout.String(), testMessage) {
|
||||
return nil, fmt.Errorf("test message %s not found in topic", testMessage)
|
||||
}
|
||||
return util.Int(1), nil
|
||||
}, time.Second, 2*time.Second, 5*time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||
|
|
73
cmd/tier.go
73
cmd/tier.go
|
@ -56,8 +56,27 @@ var cmdTier = &cli.Command{
|
|||
&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
|
||||
Description: `Add a new tier to the ntfy user database.
|
||||
|
||||
Tiers can be used to grant users higher limits based, such as daily message limits, attachment size, or
|
||||
make it possible for users to reserve topics.
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
|
||||
Examples:
|
||||
ntfy tier add pro # Add tier with code "pro", using the defaults
|
||||
ntfy tier add \ # Add a tier with custom limits
|
||||
--name="Pro" \
|
||||
--message-limit=10000 \
|
||||
--message-expiry-duration=24h \
|
||||
--email-limit=50 \
|
||||
--reservation-limit=10 \
|
||||
--attachment-file-size-limit=100M \
|
||||
--attachment-total-size-limit=1G \
|
||||
--attachment-expiry-duration=12h \
|
||||
--attachment-bandwidth-limit=5G \
|
||||
pro
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -78,8 +97,20 @@ FIXME
|
|||
&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
|
||||
Description: `Updates a tier to change the limits.
|
||||
|
||||
After updating a tier, you may have to restart the ntfy server to apply them
|
||||
to all visitors.
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
|
||||
Examples:
|
||||
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
||||
ntfy tier change \ # Update multiple limits and fields
|
||||
--message-expiry-duration=24h \
|
||||
--stripe-price-id=price_1234 \
|
||||
pro
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -88,8 +119,16 @@ FIXME
|
|||
Usage: "Removes a tier",
|
||||
UsageText: "ntfy tier remove CODE",
|
||||
Action: execTierDel,
|
||||
Description: `
|
||||
FIXME
|
||||
Description: `Remove a tier from the ntfy user database.
|
||||
|
||||
You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
|
||||
to remove or switch their tier first.
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
|
||||
Example:
|
||||
ntfy tier del pro
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -97,22 +136,26 @@ FIXME
|
|||
Aliases: []string{"l"},
|
||||
Usage: "Shows a list of tiers",
|
||||
Action: execTierList,
|
||||
Description: `
|
||||
FIXME
|
||||
Description: `Shows a list of all configured tiers.
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
`,
|
||||
},
|
||||
},
|
||||
Description: `Manage tier of the ntfy server.
|
||||
Description: `Manage tiers 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.
|
||||
The command allows you to add/remove/change tiers in the ntfy user database. Tiers are used
|
||||
to grant users higher limits, such as daily message limits, attachment size, or make it
|
||||
possible for users to reserve topics.
|
||||
|
||||
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
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
|
||||
Examples:
|
||||
ntfy tier add pro # Add tier with code "pro", using the defaults
|
||||
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
||||
ntfy tier del pro # Delete an existing tier
|
||||
`,
|
||||
}
|
||||
|
||||
|
|
46
cmd/tier_test.go
Normal file
46
cmd/tier_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, _, _, stderr := newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
|
||||
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
|
||||
|
||||
err := runTierCommand(app, conf, "add", "pro")
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "tier pro already exists", err.Error())
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "list"))
|
||||
require.Contains(t, stderr.String(), "tier pro (id: ti_")
|
||||
require.Contains(t, stderr.String(), "- Name: Pro")
|
||||
require.Contains(t, stderr.String(), "- Message limit: 1234")
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "change", "--message-limit", "999", "pro"))
|
||||
require.Contains(t, stderr.String(), "- Message limit: 999")
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
|
||||
require.Contains(t, stderr.String(), "tier pro removed")
|
||||
}
|
||||
|
||||
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||
userArgs := []string{
|
||||
"ntfy",
|
||||
"tier",
|
||||
"--auth-file=" + conf.AuthFile,
|
||||
"--auth-default-access=" + conf.AuthDefault.String(),
|
||||
}
|
||||
return app.Run(append(userArgs, args...))
|
||||
}
|
12
cmd/user.go
12
cmd/user.go
|
@ -139,22 +139,22 @@ Example:
|
|||
Action: execUserList,
|
||||
Description: `Shows a list of all configured users, including the everyone ('*') user.
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
|
||||
This command is an alias to calling 'ntfy access' (display access control list).
|
||||
|
||||
This is a server-only command. It directly reads from the user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.
|
||||
`,
|
||||
},
|
||||
},
|
||||
Description: `Manage users of the ntfy server.
|
||||
|
||||
The command allows you to add/remove/change users in the ntfy user database, as well as change
|
||||
passwords or roles.
|
||||
|
||||
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'.
|
||||
|
||||
The command allows you to add/remove/change users in the ntfy user database, as well as change
|
||||
passwords or roles.
|
||||
|
||||
Examples:
|
||||
ntfy user list # Shows list of users (alias: 'ntfy access')
|
||||
ntfy user add phil # Add regular user phil
|
||||
|
|
14
log/event.go
14
log/event.go
|
@ -11,13 +11,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
tagField = "tag"
|
||||
errorField = "error"
|
||||
tagField = "tag"
|
||||
errorField = "error"
|
||||
timestampFormat = "2006-01-02T15:04:05.999Z07:00"
|
||||
)
|
||||
|
||||
// Event represents a single log event
|
||||
type Event struct {
|
||||
Timestamp int64 `json:"time"`
|
||||
Timestamp string `json:"time"`
|
||||
Level Level `json:"level"`
|
||||
Message string `json:"message"`
|
||||
fields Context
|
||||
|
@ -25,8 +26,9 @@ type Event struct {
|
|||
|
||||
// newEvent creates a new log event
|
||||
func newEvent() *Event {
|
||||
now := time.Now()
|
||||
return &Event{
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Timestamp: now.Format(timestampFormat),
|
||||
fields: make(Context),
|
||||
}
|
||||
}
|
||||
|
@ -70,8 +72,8 @@ func (e *Event) Tag(tag string) *Event {
|
|||
}
|
||||
|
||||
// Time sets the time field
|
||||
func (e *Event) Time(time time.Time) *Event {
|
||||
e.Timestamp = time.UnixMilli()
|
||||
func (e *Event) Time(t time.Time) *Event {
|
||||
e.Timestamp = t.Format(timestampFormat)
|
||||
return e
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestLog_TagContextFieldFields(t *testing.T) {
|
|||
Tag("mytag").
|
||||
Field("field2", 123).
|
||||
Field("field1", "value1").
|
||||
Time(time.Unix(123, 0)).
|
||||
Time(time.Unix(123, 999000000).UTC()).
|
||||
Info("hi there %s", "phil")
|
||||
log.
|
||||
Tag("not-stripe").
|
||||
|
@ -48,11 +48,11 @@ func TestLog_TagContextFieldFields(t *testing.T) {
|
|||
}).
|
||||
Tag("stripe").
|
||||
Err(err).
|
||||
Time(time.Unix(456, 0)).
|
||||
Time(time.Unix(456, 123000000).UTC()).
|
||||
Debug("Subscription status %s", "active")
|
||||
|
||||
expected := `{"time":123000,"level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
|
||||
{"time":456000,"level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
|
||||
expected := `{"time":"1970-01-01T00:02:03.999Z","level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
|
||||
{"time":"1970-01-01T00:07:36.123Z","level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
|
||||
`
|
||||
require.Equal(t, expected, out.String())
|
||||
}
|
||||
|
|
|
@ -24,7 +24,9 @@ func logv(v *visitor) *log.Event {
|
|||
|
||||
// logr creates a new log event with HTTP request and visitor fields
|
||||
func logvr(v *visitor, r *http.Request) *log.Event {
|
||||
return logv(v).Fields(httpContext(r))
|
||||
return logv(v).
|
||||
Fields(httpContext(r)).
|
||||
Fields(requestLimiterFields(v.RequestLimiter()))
|
||||
}
|
||||
|
||||
// logvrm creates a new log event with HTTP request, visitor fields and message fields
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
- HIGH Rate limiting: Sensitive endpoints (account/login/change-password/...)
|
||||
- HIGH Account limit creation triggers when account is taken!
|
||||
- HIGH Docs
|
||||
- HIGH CLI "ntfy tier [add|list|delete]"
|
||||
- HIGH make request limit independent of message limit again
|
||||
- HIGH Self-review
|
||||
- MEDIUM: Test for expiring messages after reservation removal
|
||||
- MEDIUM: Test new token endpoints & never-expiring token
|
||||
|
@ -235,8 +235,8 @@ func (s *Server) Run() error {
|
|||
}
|
||||
log.Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
|
||||
if log.IsFile() {
|
||||
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s, log file is %s\n", listenStr, s.config.Version, log.File())
|
||||
fmt.Fprintln(os.Stderr, "No more output is expected.")
|
||||
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
|
||||
fmt.Fprintf(os.Stderr, "Logs are written to %s\n", log.File())
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handle)
|
||||
|
|
Loading…
Reference in a new issue