Compare commits

...

21 Commits

Author SHA1 Message Date
binwiederhier
ba86e08ffe Release notes 2025-08-08 16:19:02 -04:00
binwiederhier
2d9e2356b1 Release notes 2025-08-08 16:13:39 -04:00
binwiederhier
fe5c844a21 Add test 2025-08-08 16:10:49 -04:00
binwiederhier
97410db301 Merge remote-tracking branch 'timofej673/main' into message-cache-lock 2025-08-08 16:06:27 -04:00
binwiederhier
887751cd5d Release notes 2025-08-08 15:34:30 -04:00
Philipp C. Heckel
044326068c Merge pull request #1420 from binwiederhier/debian-stripe
WIP: Add build flags to remove Firebase, Stripe & WebPush (for Debian packaging)
2025-08-08 21:27:22 +02:00
binwiederhier
57a51ab2da Fix tests 2025-08-08 15:16:53 -04:00
binwiederhier
998dbd9054 Undo main.go 2025-08-08 15:02:09 -04:00
binwiederhier
a5a55bd43a Move WebPush tests 2025-08-07 18:54:37 +02:00
binwiederhier
00409d834b Add build flag for webpush 2025-08-07 18:31:42 +02:00
binwiederhier
d9ab7cc78d Add "nowebpush" build tag 2025-08-07 17:39:25 +02:00
binwiederhier
99a2ca8802 Add build tags for Firebase 2025-08-07 17:24:57 +02:00
binwiederhier
ea338ae4fa Make it easy to build without Stripe 2025-08-07 16:41:39 +02:00
binwiederhier
32fa8d43c1 Merge branch 'main' into debian-stripe 2025-08-07 15:34:54 +02:00
Philipp C. Heckel
0f166e0a1d Merge pull request #1417 from orblivion/patch-2
Typo in publish.md
2025-08-05 21:13:39 +02:00
Daniel Krol
46e423fc40 Typo in publish.md 2025-08-05 14:39:57 -04:00
timof
f8082d9481 Update message_cache.go 2025-07-30 00:12:45 +04:00
timof
d9ecee7200 Merge branch 'binwiederhier:main' into main 2025-07-28 10:37:31 +04:00
timof
214f70e62f Merge branch 'binwiederhier:main' into main 2025-07-21 16:52:25 +04:00
timof
006f73af7d Update message_cache.go
Added lock in add_messages to avoid "database is locked" error
Small code reformatting
2025-07-21 12:02:06 +04:00
binwiederhier
5ccc131e73 Derp 2025-07-04 06:41:14 +02:00
22 changed files with 303 additions and 80 deletions

View File

@@ -16,10 +16,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/server" "heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -279,6 +279,8 @@ func execServe(c *cli.Context) error {
// Check values // Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist") return errors.New("if set, FCM key file must exist")
} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") { } else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys") return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
} else if keepaliveInterval < 5*time.Second { } else if keepaliveInterval < 5*time.Second {
@@ -320,6 +322,8 @@ func execServe(c *cli.Context) error {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set") return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if enableSignup && !enableLogin { } else if enableSignup && !enableLogin {
return errors.New("cannot set enable-signup without also setting enable-login") return errors.New("cannot set enable-signup without also setting enable-login")
} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") { } else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set") return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") { } else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
@@ -329,6 +333,8 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 { if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M") return errors.New("message-size-limit cannot be higher than 5M")
} }
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" { } else if behindProxy && proxyForwardedHeader == "" {
@@ -396,8 +402,7 @@ func execServe(c *cli.Context) error {
// Stripe things // Stripe things
if stripeSecretKey != "" { if stripeSecretKey != "" {
stripe.EnableTelemetry = false // Whoa! payments.Setup(stripeSecretKey)
stripe.Key = stripeSecretKey
} }
// Add default forbidden topics // Add default forbidden topics

View File

@@ -1,4 +1,4 @@
//go:build !noserver //go:build !noserver && !nowebpush
package cmd package cmd

View File

@@ -1106,7 +1106,7 @@ Which will result in a notification that looks like this:
When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your
webhook payload. webhook payload.
Inline templates are most useful for templated one-off messages, of if you do not control the ntfy server (e.g., if you're using ntfy.sh). Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh).
Consider using [pre-defined templates](#pre-defined-templates) or [custom templates](#custom-templates) instead, Consider using [pre-defined templates](#pre-defined-templates) or [custom templates](#custom-templates) instead,
if you control the ntfy server, as templates are much easier to maintain. if you control the ntfy server, as templates are much easier to maintain.

View File

@@ -1468,6 +1468,14 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet ## Not released yet
### ntfy server v2.15.0 (UNRELEASED)
**Bug fixes + maintenance:**
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
### ntfy Android app v1.16.1 (UNRELEASED) ### ntfy Android app v1.16.1 (UNRELEASED)
**Features:** **Features:**

21
payments/payments.go Normal file
View File

@@ -0,0 +1,21 @@
//go:build !nopayments
package payments
import "github.com/stripe/stripe-go/v74"
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = true
// SubscriptionStatus is an alias for stripe.SubscriptionStatus
type SubscriptionStatus stripe.SubscriptionStatus
// PriceRecurringInterval is an alias for stripe.PriceRecurringInterval
type PriceRecurringInterval stripe.PriceRecurringInterval
// Setup sets the Stripe secret key and disables telemetry
func Setup(stripeSecretKey string) {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
}

View File

@@ -0,0 +1,18 @@
//go:build nopayments
package payments
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = false
// SubscriptionStatus is a dummy type
type SubscriptionStatus string
// PriceRecurringInterval is dummy type
type PriceRecurringInterval string
// Setup is a dummy type
func Setup(stripeSecretKey string) {
// Nothing to see here
}

View File

@@ -8,6 +8,7 @@ import (
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
_ "github.com/mattn/go-sqlite3" // SQLite driver _ "github.com/mattn/go-sqlite3" // SQLite driver
@@ -283,6 +284,7 @@ type messageCache struct {
db *sql.DB db *sql.DB
queue *util.BatchingQueue[*message] queue *util.BatchingQueue[*message]
nop bool nop bool
mu sync.Mutex
} }
// newSqliteCache creates a SQLite file-backed cache // newSqliteCache creates a SQLite file-backed cache
@@ -347,6 +349,8 @@ func (c *messageCache) AddMessage(m *message) error {
// addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until // addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until
// SQLite's busy_timeout is exceeded before erroring out. // SQLite's busy_timeout is exceeded before erroring out.
func (c *messageCache) addMessages(ms []*message) error { func (c *messageCache) addMessages(ms []*message) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.nop { if c.nop {
return nil return nil
} }
@@ -528,6 +532,8 @@ func (c *messageCache) Message(id string) (*message, error) {
} }
func (c *messageCache) MarkPublished(m *message) error { func (c *messageCache) MarkPublished(m *message) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateMessagePublishedQuery, m.ID) _, err := c.db.Exec(updateMessagePublishedQuery, m.ID)
return err return err
} }
@@ -573,6 +579,8 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
} }
func (c *messageCache) DeleteMessages(ids ...string) error { func (c *messageCache) DeleteMessages(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -587,6 +595,8 @@ func (c *messageCache) DeleteMessages(ids ...string) error {
} }
func (c *messageCache) ExpireMessages(topics ...string) error { func (c *messageCache) ExpireMessages(topics ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -621,6 +631,8 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
} }
func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error { func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -766,6 +778,8 @@ func readMessage(rows *sql.Rows) (*message, error) {
} }
func (c *messageCache) UpdateStats(messages int64) error { func (c *messageCache) UpdateStats(messages int64) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateStatsQuery, messages) _, err := c.db.Exec(updateStatsQuery, messages)
return err return err
} }

View File

@@ -3,8 +3,10 @@ package server
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"sync"
"testing" "testing"
"time" "time"
@@ -90,6 +92,26 @@ func testCacheMessages(t *testing.T, c *messageCache) {
require.Empty(t, messages) require.Empty(t, messages)
} }
func TestSqliteCache_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newSqliteTestCache(t))
}
func TestMemCache_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newMemTestCache(t))
}
func testCacheMessagesLock(t *testing.T, c *messageCache) {
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "test message")))
wg.Done()
}()
}
wg.Wait()
}
func TestSqliteCache_MessagesScheduled(t *testing.T) { func TestSqliteCache_MessagesScheduled(t *testing.T) {
testCacheMessagesScheduled(t, newSqliteTestCache(t)) testCacheMessagesScheduled(t, newSqliteTestCache(t))
} }

View File

@@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/payments"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -165,7 +166,7 @@ func New(conf *Config) (*Server, error) {
mailer = &smtpSender{config: conf} mailer = &smtpSender{config: conf}
} }
var stripe stripeAPI var stripe stripeAPI
if conf.StripeSecretKey != "" { if payments.Available && conf.StripeSecretKey != "" {
stripe = newStripeAPI() stripe = newStripeAPI()
} }
messageCache, err := createMessageCache(conf) messageCache, err := createMessageCache(conf)

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server package server
import ( import (
@@ -14,6 +16,10 @@ import (
) )
const ( const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = true
fcmMessageLimit = 4000 fcmMessageLimit = 4000
fcmApnsBodyMessageLimit = 100 fcmApnsBodyMessageLimit = 100
) )
@@ -73,7 +79,7 @@ type firebaseSenderImpl struct {
client *messaging.Client client *messaging.Client
} }
func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) { func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile)) fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,38 @@
//go:build nofirebase
package server
import (
"errors"
"heckel.io/ntfy/v2/user"
)
const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = false
)
var (
errFirebaseNotAvailable = errors.New("Firebase not available")
errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase")
)
type firebaseClient struct {
}
func (c *firebaseClient) Send(v *visitor, m *message) error {
return errFirebaseNotAvailable
}
type firebaseSender interface {
Send(m string) error
}
func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
return nil
}
func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
return nil, errFirebaseNotAvailable
}

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server package server
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server package server
import ( import (
@@ -12,6 +14,7 @@ import (
"github.com/stripe/stripe-go/v74/subscription" "github.com/stripe/stripe-go/v74/subscription"
"github.com/stripe/stripe-go/v74/webhook" "github.com/stripe/stripe-go/v74/webhook"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"io" "io"
@@ -22,7 +25,7 @@ import (
// Payments in ntfy are done via Stripe. // Payments in ntfy are done via Stripe.
// //
// Pretty much all payments related things are in this file. The following processes // Pretty much all payments-related things are in this file. The following processes
// handle payments: // handle payments:
// //
// - Checkout: // - Checkout:
@@ -464,8 +467,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
billing := &user.Billing{ billing := &user.Billing{
StripeCustomerID: customerID, StripeCustomerID: customerID,
StripeSubscriptionID: subscriptionID, StripeSubscriptionID: subscriptionID,
StripeSubscriptionStatus: stripe.SubscriptionStatus(status), StripeSubscriptionStatus: payments.SubscriptionStatus(status),
StripeSubscriptionInterval: stripe.PriceRecurringInterval(interval), StripeSubscriptionInterval: payments.PriceRecurringInterval(interval),
StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0), StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0),
StripeSubscriptionCancelAt: time.Unix(cancelAt, 0), StripeSubscriptionCancelAt: time.Unix(cancelAt, 0),
} }

View File

@@ -0,0 +1,47 @@
//go:build nopayments
package server
import (
"net/http"
)
type stripeAPI interface {
CancelSubscription(id string) (string, error)
}
func newStripeAPI() stripeAPI {
return nil
}
func (s *Server) fetchStripePrices() (map[string]int64, error) {
return nil, errHTTPNotFound
}
func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server package server
import ( import (
@@ -6,6 +8,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74" "github.com/stripe/stripe-go/v74"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"io" "io"
@@ -345,8 +348,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Nil(t, u.Tier) require.Nil(t, u.Tier)
require.Equal(t, "", u.Billing.StripeCustomerID) require.Equal(t, "", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID) require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval) require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users! require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users!
@@ -362,8 +365,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Equal(t, "starter", u.Tier.Code) // Not "pro" require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval) require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages) require.Equal(t, int64(0), u.Stats.Messages)
@@ -473,8 +476,8 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
billing := &user.Billing{ billing := &user.Billing{
StripeCustomerID: "acct_5555", StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234", StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue, StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0), StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(456, 0), StripeSubscriptionCancelAt: time.Unix(456, 0),
} }
@@ -517,8 +520,8 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
require.Equal(t, "starter", u.Tier.Code) // Not "pro" require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due" require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due"
require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month" require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month"
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
@@ -580,8 +583,8 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{ require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{
StripeCustomerID: "acct_5555", StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234", StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue, StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0), StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(0, 0), StripeSubscriptionCancelAt: time.Unix(0, 0),
})) }))
@@ -598,7 +601,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, u.Tier) require.Nil(t, u.Tier)
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID) require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())

View File

@@ -23,7 +23,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -281,30 +280,6 @@ func TestServer_WebEnabled(t *testing.T) {
rr = request(t, s2, "GET", "/app.html", "", nil) rr = request(t, s2, "GET", "/app.html", "", nil)
require.Equal(t, 200, rr.Code) require.Equal(t, 200, rr.Code)
} }
func TestServer_WebPushEnabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_PublishLargeMessage(t *testing.T) { func TestServer_PublishLargeMessage(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AttachmentCacheDir = "" // Disable attachments c.AttachmentCacheDir = "" // Disable attachments
@@ -3257,17 +3232,6 @@ func newTestConfigWithAuthFile(t *testing.T) *Config {
return conf return conf
} }
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}
func newTestServer(t *testing.T, config *Config) *Server { func newTestServer(t *testing.T, config *Config) *Server {
server, err := New(config) server, err := New(config)
require.Nil(t, err) require.Nil(t, err)

View File

@@ -1,3 +1,5 @@
//go:build !nowebpush
package server package server
import ( import (
@@ -13,6 +15,10 @@ import (
) )
const ( const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = true
webPushTopicSubscribeLimit = 50 webPushTopicSubscribeLimit = 50
) )

View File

@@ -0,0 +1,29 @@
//go:build nowebpush
package server
import (
"net/http"
)
const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = false
)
func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
// Nothing to see here
}
func (s *Server) pruneAndNotifyWebPushSubscriptions() {
// Nothing to see here
}

View File

@@ -1,8 +1,11 @@
//go:build !nowebpush
package server package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -10,6 +13,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"path/filepath"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@@ -20,6 +24,28 @@ const (
testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
) )
func TestServer_WebPush_Enabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_WebPush_Disabled(t *testing.T) { func TestServer_WebPush_Disabled(t *testing.T) {
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))
@@ -254,3 +280,14 @@ func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLen
require.Nil(t, err) require.Nil(t, err)
require.Len(t, subs, expectedLength) require.Len(t, subs, expectedLength)
} }
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}

View File

@@ -7,9 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
@@ -1244,8 +1244,8 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
Billing: &Billing{ Billing: &Billing{
StripeCustomerID: stripeCustomerID.String, // May be empty StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty StripeSubscriptionStatus: payments.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: stripe.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty StripeSubscriptionInterval: payments.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
}, },

View File

@@ -4,7 +4,6 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"net/netip" "net/netip"
@@ -164,8 +163,8 @@ func TestManager_AddUser_And_Query(t *testing.T) {
require.Nil(t, a.ChangeBilling("user", &Billing{ require.Nil(t, a.ChangeBilling("user", &Billing{
StripeCustomerID: "acct_123", StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123", StripeSubscriptionID: "sub_123",
StripeSubscriptionStatus: stripe.SubscriptionStatusActive, StripeSubscriptionStatus: "active",
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: "month",
StripeSubscriptionPaidUntil: time.Now().Add(time.Hour), StripeSubscriptionPaidUntil: time.Now().Add(time.Hour),
StripeSubscriptionCancelAt: time.Unix(0, 0), StripeSubscriptionCancelAt: time.Unix(0, 0),
})) }))

View File

@@ -2,8 +2,8 @@ package user
import ( import (
"errors" "errors"
"github.com/stripe/stripe-go/v74"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
@@ -140,8 +140,8 @@ type Stats struct {
type Billing struct { type Billing struct {
StripeCustomerID string StripeCustomerID string
StripeSubscriptionID string StripeSubscriptionID string
StripeSubscriptionStatus stripe.SubscriptionStatus StripeSubscriptionStatus payments.SubscriptionStatus
StripeSubscriptionInterval stripe.PriceRecurringInterval StripeSubscriptionInterval payments.PriceRecurringInterval
StripeSubscriptionPaidUntil time.Time StripeSubscriptionPaidUntil time.Time
StripeSubscriptionCancelAt time.Time StripeSubscriptionCancelAt time.Time
} }