Add relay identity management and subscription enhancements.
Some checks failed
Go / build (push) Has been cancelled

- Introduced relay identity management for subscriptions and follow-list sync.
- Added `IdentityRequested` function to handle the `identity` subcommand.
- Implemented periodic follow-list synchronization for active subscribers.
- Enhanced payment handling to include payer pubkey and subscription updates.
- Added trial expiry and subscription expiry notifications.
This commit is contained in:
2025-09-23 14:22:24 +01:00
parent 7736bb7640
commit 2ba361c915
11 changed files with 928 additions and 25 deletions

View File

@@ -66,3 +66,15 @@ func (s *S) Type() (typ string) {
}
return
}
// AddFollow forwards a pubkey to the active ACL if it supports dynamic follows
func (s *S) AddFollow(pub []byte) {
for _, i := range s.ACL {
if i.Type() == s.Active.Load() {
if f, ok := i.(*Follows); ok {
f.AddFollow(pub)
}
break
}
}
}

View File

@@ -1,6 +1,7 @@
package acl
import (
"bytes"
"context"
"reflect"
"strings"
@@ -370,6 +371,32 @@ func (f *Follows) GetFollowedPubkeys() [][]byte {
return followedPubkeys
}
// AddFollow appends a pubkey to the in-memory follows list if not already present
// and signals the syncer to refresh subscriptions.
func (f *Follows) AddFollow(pub []byte) {
if len(pub) == 0 {
return
}
f.followsMx.Lock()
defer f.followsMx.Unlock()
for _, p := range f.follows {
if bytes.Equal(p, pub) {
return
}
}
b := make([]byte, len(pub))
copy(b, pub)
f.follows = append(f.follows, b)
// notify syncer if initialized
if f.updated != nil {
select {
case f.updated <- struct{}{}:
default:
// if channel is full or not yet listened to, ignore
}
}
}
func init() {
log.T.F("registering follows ACL")
Registry.Register(new(Follows))

81
pkg/database/identity.go Normal file
View File

@@ -0,0 +1,81 @@
package database
import (
"errors"
"fmt"
"github.com/dgraph-io/badger/v4"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/crypto/keys"
"next.orly.dev/pkg/encoders/hex"
)
const relayIdentitySecretKey = "relay:identity:sk"
// GetRelayIdentitySecret returns the relay identity secret key bytes if present.
// If the key is not found, returns (nil, badger.ErrKeyNotFound).
func (d *D) GetRelayIdentitySecret() (skb []byte, err error) {
err = d.DB.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(relayIdentitySecretKey))
if errors.Is(err, badger.ErrKeyNotFound) {
return err
}
if err != nil {
return err
}
return item.Value(func(val []byte) error {
// value stored as hex string
b, err := hex.Dec(string(val))
if err != nil {
return err
}
skb = make([]byte, len(b))
copy(skb, b)
return nil
})
})
return
}
// SetRelayIdentitySecret stores the relay identity secret key bytes (expects 32 bytes).
func (d *D) SetRelayIdentitySecret(skb []byte) (err error) {
if len(skb) != 32 {
return fmt.Errorf("invalid secret key length: %d", len(skb))
}
val := []byte(hex.Enc(skb))
return d.DB.Update(func(txn *badger.Txn) error {
return txn.Set([]byte(relayIdentitySecretKey), val)
})
}
// GetOrCreateRelayIdentitySecret retrieves the existing relay identity secret
// key or creates and stores a new one if none exists.
func (d *D) GetOrCreateRelayIdentitySecret() (skb []byte, err error) {
// Try get fast path
if skb, err = d.GetRelayIdentitySecret(); err == nil && len(skb) == 32 {
return skb, nil
}
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return nil, err
}
// Create new key and store atomically
var gen []byte
if gen, err = keys.GenerateSecretKey(); chk.E(err) {
return nil, err
}
if err = d.SetRelayIdentitySecret(gen); chk.E(err) {
return nil, err
}
log.I.F("generated new relay identity key (pub=%s)", mustPub(gen))
return gen, nil
}
func mustPub(skb []byte) string {
pk, err := keys.SecretBytesToPubKeyHex(skb)
if err != nil {
return ""
}
return pk
}

View File

@@ -188,3 +188,30 @@ func (d *D) GetPaymentHistory(pubkey []byte) ([]Payment, error) {
return payments, err
}
// IsFirstTimeUser checks if a user is logging in for the first time and marks them as seen
func (d *D) IsFirstTimeUser(pubkey []byte) (bool, error) {
key := fmt.Sprintf("firstlogin:%s", hex.EncodeToString(pubkey))
isFirstTime := false
err := d.DB.Update(
func(txn *badger.Txn) error {
_, err := txn.Get([]byte(key))
if errors.Is(err, badger.ErrKeyNotFound) {
// First time - record the login
isFirstTime = true
now := time.Now()
data, err := json.Marshal(map[string]interface{}{
"first_login": now,
})
if err != nil {
return err
}
return txn.Set([]byte(key), data)
}
return err // Return any other error as-is
},
)
return isFirstTime, err
}

View File

@@ -1 +1 @@
v0.7.1
v0.8.0