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"
"time"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
@@ -279,6 +279,8 @@ func execServe(c *cli.Context) error {
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
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 == "") {
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 {
@@ -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")
} else if enableSignup && !enableLogin {
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 == "") {
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 == "") {
@@ -329,6 +333,8 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 {
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 {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" {
@@ -396,8 +402,7 @@ func execServe(c *cli.Context) error {
// Stripe things
if stripeSecretKey != "" {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
payments.Setup(stripeSecretKey)
}
// Add default forbidden topics

View File

@@ -1,4 +1,4 @@
//go:build !noserver
//go:build !noserver && !nowebpush
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
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,
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
### 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)
**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"
"path/filepath"
"strings"
"sync"
"time"
_ "github.com/mattn/go-sqlite3" // SQLite driver
@@ -36,7 +37,7 @@ const (
priority INT NOT NULL,
tags TEXT NOT NULL,
click TEXT NOT NULL,
icon TEXT NOT NULL,
icon TEXT NOT NULL,
actions TEXT NOT NULL,
attachment_name TEXT NOT NULL,
attachment_type TEXT NOT NULL,
@@ -73,30 +74,30 @@ const (
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE mid = ?
`
selectMessagesSinceTimeQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id
`
@@ -106,10 +107,10 @@ const (
WHERE topic = ? AND published = 1
ORDER BY time DESC, id DESC
LIMIT 1
`
`
selectMessagesDueQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
FROM messages
WHERE time <= ? AND published = 0
ORDER BY time, id
`
@@ -283,6 +284,7 @@ type messageCache struct {
db *sql.DB
queue *util.BatchingQueue[*message]
nop bool
mu sync.Mutex
}
// 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
// SQLite's busy_timeout is exceeded before erroring out.
func (c *messageCache) addMessages(ms []*message) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.nop {
return nil
}
@@ -528,6 +532,8 @@ func (c *messageCache) Message(id string) (*message, error) {
}
func (c *messageCache) MarkPublished(m *message) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateMessagePublishedQuery, m.ID)
return err
}
@@ -573,6 +579,8 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
}
func (c *messageCache) DeleteMessages(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin()
if err != nil {
return err
@@ -587,6 +595,8 @@ func (c *messageCache) DeleteMessages(ids ...string) error {
}
func (c *messageCache) ExpireMessages(topics ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin()
if err != nil {
return err
@@ -621,6 +631,8 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
}
func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin()
if err != nil {
return err
@@ -766,6 +778,8 @@ func readMessage(rows *sql.Rows) (*message, error) {
}
func (c *messageCache) UpdateStats(messages int64) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateStatsQuery, messages)
return err
}

View File

@@ -3,8 +3,10 @@ package server
import (
"database/sql"
"fmt"
"github.com/stretchr/testify/assert"
"net/netip"
"path/filepath"
"sync"
"testing"
"time"
@@ -90,6 +92,26 @@ func testCacheMessages(t *testing.T, c *messageCache) {
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) {
testCacheMessagesScheduled(t, newSqliteTestCache(t))
}

View File

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

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server
import (
@@ -14,6 +16,10 @@ import (
)
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
fcmApnsBodyMessageLimit = 100
)
@@ -73,7 +79,7 @@ type firebaseSenderImpl struct {
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))
if err != nil {
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
import (

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server
import (
@@ -12,6 +14,7 @@ import (
"github.com/stripe/stripe-go/v74/subscription"
"github.com/stripe/stripe-go/v74/webhook"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"io"
@@ -22,7 +25,7 @@ import (
// 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:
//
// - Checkout:
@@ -464,8 +467,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
billing := &user.Billing{
StripeCustomerID: customerID,
StripeSubscriptionID: subscriptionID,
StripeSubscriptionStatus: stripe.SubscriptionStatus(status),
StripeSubscriptionInterval: stripe.PriceRecurringInterval(interval),
StripeSubscriptionStatus: payments.SubscriptionStatus(status),
StripeSubscriptionInterval: payments.PriceRecurringInterval(interval),
StripeSubscriptionPaidUntil: time.Unix(paidUntil, 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
import (
@@ -6,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/time/rate"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"io"
@@ -345,8 +348,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Nil(t, u.Tier)
require.Equal(t, "", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.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!
@@ -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, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval)
require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)
require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
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{
StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(456, 0),
}
@@ -517,10 +520,10 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due"
require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month"
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due"
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(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
// Verify that reservations were deleted
r, err := s.userManager.Reservations("phil")
@@ -580,8 +583,8 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{
StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(0, 0),
}))
@@ -598,7 +601,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, u.Tier)
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
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.StripeSubscriptionCancelAt.Unix())

View File

@@ -23,7 +23,6 @@ import (
"testing"
"time"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
@@ -281,30 +280,6 @@ func TestServer_WebEnabled(t *testing.T) {
rr = request(t, s2, "GET", "/app.html", "", nil)
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) {
c := newTestConfig(t)
c.AttachmentCacheDir = "" // Disable attachments
@@ -3257,17 +3232,6 @@ func newTestConfigWithAuthFile(t *testing.T) *Config {
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 {
server, err := New(config)
require.Nil(t, err)

View File

@@ -1,3 +1,5 @@
//go:build !nowebpush
package server
import (
@@ -13,6 +15,10 @@ import (
)
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
)

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
import (
"encoding/json"
"fmt"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
@@ -10,6 +13,7 @@ import (
"net/http"
"net/http/httptest"
"net/netip"
"path/filepath"
"strings"
"sync/atomic"
"testing"
@@ -20,6 +24,28 @@ const (
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) {
s := newTestServer(t, newTestConfig(t))
@@ -254,3 +280,14 @@ func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLen
require.Nil(t, err)
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"
"fmt"
"github.com/mattn/go-sqlite3"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/util"
"net/netip"
"path/filepath"
@@ -1242,12 +1242,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
Calls: calls,
},
Billing: &Billing{
StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: stripe.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: payments.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: payments.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
},
Deleted: deleted.Valid,
}

View File

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

View File

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