package database import ( "encoding/binary" "fmt" "time" "github.com/dgraph-io/badger/v4" "lol.mleku.dev/chk" "lol.mleku.dev/log" "next.orly.dev/pkg/encoders/hex" ) // NIP43Membership represents membership metadata for NIP-43 type NIP43Membership struct { Pubkey []byte AddedAt time.Time InviteCode string } // Database key prefixes for NIP-43 const ( nip43MemberPrefix = "nip43:member:" nip43InvitePrefix = "nip43:invite:" ) // AddNIP43Member adds a member to the NIP-43 membership list func (d *D) AddNIP43Member(pubkey []byte, inviteCode string) error { if len(pubkey) != 32 { return fmt.Errorf("invalid pubkey length: %d", len(pubkey)) } key := append([]byte(nip43MemberPrefix), pubkey...) // Create membership record membership := NIP43Membership{ Pubkey: pubkey, AddedAt: time.Now(), InviteCode: inviteCode, } // Serialize membership data val := serializeNIP43Membership(membership) return d.DB.Update(func(txn *badger.Txn) error { return txn.Set(key, val) }) } // RemoveNIP43Member removes a member from the NIP-43 membership list func (d *D) RemoveNIP43Member(pubkey []byte) error { if len(pubkey) != 32 { return fmt.Errorf("invalid pubkey length: %d", len(pubkey)) } key := append([]byte(nip43MemberPrefix), pubkey...) return d.DB.Update(func(txn *badger.Txn) error { return txn.Delete(key) }) } // IsNIP43Member checks if a pubkey is a NIP-43 member func (d *D) IsNIP43Member(pubkey []byte) (isMember bool, err error) { if len(pubkey) != 32 { return false, fmt.Errorf("invalid pubkey length: %d", len(pubkey)) } key := append([]byte(nip43MemberPrefix), pubkey...) err = d.DB.View(func(txn *badger.Txn) error { _, err := txn.Get(key) if err == badger.ErrKeyNotFound { isMember = false return nil } if err != nil { return err } isMember = true return nil }) return isMember, err } // GetNIP43Membership retrieves membership details for a pubkey func (d *D) GetNIP43Membership(pubkey []byte) (*NIP43Membership, error) { if len(pubkey) != 32 { return nil, fmt.Errorf("invalid pubkey length: %d", len(pubkey)) } key := append([]byte(nip43MemberPrefix), pubkey...) var membership *NIP43Membership err := d.DB.View(func(txn *badger.Txn) error { item, err := txn.Get(key) if err != nil { return err } return item.Value(func(val []byte) error { membership = deserializeNIP43Membership(val) return nil }) }) if err != nil { return nil, err } return membership, nil } // GetAllNIP43Members returns all NIP-43 members func (d *D) GetAllNIP43Members() ([][]byte, error) { var members [][]byte prefix := []byte(nip43MemberPrefix) err := d.DB.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.Prefix = prefix opts.PrefetchValues = false // We only need keys it := txn.NewIterator(opts) defer it.Close() for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { item := it.Item() key := item.Key() // Extract pubkey from key (skip prefix) pubkey := make([]byte, 32) copy(pubkey, key[len(prefix):]) members = append(members, pubkey) } return nil }) return members, err } // StoreInviteCode stores an invite code with expiry func (d *D) StoreInviteCode(code string, expiresAt time.Time) error { key := append([]byte(nip43InvitePrefix), []byte(code)...) // Serialize expiry time as unix timestamp val := make([]byte, 8) binary.BigEndian.PutUint64(val, uint64(expiresAt.Unix())) return d.DB.Update(func(txn *badger.Txn) error { entry := badger.NewEntry(key, val).WithTTL(time.Until(expiresAt)) return txn.SetEntry(entry) }) } // ValidateInviteCode checks if an invite code is valid and not expired func (d *D) ValidateInviteCode(code string) (valid bool, err error) { key := append([]byte(nip43InvitePrefix), []byte(code)...) err = d.DB.View(func(txn *badger.Txn) error { item, err := txn.Get(key) if err == badger.ErrKeyNotFound { valid = false return nil } if err != nil { return err } return item.Value(func(val []byte) error { if len(val) != 8 { return fmt.Errorf("invalid invite code value") } expiresAt := int64(binary.BigEndian.Uint64(val)) valid = time.Now().Unix() < expiresAt return nil }) }) return valid, err } // DeleteInviteCode removes an invite code (after use) func (d *D) DeleteInviteCode(code string) error { key := append([]byte(nip43InvitePrefix), []byte(code)...) return d.DB.Update(func(txn *badger.Txn) error { return txn.Delete(key) }) } // Helper functions for serialization func serializeNIP43Membership(m NIP43Membership) []byte { // Format: [pubkey(32)] [timestamp(8)] [invite_code_len(2)] [invite_code] codeBytes := []byte(m.InviteCode) codeLen := len(codeBytes) buf := make([]byte, 32+8+2+codeLen) // Copy pubkey copy(buf[0:32], m.Pubkey) // Write timestamp binary.BigEndian.PutUint64(buf[32:40], uint64(m.AddedAt.Unix())) // Write invite code length binary.BigEndian.PutUint16(buf[40:42], uint16(codeLen)) // Write invite code copy(buf[42:], codeBytes) return buf } func deserializeNIP43Membership(data []byte) *NIP43Membership { if len(data) < 42 { return nil } m := &NIP43Membership{} // Read pubkey m.Pubkey = make([]byte, 32) copy(m.Pubkey, data[0:32]) // Read timestamp timestamp := binary.BigEndian.Uint64(data[32:40]) m.AddedAt = time.Unix(int64(timestamp), 0) // Read invite code codeLen := binary.BigEndian.Uint16(data[40:42]) if len(data) >= 42+int(codeLen) { m.InviteCode = string(data[42 : 42+codeLen]) } return m } // PublishNIP43MembershipEvent publishes membership change events func (d *D) PublishNIP43MembershipEvent(kind int, pubkey []byte) error { log.I.F("publishing NIP-43 event kind %d for pubkey %s", kind, hex.Enc(pubkey)) // Get relay identity relaySecret, err := d.GetOrCreateRelayIdentitySecret() if chk.E(err) { return err } // This would integrate with the event publisher // For now, just log it log.D.F("would publish kind %d event for member %s", kind, hex.Enc(pubkey)) // The actual publishing will be done by the handler _ = relaySecret return nil }