Files
next.orly.dev/pkg/wasmdb/nip43.go

214 lines
5.3 KiB
Go

//go:build js && wasm
package wasmdb
import (
"bytes"
"encoding/binary"
"errors"
"time"
"github.com/aperturerobotics/go-indexeddb/idb"
"github.com/hack-pad/safejs"
"next.orly.dev/pkg/database"
)
const (
// NIP43StoreName is the object store for NIP-43 membership
NIP43StoreName = "nip43"
// InvitesStoreName is the object store for invite codes
InvitesStoreName = "invites"
)
// AddNIP43Member adds a pubkey as a NIP-43 member with the given invite code
func (w *W) AddNIP43Member(pubkey []byte, inviteCode string) error {
if len(pubkey) != 32 {
return errors.New("invalid pubkey length")
}
// Create membership record
membership := &database.NIP43Membership{
Pubkey: make([]byte, 32),
InviteCode: inviteCode,
AddedAt: time.Now(),
}
copy(membership.Pubkey, pubkey)
// Serialize membership
data := w.serializeNIP43Membership(membership)
// Store using pubkey as key
return w.setStoreValue(NIP43StoreName, string(pubkey), data)
}
// RemoveNIP43Member removes a pubkey from NIP-43 membership
func (w *W) RemoveNIP43Member(pubkey []byte) error {
return w.deleteStoreValue(NIP43StoreName, string(pubkey))
}
// IsNIP43Member checks if a pubkey is a NIP-43 member
func (w *W) IsNIP43Member(pubkey []byte) (isMember bool, err error) {
data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
if err != nil {
return false, nil // Not found is not an error, just not a member
}
return data != nil, nil
}
// GetNIP43Membership returns the full membership details for a pubkey
func (w *W) GetNIP43Membership(pubkey []byte) (*database.NIP43Membership, error) {
data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
if err != nil {
return nil, err
}
if data == nil {
return nil, errors.New("membership not found")
}
return w.deserializeNIP43Membership(data)
}
// GetAllNIP43Members returns all NIP-43 member pubkeys
func (w *W) GetAllNIP43Members() ([][]byte, error) {
tx, err := w.db.Transaction(idb.TransactionReadOnly, NIP43StoreName)
if err != nil {
return nil, err
}
store, err := tx.ObjectStore(NIP43StoreName)
if err != nil {
return nil, err
}
var members [][]byte
cursorReq, err := store.OpenCursor(idb.CursorNext)
if err != nil {
return nil, err
}
err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
keyVal, keyErr := cursor.Key()
if keyErr != nil {
return keyErr
}
// Key is the pubkey stored as string
keyBytes := safeValueToBytes(keyVal)
if len(keyBytes) == 32 {
pubkey := make([]byte, 32)
copy(pubkey, keyBytes)
members = append(members, pubkey)
}
return cursor.Continue()
})
if err != nil && err.Error() != "found" {
return nil, err
}
return members, nil
}
// StoreInviteCode stores an invite code with expiration time
func (w *W) StoreInviteCode(code string, expiresAt time.Time) error {
// Serialize expiration time as unix timestamp
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, uint64(expiresAt.Unix()))
return w.setStoreValue(InvitesStoreName, code, data)
}
// ValidateInviteCode checks if an invite code is valid (exists and not expired)
func (w *W) ValidateInviteCode(code string) (valid bool, err error) {
data, err := w.getStoreValue(InvitesStoreName, code)
if err != nil {
return false, nil
}
if data == nil || len(data) < 8 {
return false, nil
}
// Check expiration
expiresAt := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
if time.Now().After(expiresAt) {
return false, nil
}
return true, nil
}
// DeleteInviteCode removes an invite code
func (w *W) DeleteInviteCode(code string) error {
return w.deleteStoreValue(InvitesStoreName, code)
}
// PublishNIP43MembershipEvent is a no-op in WASM (events are handled by the relay)
func (w *W) PublishNIP43MembershipEvent(kind int, pubkey []byte) error {
// In WASM context, this would typically be handled by the client
// This is a no-op implementation
return nil
}
// serializeNIP43Membership converts a membership to bytes for storage
func (w *W) serializeNIP43Membership(m *database.NIP43Membership) []byte {
buf := new(bytes.Buffer)
// Write pubkey (32 bytes)
buf.Write(m.Pubkey)
// Write AddedAt as unix timestamp (8 bytes)
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(m.AddedAt.Unix()))
buf.Write(ts)
// Write invite code length (4 bytes) + invite code
codeBytes := []byte(m.InviteCode)
codeLen := make([]byte, 4)
binary.BigEndian.PutUint32(codeLen, uint32(len(codeBytes)))
buf.Write(codeLen)
buf.Write(codeBytes)
return buf.Bytes()
}
// deserializeNIP43Membership converts bytes back to a membership
func (w *W) deserializeNIP43Membership(data []byte) (*database.NIP43Membership, error) {
if len(data) < 44 { // 32 + 8 + 4 minimum
return nil, errors.New("invalid membership data")
}
m := &database.NIP43Membership{}
// Read pubkey
m.Pubkey = make([]byte, 32)
copy(m.Pubkey, data[:32])
// Read AddedAt
m.AddedAt = time.Unix(int64(binary.BigEndian.Uint64(data[32:40])), 0)
// Read invite code
codeLen := binary.BigEndian.Uint32(data[40:44])
if len(data) < int(44+codeLen) {
return nil, errors.New("invalid invite code length")
}
m.InviteCode = string(data[44 : 44+codeLen])
return m, nil
}
// Helper to convert safejs.Value to string for keys
func safeValueToString(v safejs.Value) string {
if v.IsUndefined() || v.IsNull() {
return ""
}
str, err := v.String()
if err != nil {
return ""
}
return str
}