Documentation
¶
Overview ¶
client/core/eventSerializer.go
Index ¶
- Constants
- func ApplyClientTag(event *nostr.Event, enabled bool, name string)
- func AssembleMediaServerEvent(existing *nostr.Event, kind int, pubkey string, servers []string) (*nostr.Event, error)
- func AssembleProfileEvent(existing *nostr.Event, edits map[string]string) (*nostr.Event, error)
- func AssembleRelayListEvent(existing *nostr.Event, kind int, pubkey string, entries []RelayListEntry) (*nostr.Event, error)
- func ComputeEventID(event *nostr.Event) (string, error)
- func ContactListFilter(pubkey string) nostr.Filter
- func CreateNostrMessage(messageType string, args ...interface{}) ([]byte, error)
- func DerivePublicKey(privateKeyHex string) (string, error)
- func DeserializeEvent(data []byte) (*nostr.Event, error)
- func EventFromJSON(data []byte) (*nostr.Event, error)
- func EventToJSON(event *nostr.Event) ([]byte, error)
- func GeneratePrivateKey() (string, error)
- func GetPublicKeyFromExtension() (string, error)
- func NotesFilter(authors []string, limit int) nostr.Filter
- func ParseNostrMessage(data []byte) (messageType string, args []interface{}, err error)
- func ParseRelayTagURLs(event *nostr.Event) []string
- func ProfileFilter(pubkey string) nostr.Filter
- func ReactionsFilter(eventID string) nostr.Filter
- func RecentNotesFilter(limit int, maxAge time.Duration) nostr.Filter
- func RelayListFilter(pubkey string) nostr.Filter
- func SerializeEvent(evt nostr.Event) string
- func SerializeEventArray(events []*nostr.Event) ([]byte, error)
- func SetLogger(l Logger)
- func SignEventWithExtension(event *nostr.Event) error
- func TimeRangeFilter(since, until time.Time, kinds []int) nostr.Filter
- func ValidateEventStructure(event *nostr.Event) error
- func VerifyEventSignature(event *nostr.Event) bool
- type AuthState
- type BroadcastResult
- func BroadcastEvent(ctx context.Context, event *nostr.Event, relays []string, pool *RelayPool) []BroadcastResult
- func BroadcastToUserRelays(ctx context.Context, event *nostr.Event, pubkey string, client *Client) []BroadcastResult
- func BroadcastWithRetry(ctx context.Context, event *nostr.Event, relays []string, pool *RelayPool, ...) []BroadcastResult
- func PublishEvent(ctx context.Context, client *Client, signer *EventSigner, ...) (*nostr.Event, []BroadcastResult, error)
- func PublishEventWithRetry(ctx context.Context, client *Client, signer *EventSigner, ...) (*nostr.Event, []BroadcastResult, error)
- type BroadcastSummary
- type Client
- func (c *Client) AppRelays(role Role) []string
- func (c *Client) AuthChallenge(url string) string
- func (c *Client) AuthRequests() []AuthState
- func (c *Client) ClearFixedRelays()
- func (c *Client) Close() error
- func (c *Client) ConnectToRelays(urls []string) error
- func (c *Client) ConnectToRelaysWithRetry(urls []string, maxRetries int) error
- func (c *Client) DisconnectFromRelay(relayURL string) error
- func (c *Client) DisconnectFromRelays(relayURLs []string) error
- func (c *Client) FetchEvents(ctx context.Context, filters []nostr.Filter, relays []string, limit int, ...) []*nostr.Event
- func (c *Client) FetchMediaServerList(pubkey string, kind int) *nostr.Event
- func (c *Client) FetchRelayInfo(url string) *RelayInfo
- func (c *Client) FetchRelayList(pubkey string, kind int) *nostr.Event
- func (c *Client) FetchUserRelayLists(pubkey string) *UserRelayLists
- func (c *Client) FixedRelaysEnabled() bool
- func (c *Client) GetConnectedRelays() []string
- func (c *Client) GetRelayStatus() map[string]string
- func (c *Client) GetUserProfile(ctx context.Context, pubkey string, relayHints []string) (*nostr.Event, error)
- func (c *Client) GetUserRelays(pubkey string) (*Mailboxes, error)
- func (c *Client) InvalidateMediaServers(pubkey string)
- func (c *Client) InvalidateUserRelayLists(pubkey string)
- func (c *Client) InvalidateUserRelays(pubkey string)
- func (c *Client) KnownRelays() []string
- func (c *Client) KnownRelaysWithStatus() []KnownRelayStatus
- func (c *Client) NewUserContext(pubkey string, opts ...UserOption) *UserContext
- func (c *Client) OwnListRelays(pubkey string) []string
- func (c *Client) PinRelays(urls ...string)
- func (c *Client) PingRelay(relayURL string) int
- func (c *Client) PingRelays(urls []string) map[string]int
- func (c *Client) PoolStats() PoolStats
- func (c *Client) PublishEvent(ctx context.Context, event *nostr.Event, targetRelays []string) ([]BroadcastResult, error)
- func (c *Client) PublishEventStream(ctx context.Context, event *nostr.Event, relays []string) <-chan BroadcastResult
- func (c *Client) PublishEventWithRetry(ctx context.Context, event *nostr.Event, targetRelays []string, maxRetries int) ([]BroadcastResult, error)
- func (c *Client) QueryEvents(ctx context.Context, filter nostr.Filter, relays []string, ...) []*nostr.Event
- func (c *Client) RemoveAuth(url string)
- func (c *Client) ReplaceRelayConnections(newRelays []RelayConfig) error
- func (c *Client) ResolveMediaServers(pubkey string) *MediaServers
- func (c *Client) ResolveRelays(pubkey string) *UserRelays
- func (c *Client) ResolveUserRelayLists(pubkey string) *UserRelayLists
- func (c *Client) Route(op RouteOp, target string) []string
- func (c *Client) RouteFetch(pubkey string) []string
- func (c *Client) RouteMetadata(pubkey string) []string
- func (c *Client) RoutePublish(event *nostr.Event) []string
- func (c *Client) SeedKnownRelays()
- func (c *Client) SendAuth(url string, signedEvent *nostr.Event) error
- func (c *Client) SetAppRelays(role Role, urls []string)
- func (c *Client) SetFixedRelays(readRelays, writeRelays []string)
- func (c *Client) StartEvictionSweeper(ctx context.Context, interval time.Duration)
- func (c *Client) StreamEvents(ctx context.Context, filter nostr.Filter, relays []string, ...) <-chan *nostr.Event
- func (c *Client) Subscribe(ctx context.Context, filters []nostr.Filter, relayHints []string) (*Subscription, error)
- func (c *Client) SwitchToIndexRelays() error
- func (c *Client) SwitchToUserRelays(userRelays []RelayConfig) error
- func (c *Client) WarmMediaServers(pubkey string)
- func (c *Client) WarmRelays(pubkey string)
- func (c *Client) WarmUserRelayLists(pubkey string)
- type ClientError
- type Config
- type ConnectionStatus
- type EncryptedContent
- type EncryptedFlags
- type Encrypter
- type EncrypterV3
- type EventBuilder
- func NewContactList() *EventBuilder
- func NewDeletion(eventIDs []string, reason string) *EventBuilder
- func NewEventBuilder(kind int) *EventBuilder
- func NewProfile() *EventBuilder
- func NewReaction(eventID string, content string) *EventBuilder
- func NewRelayList() *EventBuilder
- func NewRepost(eventID string, relayHint string) *EventBuilder
- func NewTextNote(content string) *EventBuilder
- func (eb *EventBuilder) ATag(kind int, pubkey string, dTag string, relayHint ...string) *EventBuilder
- func (eb *EventBuilder) Build() *nostr.Event
- func (eb *EventBuilder) Content(content string) *EventBuilder
- func (eb *EventBuilder) CreatedAt(t time.Time) *EventBuilder
- func (eb *EventBuilder) DTag(identifier string) *EventBuilder
- func (eb *EventBuilder) ETag(eventID string, relayHint, marker string) *EventBuilder
- func (eb *EventBuilder) PTag(pubkey string, relayHint ...string) *EventBuilder
- func (eb *EventBuilder) RTag(relayURL string, marker string) *EventBuilder
- func (eb *EventBuilder) TTag(hashtag string) *EventBuilder
- func (eb *EventBuilder) Tag(name string, values ...string) *EventBuilder
- type EventSigner
- func (es *EventSigner) GetPrivateKeyHex() string
- func (es *EventSigner) GetPublicKey() string
- func (es *EventSigner) NIP44Decrypt(peerPubKey, payload string) (string, error)
- func (es *EventSigner) NIP44Encrypt(peerPubKey, plaintext string) (string, error)
- func (es *EventSigner) NIP44V3Decrypt(peerPubKey string, expectedKind uint32, expectedScope []byte, payload string) ([]byte, error)
- func (es *EventSigner) NIP44V3Encrypt(peerPubKey string, kind uint32, scope, plaintext []byte) (string, error)
- func (es *EventSigner) PublicKey() string
- func (es *EventSigner) SignEvent(event *nostr.Event) error
- type FilterBuilder
- func (fb *FilterBuilder) Authors(pubkeys ...string) *FilterBuilder
- func (fb *FilterBuilder) Build() nostr.Filter
- func (fb *FilterBuilder) IDs(ids ...string) *FilterBuilder
- func (fb *FilterBuilder) Kinds(kinds ...int) *FilterBuilder
- func (fb *FilterBuilder) Limit(limit int) *FilterBuilder
- func (fb *FilterBuilder) Since(timestamp time.Time) *FilterBuilder
- func (fb *FilterBuilder) Tag(name string, values ...string) *FilterBuilder
- func (fb *FilterBuilder) Until(timestamp time.Time) *FilterBuilder
- type KnownRelayStatus
- type Logger
- type Mailboxes
- type MediaDirectory
- type MediaServerInfo
- type MediaServers
- type MessageRouter
- func (mr *MessageRouter) AuthChallenge(relayURL string) string
- func (mr *MessageRouter) AuthStates() []AuthState
- func (mr *MessageRouter) MarkAuthed(relayURL string)
- func (mr *MessageRouter) RegisterOKWaiter(eventID string, buf int) chan OKResult
- func (mr *MessageRouter) RegisterSubscription(subID string, sub *Subscription)
- func (mr *MessageRouter) RemoveAuth(relayURL string)
- func (mr *MessageRouter) RouteAuth(relayURL, challenge string)
- func (mr *MessageRouter) RouteMessage(subID string, messageType string, data interface{}, relayURL string)
- func (mr *MessageRouter) RouteOK(eventID string, ok OKResult)
- func (mr *MessageRouter) UnregisterOKWaiter(eventID string)
- func (mr *MessageRouter) UnregisterSubscription(subID string)
- type OKResult
- type PoolStats
- type RelayConfig
- type RelayConnection
- type RelayDirectory
- type RelayInfo
- type RelayLimits
- type RelayListEntry
- type RelayListStore
- type RelayLiveStatus
- type RelayPool
- func (rp *RelayPool) Acquire(url string) (*RelayConnection, error)
- func (rp *RelayPool) AuthChallenge(url string) string
- func (rp *RelayPool) AuthRequests() []AuthState
- func (rp *RelayPool) BroadcastMessage(message interface{}, urls []string) error
- func (rp *RelayPool) Close() error
- func (rp *RelayPool) CloseConnection(url string) error
- func (rp *RelayPool) Connect(url string) error
- func (rp *RelayPool) EnsureConnectedForSend(url string, timeout time.Duration) error
- func (rp *RelayPool) GetConnectedRelays() []string
- func (rp *RelayPool) GetConnection(url string) (*RelayConnection, error)
- func (rp *RelayPool) Pin(urls ...string)
- func (rp *RelayPool) RegisterSubscription(subID string, sub *Subscription)
- func (rp *RelayPool) Release(url string)
- func (rp *RelayPool) RemoveAuth(url string)
- func (rp *RelayPool) SendAuth(url string, signedEvent *nostr.Event) error
- func (rp *RelayPool) SendMessage(url string, message interface{}) error
- func (rp *RelayPool) StartEvictionSweeper(ctx context.Context, interval time.Duration)
- func (rp *RelayPool) Stats() PoolStats
- func (rp *RelayPool) StatusOf(url string) RelayLiveStatus
- func (rp *RelayPool) UnregisterSubscription(subID string)
- type Role
- type RouteOp
- type SessionRelays
- type Signer
- type StreamOption
- type Subscription
- func (s *Subscription) AddRelay(url string) error
- func (s *Subscription) Close() error
- func (s *Subscription) GetFilters() []nostr.Filter
- func (s *Subscription) GetRelayCount() int
- func (s *Subscription) IsActive() bool
- func (s *Subscription) RemoveRelay(url string) error
- func (s *Subscription) Start() error
- type UserContext
- func (uc *UserContext) ClearFixedRelays()
- func (uc *UserContext) Client() *Client
- func (uc *UserContext) FetchNotes(ctx context.Context, author string, opts ...StreamOption) []*nostr.Event
- func (uc *UserContext) FixedRelaysEnabled() bool
- func (uc *UserContext) PinFixedRelays(readRelays, writeRelays []string)
- func (uc *UserContext) PublicKey() string
- func (uc *UserContext) Publish(ctx context.Context, event *nostr.Event) ([]BroadcastResult, error)
- func (uc *UserContext) Relays() *SessionRelays
- func (uc *UserContext) Reply(ctx context.Context, parent *nostr.Event, content string) (*nostr.Event, []BroadcastResult, error)
- func (uc *UserContext) Sign(event *nostr.Event) error
- func (uc *UserContext) SignAndPublish(ctx context.Context, event *nostr.Event) ([]BroadcastResult, error)
- func (uc *UserContext) Signer() Signer
- func (uc *UserContext) StreamNotes(ctx context.Context, author string, opts ...StreamOption) <-chan *nostr.Event
- type UserOption
- type UserRelayLists
- type UserRelays
Examples ¶
Constants ¶
const ( MediaKindBlossom = "blossom" // BUD-03 kind 10063; BUD-02 /upload, BUD-04 /mirror MediaKindNIP96 = "nip96" // kind 10096; NIP-96 multipart upload + NIP-98 auth )
Media-server protocol kinds. Blossom is nostr-native (blobs addressed by sha256, signed kind-24242 auth); NIP-96 is traditional HTTP storage authed with NIP-98. grain prefers Blossom and treats NIP-96 as a legacy fallback.
const DefaultClientTagName = "grain"
DefaultClientTagName is grain's client-tag value when none is configured. The "on by default" semantics live in the resolving handler + config loader; this is just the fallback name.
Variables ¶
This section is empty.
Functions ¶
func ApplyClientTag ¶
ApplyClientTag rewrites an event's client tag in place per grain's policy: strip any foreign `client` tag always, add grain's own when enabled. Safe on a nil event. Build handlers call this after assembling an event grain authors.
Example ¶
Stamping grain's NIP-89 client tag on an event before signing (any foreign client tag is stripped first; pass false to strip without re-adding).
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
nostr "github.com/0ceanslim/grain/server/types"
)
func main() {
ev := &nostr.Event{Kind: 1, Content: "gm"}
core.ApplyClientTag(ev, true, "grain")
fmt.Println(len(ev.Tags))
}
Output:
func AssembleMediaServerEvent ¶
func AssembleMediaServerEvent(existing *nostr.Event, kind int, pubkey string, servers []string) (*nostr.Event, error)
AssembleMediaServerEvent builds an UNSIGNED media-server list event (Blossom kind 10063 or NIP-96 kind 10096) from the user's existing one, replacing the server list while preserving every other tag and the content — the same conservative "don't drop data" rule as AssembleProfileEvent.
servers are written as `["server", <url>]` tags in the given order (primary first), normalised and de-duplicated. existing may be nil (a first list). The returned event has no ID or Sig: the caller signs it.
func AssembleProfileEvent ¶
AssembleProfileEvent builds an UNSIGNED kind-0 (profile metadata) event from an existing one by applying edits. It is deliberately conservative about not losing data:
- Every existing content field is preserved; only edited fields are overwritten. The content JSON stays the interoperable source of truth that all clients read.
- Every existing tag is preserved; for each edited field a [key, value] tag is upserted — replacing a prior tag with the same key rather than duplicating — so tag-aware clients can read the field too (dual-write).
The returned event has no ID or Sig: the caller signs it. Returns an error if the existing content isn't valid JSON, rather than silently clobbering a profile we can't safely parse.
func AssembleRelayListEvent ¶
func AssembleRelayListEvent(existing *nostr.Event, kind int, pubkey string, entries []RelayListEntry) (*nostr.Event, error)
AssembleRelayListEvent builds an UNSIGNED relay-list event of the given kind from the user's existing one, rewriting the relay tags while preserving the content and every non-relay tag — the same conservative "don't drop data" rule as the profile and media-server editors.
Tag shape by kind:
- 10002 (NIP-65): ["r", url] (both), ["r", url, "read"], or ["r", url, "write"]
- 10050 / 10006 / 10007 / 10012: ["relay", url]
existing may be nil (a first list). The returned event has no ID or Sig: the caller signs it.
Example ¶
Writing a relay list: assemble an UNSIGNED 10002 from the desired entries, then sign + outbox-route + publish it with the user context.
package main
import (
"context"
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
signer, err := core.NewEventSigner("64-char-hex-private-key")
if err != nil {
return
}
uc := client.NewUserContext(signer.PublicKey(), core.WithSigner(signer))
entries := []core.RelayListEntry{
{URL: "wss://out.example.com", Write: true}, // outbox only
{URL: "wss://in.example.com", Read: true}, // inbox only
{URL: "wss://both.example.com", Read: true, Write: true}, // both (unmarked)
}
unsigned, err := core.AssembleRelayListEvent(nil, 10002, uc.PublicKey(), entries)
if err != nil {
return
}
results, err := uc.SignAndPublish(context.Background(), unsigned)
if err != nil {
return
}
fmt.Printf("published to %d relays\n", len(results))
}
Output:
func ComputeEventID ¶
ComputeEventID computes the event ID according to NIP-01
func ContactListFilter ¶
ContactListFilter creates a filter for contact lists (kind 3)
func CreateNostrMessage ¶
CreateNostrMessage creates a properly formatted Nostr protocol message
func DerivePublicKey ¶
DerivePublicKey derives a public key from a private key hex
func DeserializeEvent ¶
DeserializeEvent deserializes JSON bytes to an event
func EventFromJSON ¶
EventFromJSON parses an event from JSON
func EventToJSON ¶
EventToJSON converts an event to pretty-printed JSON
func GeneratePrivateKey ¶
GeneratePrivateKey generates a new random private key
func GetPublicKeyFromExtension ¶
GetPublicKeyFromExtension attempts to get public key from browser extension
func NotesFilter ¶
NotesFilter creates a filter for notes from specific authors
func ParseNostrMessage ¶
ParseNostrMessage parses a Nostr protocol message
func ParseRelayTagURLs ¶
ParseRelayTagURLs extracts the `relay` tag URLs from a NIP-17 / NIP-51 relay-list event (kinds 10050 / 10006 / 10007 / 10012), normalised and de-duplicated, in order.
func ProfileFilter ¶
ProfileFilter creates a filter for user profiles (kind 0)
func ReactionsFilter ¶
ReactionsFilter creates a filter for reactions to a specific event
func RecentNotesFilter ¶
RecentNotesFilter creates a filter for recent notes
func RelayListFilter ¶
RelayListFilter creates a filter for relay lists (kind 10002)
func SerializeEvent ¶
SerializeEvent manually constructs the JSON string for event serialization according to NIP-01
func SerializeEventArray ¶
SerializeEventArray serializes an event for inclusion in a Nostr message array
func SetLogger ¶
func SetLogger(l Logger)
SetLogger replaces the process-wide logger the client library writes through. It is package-global by design: the library logs from many package-level functions with no Client in scope, so threading a per-Client logger everywhere would add a parameter to most of the surface for little gain. Call it once at startup. A nil logger is ignored; passing log.ClientCore() restores the default. Safe for concurrent use.
Example ¶
Replacing grain's logging so the library writes through a consumer's logger. Any *slog.Logger satisfies core.Logger; a consumer can also implement the four methods (Debug/Info/Warn/Error) over their own logging stack.
package main
import (
"log/slog"
"github.com/0ceanslim/grain/client/core"
)
func main() {
core.SetLogger(slog.Default())
}
Output:
func SignEventWithExtension ¶
SignEventWithExtension attempts to sign an event using browser extension (NIP-07)
func TimeRangeFilter ¶
TimeRangeFilter creates a filter for events within a specific time range
func ValidateEventStructure ¶
ValidateEventStructure validates the basic structure of an event
func VerifyEventSignature ¶
VerifyEventSignature verifies an event's signature
Types ¶
type AuthState ¶
type AuthState struct {
Relay string `json:"relay"`
Challenge string `json:"challenge"`
Authed bool `json:"authed"`
At time.Time `json:"at"`
}
AuthState is what the pool has observed about one relay's NIP-42 status this session: its latest challenge and whether we've answered it.
type BroadcastResult ¶
type BroadcastResult struct {
RelayURL string
Success bool
Accepted bool
Reason string
Error error
Message string
Duration time.Duration
}
BroadcastResult represents the result of broadcasting to a single relay. Success means the EVENT was sent; Accepted is the relay's NIP-20 OK verdict (whether it actually stored the event), with Reason carrying any message.
func BroadcastEvent ¶
func BroadcastEvent(ctx context.Context, event *nostr.Event, relays []string, pool *RelayPool) []BroadcastResult
BroadcastEvent sends an event to multiple relays using the relay pool. The context bounds the per-relay connect and the NIP-20 OK-collection wait, so a caller can cancel or deadline the whole publish.
func BroadcastToUserRelays ¶
func BroadcastToUserRelays(ctx context.Context, event *nostr.Event, pubkey string, client *Client) []BroadcastResult
BroadcastToUserRelays broadcasts an event to a user's preferred relays
func BroadcastWithRetry ¶
func BroadcastWithRetry(ctx context.Context, event *nostr.Event, relays []string, pool *RelayPool, maxRetries int) []BroadcastResult
BroadcastWithRetry broadcasts an event with retry logic
func PublishEvent ¶
func PublishEvent(ctx context.Context, client *Client, signer *EventSigner, eventBuilder *EventBuilder, targetRelays []string) (*nostr.Event, []BroadcastResult, error)
PublishEvent is a high-level function to build, sign, and broadcast an event
func PublishEventWithRetry ¶
func PublishEventWithRetry(ctx context.Context, client *Client, signer *EventSigner, eventBuilder *EventBuilder, targetRelays []string, maxRetries int) (*nostr.Event, []BroadcastResult, error)
PublishEventWithRetry publishes an event with retry logic
type BroadcastSummary ¶
type BroadcastSummary struct {
TotalRelays int
Successful int
Failed int
SuccessRate float64
AverageDuration time.Duration
Errors []string
}
BroadcastSummary provides a summary of broadcast results
func SummarizeBroadcast ¶
func SummarizeBroadcast(results []BroadcastResult) BroadcastSummary
SummarizeBroadcast creates a summary of broadcast results
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client represents the main Nostr client with connection pooling
Example (ReadOnly) ¶
Read-only: fetch a user's recent notes from their outbox relays. No signer is attached, so the context can read but not publish.
package main
import (
"context"
"fmt"
"time"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
uc := client.NewUserContext("author-pubkey-hex")
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
notes := uc.FetchNotes(ctx, "author-pubkey-hex", core.WithLimit(20))
fmt.Printf("fetched %d notes\n", len(notes))
}
Output:
func (*Client) AppRelays ¶
AppRelays returns the session relays for a locally-configured role. RoleIndexer falls back to the configured index relays when the user hasn't overridden it; the other roles return nil when unset.
func (*Client) AuthChallenge ¶
AuthChallenge returns a relay's pending AUTH challenge ("" if none).
func (*Client) AuthRequests ¶
AuthRequests returns the relays that have challenged the pool for NIP-42 AUTH.
func (*Client) ClearFixedRelays ¶
func (c *Client) ClearFixedRelays()
ClearFixedRelays disables the override and restores default outbox routing.
func (*Client) ConnectToRelays ¶
ConnectToRelays establishes connections to multiple relay URLs
func (*Client) ConnectToRelaysWithRetry ¶
ConnectToRelaysWithRetry establishes connections with retry logic
func (*Client) DisconnectFromRelay ¶ added in v0.4.12
DisconnectFromRelay closes a specific relay connection
func (*Client) DisconnectFromRelays ¶ added in v0.4.12
DisconnectFromRelays closes connections to multiple relays
func (*Client) FetchEvents ¶
func (c *Client) FetchEvents(ctx context.Context, filters []nostr.Filter, relays []string, limit int, timeout time.Duration) []*nostr.Event
FetchEvents collects up to limit distinct events (deduped by id) matching filters from the given relays, returning when every relay has sent EOSE, the limit is reached, or the timeout elapses. Unlike collectLatestReplaceable (which keeps only the single newest), this gathers a batch — used for bulk queries like relay-list seeding.
func (*Client) FetchMediaServerList ¶
FetchMediaServerList returns the user's latest raw event of the given media-server list kind (10063 Blossom or 10096 NIP-96), or nil if none is found. Used by the build flow to preserve any non-server tags when republishing the list. Queries the index relays plus the user's cached outbox, where these lists most often live.
func (*Client) FetchRelayInfo ¶
FetchRelayInfo returns a relay's NIP-11 document, TTL-cached. It is a plain HTTP GET of the relay's root with `Accept: application/nostr+json` — not a pool/WebSocket connection — so the known-relays browser can show name, software, supported NIPs, and the auth/payment flags. Returns nil when the relay serves no NIP-11 or it can't be parsed (also cached, to avoid retry storms on a relay that doesn't advertise one).
func (*Client) FetchRelayList ¶
FetchRelayList returns the user's latest raw relay-list event of the given kind, or nil. Used by the build flow to preserve non-relay tags on republish. Mirrors FetchMediaServerList: index relays plus the user's cached outbox.
func (*Client) FetchUserRelayLists ¶
func (c *Client) FetchUserRelayLists(pubkey string) *UserRelayLists
FetchUserRelayLists resolves a user's relay lists for every kind the relay manager shows, fetching the kinds concurrently. Each goroutine writes a distinct field, so no locking is needed.
Example ¶
Reading a user's relay lists: NIP-65 (with read/write markers) plus the NIP-51/37 mailbox lists.
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
lists := client.FetchUserRelayLists("author-pubkey-hex")
for _, e := range lists.NIP65 {
fmt.Println(e.URL, "read:", e.Read, "write:", e.Write)
}
fmt.Println("dm relays:", lists.DM)
}
Output:
func (*Client) FixedRelaysEnabled ¶
FixedRelaysEnabled reports whether the fixed-relay override is active.
func (*Client) GetConnectedRelays ¶
GetConnectedRelays returns a list of currently connected relay URLs
func (*Client) GetRelayStatus ¶
GetRelayStatus returns detailed status of all relay connections
func (*Client) GetUserProfile ¶
func (*Client) GetUserRelays ¶
GetUserRelays retrieves a user's relay list (NIP-65), resolved through the directory — an index-relay query that is TTL-cached and single-flighted — rather than fanning a REQ out to every connected relay. That fan-out is what melted the dashboard during a mutelist sync once the outbox pool had grown to dozens of connections: each author's lookup blasted all of them. Returns empty mailboxes (not an error) when the user has published no relay list.
func (*Client) InvalidateMediaServers ¶
InvalidateMediaServers drops a user's cached media-server lists so the next resolve re-fetches — used after the user publishes an updated list.
func (*Client) InvalidateUserRelayLists ¶
InvalidateUserRelayLists drops a user's cached relay lists so the next resolve re-fetches — call after they publish a new relay-list event.
func (*Client) InvalidateUserRelays ¶
InvalidateUserRelays drops a user's cached relay-role resolution so the next lookup re-resolves — call after the user republishes their own 10002 / 10050 so routing picks up the change immediately.
func (*Client) KnownRelays ¶
KnownRelays returns the known set as a sorted slice — the relays behind PoolStats.Known, for the known-relays browser.
Example ¶
Browsing known relays: the live set, NIP-11 metadata, and TCP latency.
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
known := client.KnownRelays()
pings := client.PingRelays(known) // map[url]ms, concurrent
for _, url := range known {
name := url
if info := client.FetchRelayInfo(url); info != nil && info.Name != "" {
name = info.Name // NIP-11, TTL-cached; nil if the relay serves none
}
fmt.Printf("%s — %dms\n", name, pings[url])
}
}
Output:
func (*Client) KnownRelaysWithStatus ¶
func (c *Client) KnownRelaysWithStatus() []KnownRelayStatus
KnownRelaysWithStatus returns every known relay (sorted) annotated with its live pool status. NIP-11 detail is fetched separately, lazily, per relay.
func (*Client) NewUserContext ¶
func (c *Client) NewUserContext(pubkey string, opts ...UserOption) *UserContext
NewUserContext creates a UserContext for pubkey (64-char hex) on this client. Apply options such as WithSigner to attach a signer.
func (*Client) OwnListRelays ¶
OwnListRelays returns the relay set to read a user's own replaceable lists from: the index relays plus their resolved read+write relays. A user's own NIP-51/17 lists live on their own relays, which a cold load may not have cached. Shared by FetchUserRelayLists and the live-sync subscription.
func (*Client) PinRelays ¶
PinRelays marks relays so the idle sweeper never evicts them — used for the index/seed relays. Pinning does not dial; the connection is still established on demand by Acquire or the startup connect.
func (*Client) PingRelay ¶
PingRelay measures TCP-connect latency to a relay — a cheap reachability/ latency probe, not a WebSocket or NIP-01 round-trip — and TTL-caches it. Returns milliseconds, or -1 if the host can't be reached within the timeout (also cached, so onion/unroutable hosts don't stall every sort). Used by the known-relays "fastest first" ordering.
func (*Client) PingRelays ¶
PingRelays pings a set of relays in parallel (bounded worker pool), returning url -> ms (-1 for unreachable). The relay manager calls this for just the rows currently in view, so the set is small even though the known set is large.
func (*Client) PoolStats ¶
PoolStats returns a snapshot of the pool's counts plus Known — the distinct relays the client is aware of: configured defaults, every relay tracked in the pool, and every relay from the indexer-seeded mailbox lists the directory has resolved. Known climbs as you interact; connections grow on demand.
func (*Client) PublishEvent ¶
func (c *Client) PublishEvent(ctx context.Context, event *nostr.Event, targetRelays []string) ([]BroadcastResult, error)
PublishEvent publishes an event to specified relays
func (*Client) PublishEventStream ¶
func (c *Client) PublishEventStream(ctx context.Context, event *nostr.Event, relays []string) <-chan BroadcastResult
PublishEventStream broadcasts an already-signed event to the given relays and returns a channel that emits each relay's result as it resolves. The caller ranges the channel until it closes. Used by the streaming publish endpoint to drive the live broadcast toast.
func (*Client) PublishEventWithRetry ¶
func (c *Client) PublishEventWithRetry(ctx context.Context, event *nostr.Event, targetRelays []string, maxRetries int) ([]BroadcastResult, error)
PublishEventWithRetry publishes an event with retry logic
func (*Client) QueryEvents ¶
func (c *Client) QueryEvents(ctx context.Context, filter nostr.Filter, relays []string, opts ...StreamOption) []*nostr.Event
QueryEvents runs Client.StreamEvents and collects every event it yields, in arrival order. A blocking convenience for callers that don't need incremental delivery; pass WithLimit / WithTimeout to bound it.
func (*Client) RemoveAuth ¶
RemoveAuth forgets a relay's session AUTH state.
func (*Client) ReplaceRelayConnections ¶ added in v0.4.12
func (c *Client) ReplaceRelayConnections(newRelays []RelayConfig) error
ReplaceRelayConnections swaps the relays held for the current session.
In the outbox model this is ADDITIVE: it does not tear the shared pool down. It releases the previous session's leases (so those connections become idle-evictable once nothing else needs them) and acquires the new set, holding one lease on each for the session's lifetime. Index/seed relays are pinned separately and are never affected by a session switch.
func (*Client) ResolveMediaServers ¶
func (c *Client) ResolveMediaServers(pubkey string) *MediaServers
ResolveMediaServers returns a user's Blossom (kind 10063) and NIP-96 (kind 10096) media-server lists, resolved from their published events and cached with a TTL. Safe for concurrent use; the logged-in user is just another pubkey.
Example ¶
Resolving a user's media servers (Blossom + NIP-96) before an upload. HasAny is the "open the picker vs. prompt to set some up" decision.
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
ms := client.ResolveMediaServers("author-pubkey-hex")
if !ms.HasAny() {
fmt.Println("no media servers configured")
return
}
fmt.Printf("blossom: %v\n", ms.Blossom) // primary first
}
Output:
func (*Client) ResolveRelays ¶
func (c *Client) ResolveRelays(pubkey string) *UserRelays
ResolveRelays returns a target user's per-target relay roles (outbox / inbox / DM inbox), resolved from their published relay-list events and cached with a TTL. Safe for concurrent use; the logged-in user is just another pubkey.
func (*Client) ResolveUserRelayLists ¶
func (c *Client) ResolveUserRelayLists(pubkey string) *UserRelayLists
ResolveUserRelayLists returns a user's relay lists from cache when fresh, otherwise resolves them from the network and caches the result. The relay manager reads through this so the page renders from cache once login-hydration has warmed it.
func (*Client) Route ¶
Route returns the relays a read operation against target would use under the current routing, honouring the fixed-relay override. It is a thin, inspectable facade over Client.RouteFetch / Client.RouteMetadata.
Publishing is intentionally not covered here: it routes per-event (author outbox ∪ each recipient's inbox), so use Client.RoutePublish with the event.
func (*Client) RouteFetch ¶
RouteFetch returns the relays to read a user's authored events from: their outbox (NIP-65 write) relays, falling back to the index/seed relays when the user has no published list. If the fixed-relay override is enabled, the pinned read set is used instead (outbox routing off).
func (*Client) RouteMetadata ¶
RouteMetadata returns the relays to fetch a user's profile/metadata (kind 0) and replaceable lists from.
Flow: the index/profile-indexer relays aggregate everyone's metadata and are already connected, so they are always queried (fast, reliable). The user's own outbox often carries a fresher copy, so it is added too — but only when it is ALREADY cached, never via a blocking resolve. Resolving every profile view synchronously is what made the dashboard crawl; the cache is warmed by the relay-list lookups that happen anyway, so subsequent views include the outbox for free. Honours the fixed-relay override.
func (*Client) RoutePublish ¶
RoutePublish returns the relays an event should be published to under the outbox model: the author's own outbox PLUS every p-tagged recipient's inbox (their DM inbox for NIP-17 gift wraps), so the event reaches both the author's audience and its intended recipients. Falls back to the index/seed relays when nothing resolves.
func (*Client) SeedKnownRelays ¶
func (c *Client) SeedKnownRelays()
SeedKnownRelays bulk-fetches recent relay-list events (NIP-65 kind 10002 and NIP-17 kind 10050) from the index relays and folds every advertised relay into the directory, so the "known" set starts broad immediately instead of growing only as the user browses. Runs once at startup; safe to re-run to refresh. All relay URLs are normalised on the way in (see normalizeRelayURL), so near-duplicates collapse.
func (*Client) SendAuth ¶
SendAuth relays a browser-signed kind-22242 event to a relay.
Example ¶
Answering a relay's NIP-42 AUTH challenge: build + sign a kind-22242 event and forward it on the challenged connection.
package main
import (
"github.com/0ceanslim/grain/client/core"
nostr "github.com/0ceanslim/grain/server/types"
)
func main() {
client := core.NewClient(core.DefaultConfig())
signer, err := core.NewEventSigner("64-char-hex-private-key")
if err != nil {
return
}
uc := client.NewUserContext(signer.PublicKey(), core.WithSigner(signer))
for _, req := range client.AuthRequests() {
if req.Authed {
continue // already answered this session
}
ev := &nostr.Event{
Kind: 22242,
Tags: [][]string{
{"relay", req.Relay},
{"challenge", req.Challenge},
},
}
if err := uc.Sign(ev); err != nil {
continue
}
if err := client.SendAuth(req.Relay, ev); err != nil {
continue
}
}
}
Output:
func (*Client) SetAppRelays ¶
SetAppRelays sets (or clears, when urls is empty) the session override for a locally-configured role. Clearing RoleIndexer restores the configured default.
func (*Client) SetFixedRelays ¶
SetFixedRelays enables the fixed-relay override: every read uses readRelays and every write uses writeRelays, bypassing outbox routing entirely.
This DISABLES the outbox model — replies will not reach other users' inbox relays — and is intended only for users who explicitly want a fixed- or single-relay client. It is off by default and not recommended.
func (*Client) StartEvictionSweeper ¶
StartEvictionSweeper starts the pool's idle-connection sweeper, bounded to ctx.
func (*Client) StreamEvents ¶
func (c *Client) StreamEvents(ctx context.Context, filter nostr.Filter, relays []string, opts ...StreamOption) <-chan *nostr.Event
StreamEvents subscribes to filter across relays and streams matching events on the returned channel as each relay answers, de-duplicated by event id. The channel closes when every relay has signalled end-of-stored-events (a bounded fetch; see WithLive), the WithLimit count is reached, ctx is cancelled, or the WithTimeout elapses — whichever comes first.
This is the general, multi-relay form of a live feed: point it at a single relay (e.g. grain's own) for a single-source stream, or at a user's outbox set for an outbox feed. Per-relay failures are logged, not fatal. The caller must drain the channel (or cancel ctx) so the underlying subscription is released.
Example ¶
Streaming: a live feed of kind-1 notes from a single relay (e.g. grain's own).
package main
import (
"context"
"fmt"
"github.com/0ceanslim/grain/client/core"
nostr "github.com/0ceanslim/grain/server/types"
)
func main() {
client := core.NewClient(core.DefaultConfig())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
feed := client.StreamEvents(ctx,
nostr.Filter{Kinds: []int{1}},
[]string{"wss://relay.example.com"},
core.WithLive(), core.WithLimit(50))
for ev := range feed {
fmt.Println(ev.ID)
}
}
Output:
func (*Client) Subscribe ¶
func (c *Client) Subscribe(ctx context.Context, filters []nostr.Filter, relayHints []string) (*Subscription, error)
Subscribe creates a new subscription with filters and relay hints
func (*Client) SwitchToIndexRelays ¶ added in v0.5.0
SwitchToIndexRelays releases the current session's relay leases. There is nothing to "switch back" to in the additive model: the index/seed relays are pinned and kept up by the health check, so they remain connected regardless.
func (*Client) SwitchToUserRelays ¶ added in v0.4.12
func (c *Client) SwitchToUserRelays(userRelays []RelayConfig) error
SwitchToUserRelays holds the user's relays for the session (additive — see ReplaceRelayConnections). Index relays stay connected alongside them.
func (*Client) WarmMediaServers ¶
WarmMediaServers resolves a user's media-server lists into the cache in the background — used by login-hydration so the media settings page is instant.
func (*Client) WarmRelays ¶
WarmRelays kicks an asynchronous resolution of a user's relay lists into the directory if not already cached, so their mailbox relays join the "known" set and the cache is ready for outbox-routed fetches next time — without blocking the caller and without dialing (resolution queries the index relays only).
func (*Client) WarmUserRelayLists ¶
WarmUserRelayLists resolves a user's relay lists into the cache in the background — used by login-hydration so the settings page is instant.
type ClientError ¶
type ClientError struct {
Message string
}
ClientError represents client-specific errors
func (*ClientError) Error ¶
func (e *ClientError) Error() string
type Config ¶
type Config struct {
IndexRelays []string `json:"index_relays"`
ConnectionTimeout time.Duration `json:"connection_timeout"`
ReadTimeout time.Duration `json:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout"`
MaxConnections int `json:"max_connections"`
RetryAttempts int `json:"retry_attempts"`
RetryDelay time.Duration `json:"retry_delay"`
KeepAlive bool `json:"keep_alive"`
UserAgent string `json:"user_agent"`
// Outbox-pool lifecycle (#56). Zero values fall back to built-in defaults
// in NewRelayPool / the lease methods, so older callers that build a Config
// by hand keep working.
DialConcurrency int `json:"dial_concurrency"` // max simultaneous dials
IdleTTL time.Duration `json:"idle_ttl"` // evict a 0-lease conn after this idle span
BackoffBase time.Duration `json:"backoff_base"` // first dial-retry backoff
BackoffMax time.Duration `json:"backoff_max"` // dial-retry backoff ceiling
RelayListTTL time.Duration `json:"relay_list_ttl"` // per-user relay-list directory cache TTL
RelayListNegTTL time.Duration `json:"relay_list_neg_ttl"` // shorter TTL for "no list published"
OpenTimeout time.Duration `json:"open_timeout"` // per-dial cap for on-demand outbox dials (< ConnectionTimeout)
// Logger, when non-nil, replaces the process-wide client-library logger at
// NewClient time (see [SetLogger]). Not serialized; defaults to grain's
// client-core logging so behaviour is unchanged when unset.
Logger Logger `json:"-"`
// RelayListStore, when non-nil, backs the relay directory's per-user
// resolutions with a custom store (see [RelayListStore]) — e.g. a database
// for persistence across restarts. Not serialized; defaults to in-memory.
RelayListStore RelayListStore `json:"-"`
}
Config holds client-specific configuration
Example (RelayListStore) ¶
Backing the relay directory with a custom store (e.g. a database) instead of the default in-memory cache.
package main
import (
"github.com/0ceanslim/grain/client/core"
)
// exampleStore is a minimal RelayListStore — a pubkey -> *UserRelays map. The
// directory owns the TTL and single-flight logic, so a store only needs to be a
// correct key-value map; a real one would persist to a database.
type exampleStore struct{ m map[string]*core.UserRelays }
func (s exampleStore) Get(pk string) (*core.UserRelays, bool) { ur, ok := s.m[pk]; return ur, ok }
func (s exampleStore) Set(pk string, ur *core.UserRelays) { s.m[pk] = ur }
func (s exampleStore) Delete(pk string) { delete(s.m, pk) }
func (s exampleStore) Range(fn func(string, *core.UserRelays) bool) {
for k, v := range s.m {
if !fn(k, v) {
return
}
}
}
func main() {
cfg := core.DefaultConfig()
cfg.RelayListStore = exampleStore{m: map[string]*core.UserRelays{}}
_ = core.NewClient(cfg)
}
Output:
func ConfigFromServerConfig ¶ added in v0.4.11
func ConfigFromServerConfig(serverCfg *cfgType.ServerConfig) *Config
ConfigFromServerConfig creates a client config from server configuration
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns a sensible default configuration. The IndexRelays seed list mirrors the indexer-relay role described in #56: relays that host metadata and relay lists for everyone, used to resolve NIP-65 / DM-relay lists for arbitrary users.
type ConnectionStatus ¶
type ConnectionStatus int
ConnectionStatus represents the state of a relay connection
const ( StatusDisconnected ConnectionStatus = iota StatusConnecting StatusConnected StatusError )
type EncryptedContent ¶
type EncryptedContent struct {
Blocked string `json:"blocked,omitempty"`
Search string `json:"search,omitempty"`
Favorites string `json:"favorites,omitempty"`
Private string `json:"private,omitempty"`
}
EncryptedContent holds the raw (still-encrypted) `.content` of each NIP-51/37 list that has private entries, keyed by the same names as EncryptedFlags. Empty strings when a list has no private content.
type EncryptedFlags ¶
type EncryptedFlags struct {
Blocked bool `json:"blocked"`
Search bool `json:"search"`
Favorites bool `json:"favorites"`
Private bool `json:"private"` // 10013 — typically always encrypted
}
EncryptedFlags reports which NIP-51/37 lists have private (encrypted) entries.
type Encrypter ¶
type Encrypter interface {
// NIP44Encrypt encrypts plaintext to peerPubKey, returning a base64 NIP-44
// payload (v2, the deployed standard).
NIP44Encrypt(peerPubKey, plaintext string) (string, error)
// NIP44Decrypt decrypts a base64 NIP-44 payload from peerPubKey, accepting
// any supported version.
NIP44Decrypt(peerPubKey, payload string) (string, error)
}
Encrypter is an optional capability a Signer may also implement: NIP-44 payload encryption between the signer's key and a peer pubkey, used for NIP-51 private list content, NIP-17 DMs, and similar. It mirrors the nip44Encrypt/nip44Decrypt methods a NIP-07/NIP-46 browser signer exposes, so the same calling code works whether the key lives in the browser or in a downstream Go consumer's EventSigner.
peerPubKey is 64-char lowercase hex (x-only). For self-encryption (NIP-51 lists are encrypted to the author themselves) pass the signer's own pubkey.
type EncrypterV3 ¶
type EncrypterV3 interface {
NIP44V3Encrypt(peerPubKey string, kind uint32, scope, plaintext []byte) (string, error)
NIP44V3Decrypt(peerPubKey string, expectedKind uint32, expectedScope []byte, payload string) ([]byte, error)
}
EncrypterV3 is the optional NIP-44 v3 capability: encryption that additionally binds an event kind and a scope string (cross-context replay protection + signer access control). v3 is a draft proposal with few deployed peers, so prefer Encrypter (v2) for interop; this exists so downstream consumers can opt in. kind is a uint32; scope is a UTF-8 byte string (may be empty).
type EventBuilder ¶
type EventBuilder struct {
// contains filtered or unexported fields
}
EventBuilder provides a fluent interface for building Nostr events
func NewContactList ¶
func NewContactList() *EventBuilder
NewContactList creates a builder for a contact list (kind 3)
func NewDeletion ¶
func NewDeletion(eventIDs []string, reason string) *EventBuilder
NewDeletion creates a builder for a deletion event (kind 5)
func NewEventBuilder ¶
func NewEventBuilder(kind int) *EventBuilder
NewEventBuilder creates a new event builder with the specified kind
func NewProfile ¶
func NewProfile() *EventBuilder
NewProfile creates a builder for a profile event (kind 0)
func NewReaction ¶
func NewReaction(eventID string, content string) *EventBuilder
NewReaction creates a builder for a reaction (kind 7)
func NewRelayList ¶
func NewRelayList() *EventBuilder
NewRelayList creates a builder for a relay list (kind 10002)
func NewRepost ¶
func NewRepost(eventID string, relayHint string) *EventBuilder
NewRepost creates a builder for a repost (kind 6)
func NewTextNote ¶
func NewTextNote(content string) *EventBuilder
NewTextNote creates a builder for a text note (kind 1)
func (*EventBuilder) ATag ¶
func (eb *EventBuilder) ATag(kind int, pubkey string, dTag string, relayHint ...string) *EventBuilder
ATag adds an 'a' tag (address reference) to the event
func (*EventBuilder) Build ¶
func (eb *EventBuilder) Build() *nostr.Event
Build constructs the final Event struct (without signing)
func (*EventBuilder) Content ¶
func (eb *EventBuilder) Content(content string) *EventBuilder
Content sets the content of the event
func (*EventBuilder) CreatedAt ¶
func (eb *EventBuilder) CreatedAt(t time.Time) *EventBuilder
CreatedAt sets the created_at timestamp for the event
func (*EventBuilder) DTag ¶
func (eb *EventBuilder) DTag(identifier string) *EventBuilder
DTag adds a 'd' tag (identifier) to the event
func (*EventBuilder) ETag ¶
func (eb *EventBuilder) ETag(eventID string, relayHint, marker string) *EventBuilder
ETag adds an 'e' tag (event reference) to the event
func (*EventBuilder) PTag ¶
func (eb *EventBuilder) PTag(pubkey string, relayHint ...string) *EventBuilder
PTag adds a 'p' tag (pubkey reference) to the event
func (*EventBuilder) RTag ¶
func (eb *EventBuilder) RTag(relayURL string, marker string) *EventBuilder
RTag adds an 'r' tag (relay reference) to the event
func (*EventBuilder) TTag ¶
func (eb *EventBuilder) TTag(hashtag string) *EventBuilder
TTag adds a 't' tag (hashtag) to the event
func (*EventBuilder) Tag ¶
func (eb *EventBuilder) Tag(name string, values ...string) *EventBuilder
Tag adds a generic tag to the event
type EventSigner ¶
type EventSigner struct {
// contains filtered or unexported fields
}
EventSigner handles event signing with private keys
func NewEventSigner ¶
func NewEventSigner(privateKeyHex string) (*EventSigner, error)
NewEventSigner creates a new event signer from a hex private key
func NewEventSignerFromRandom ¶
func NewEventSignerFromRandom() (*EventSigner, error)
NewEventSignerFromRandom creates a new event signer with a random private key
func (*EventSigner) GetPrivateKeyHex ¶
func (es *EventSigner) GetPrivateKeyHex() string
GetPrivateKeyHex returns the private key in hex format (use carefully!)
func (*EventSigner) GetPublicKey ¶
func (es *EventSigner) GetPublicKey() string
GetPublicKey returns the public key in hex format
func (*EventSigner) NIP44Decrypt ¶
func (es *EventSigner) NIP44Decrypt(peerPubKey, payload string) (string, error)
NIP44Decrypt decrypts a base64 NIP-44 payload from peerPubKey (any supported version).
func (*EventSigner) NIP44Encrypt ¶
func (es *EventSigner) NIP44Encrypt(peerPubKey, plaintext string) (string, error)
NIP44Encrypt encrypts plaintext to peerPubKey using NIP-44 v2. peerPubKey is 64-char hex (x-only); for NIP-51 private lists (encrypted to self) pass the signer's own pubkey.
Example ¶
NIP-44 v2 conversation encryption with the built-in signer (the same key holder decrypts).
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
signer, err := core.NewEventSigner("64-char-hex-private-key")
if err != nil {
return
}
ciphertext, err := signer.NIP44Encrypt("peer-pubkey-hex", "hello")
if err != nil {
return
}
plaintext, err := signer.NIP44Decrypt("peer-pubkey-hex", ciphertext)
if err != nil {
return
}
fmt.Println(plaintext)
}
Output:
func (*EventSigner) NIP44V3Decrypt ¶
func (es *EventSigner) NIP44V3Decrypt(peerPubKey string, expectedKind uint32, expectedScope []byte, payload string) ([]byte, error)
NIP44V3Decrypt decrypts a NIP-44 v3 payload from peerPubKey, requiring it to match the expected kind + scope.
func (*EventSigner) NIP44V3Encrypt ¶
func (es *EventSigner) NIP44V3Encrypt(peerPubKey string, kind uint32, scope, plaintext []byte) (string, error)
NIP44V3Encrypt encrypts plaintext to peerPubKey under NIP-44 v3, binding the event kind and scope. v3 is a draft — prefer NIP44Encrypt (v2) for interop.
func (*EventSigner) PublicKey ¶
func (es *EventSigner) PublicKey() string
PublicKey returns the public key in hex format. It satisfies the Signer seam; GetPublicKey is retained as an alias for existing callers.
type FilterBuilder ¶
type FilterBuilder struct {
// contains filtered or unexported fields
}
FilterBuilder provides a fluent interface for building Nostr filters
func NewFilterBuilder ¶
func NewFilterBuilder() *FilterBuilder
NewFilterBuilder creates a new filter builder
func (*FilterBuilder) Authors ¶
func (fb *FilterBuilder) Authors(pubkeys ...string) *FilterBuilder
Authors sets the authors filter
func (*FilterBuilder) Build ¶
func (fb *FilterBuilder) Build() nostr.Filter
Build returns the constructed filter
func (*FilterBuilder) IDs ¶
func (fb *FilterBuilder) IDs(ids ...string) *FilterBuilder
IDs sets the event IDs filter
func (*FilterBuilder) Kinds ¶
func (fb *FilterBuilder) Kinds(kinds ...int) *FilterBuilder
Kinds sets the kinds filter
func (*FilterBuilder) Limit ¶
func (fb *FilterBuilder) Limit(limit int) *FilterBuilder
Limit sets the limit filter
func (*FilterBuilder) Since ¶
func (fb *FilterBuilder) Since(timestamp time.Time) *FilterBuilder
Since sets the since timestamp filter
func (*FilterBuilder) Tag ¶
func (fb *FilterBuilder) Tag(name string, values ...string) *FilterBuilder
Tag adds a tag filter
func (*FilterBuilder) Until ¶
func (fb *FilterBuilder) Until(timestamp time.Time) *FilterBuilder
Until sets the until timestamp filter
type KnownRelayStatus ¶
type KnownRelayStatus struct {
URL string `json:"url"`
Connected bool `json:"connected"`
Pinned bool `json:"pinned"`
Leased bool `json:"leased"`
}
KnownRelayStatus pairs a known relay URL with its live pool status, for the known-relays browser. The status fields are inlined rather than embedding RelayLiveStatus so the OpenAPI generator (swag) can resolve the type — the JSON shape is identical either way.
type Logger ¶
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}
Logger is the structured-logging seam for the importable client library. It matches the method set of *slog.Logger, so the standard library logger — and grain's own log.ClientCore() — satisfy it as-is. A consumer that doesn't want to be tied to grain's logging plugs in their own with SetLogger.
type Mailboxes ¶
type Mailboxes struct {
Read []string `json:"read"`
Write []string `json:"write"`
Both []string `json:"both"`
}
Mailboxes represents a user's relay preferences from NIP-65
func (Mailboxes) ToStringSlice ¶
ToStringSlice combines Read, Write, and Both into a single []string
type MediaDirectory ¶
type MediaDirectory struct {
// contains filtered or unexported fields
}
MediaDirectory caches per-user media-server resolutions with a TTL, collapsing concurrent lookups for the same pubkey into a single network fetch. It is structurally identical to RelayDirectory but kept separate: media servers are a distinct concern from relays and resolve from different event kinds.
func (*MediaDirectory) Invalidate ¶
func (d *MediaDirectory) Invalidate(pubkey string)
Invalidate drops a cached entry so the next Lookup re-resolves — call after the user publishes an updated list, so the change shows immediately.
func (*MediaDirectory) Lookup ¶
func (d *MediaDirectory) Lookup(pubkey string) *MediaServers
Lookup returns the user's media servers, resolving and caching on a miss or stale entry. Concurrent lookups for the same pubkey share one resolution.
type MediaServerInfo ¶
type MediaServerInfo struct {
URL string `json:"url"` // normalised base URL
Kind string `json:"kind"` // MediaKindBlossom | MediaKindNIP96
Name string `json:"name"` // display name, e.g. "Happy Tavern"
Cost string `json:"cost"` // "free" | "paid"
Retention string `json:"retention"` // "permanent" | "ephemeral"
Mirror bool `json:"mirror"` // accepts BUD-04 /mirror (Blossom only)
Note string `json:"note"` // short blurb shown under the entry
CTA string `json:"cta"` // optional signup / pricing link
}
MediaServerInfo is static metadata about a media server grain knows: what protocol it speaks, what it costs, how long it keeps blobs, and whether it accepts BUD-04 mirror requests. It drives the free/paid + retention chips in the settings UI and decides which servers can be offered as mirror targets.
func LookupMediaServerInfo ¶
func LookupMediaServerInfo(rawURL string) (MediaServerInfo, bool)
LookupMediaServerInfo returns grain's static capability metadata for a server URL if it knows it, matching on the normalised URL. The boolean reports a hit; unknown servers (a user's own additions) simply carry no chips.
func SuggestedMediaServers ¶
func SuggestedMediaServers() []MediaServerInfo
SuggestedMediaServers returns a copy of grain's curated quick-add suggestions (Blossom preferred, NIP-96 legacy fallback).
type MediaServers ¶
type MediaServers struct {
Blossom []string // kind 10063 — Blossom servers, primary first
NIP96 []string // kind 10096 — legacy NIP-96 HTTP servers, primary first
FetchedAt time.Time // when this was resolved
Negative bool // user has published neither list — cached briefly
}
MediaServers holds a user's published media-server lists: Blossom servers (NIP-B7 / BUD-03 kind 10063) and legacy NIP-96 HTTP servers (kind 10096). Both are `["server", <url>]` tag lists in preference order — the first entry is the user's primary, the rest are mirrors / fallbacks. Resolved from the network and cached with a TTL, mirroring the relay directory.
func (*MediaServers) HasAny ¶
func (m *MediaServers) HasAny() bool
HasAny reports whether the user has any media server configured at all. The upload flow uses this to decide between "open the picker" and "prompt the user to set some up".
type MessageRouter ¶
type MessageRouter struct {
// contains filtered or unexported fields
}
MessageRouter handles routing messages to subscriptions and OK responses to in-flight publishes.
func NewMessageRouter ¶
func NewMessageRouter() *MessageRouter
NewMessageRouter creates a new message router
func (*MessageRouter) AuthChallenge ¶
func (mr *MessageRouter) AuthChallenge(relayURL string) string
AuthChallenge returns a relay's current challenge ("" if none pending).
func (*MessageRouter) AuthStates ¶
func (mr *MessageRouter) AuthStates() []AuthState
AuthStates returns a snapshot of every relay that has challenged us.
func (*MessageRouter) MarkAuthed ¶
func (mr *MessageRouter) MarkAuthed(relayURL string)
MarkAuthed records that we've answered a relay's challenge this session.
func (*MessageRouter) RegisterOKWaiter ¶
func (mr *MessageRouter) RegisterOKWaiter(eventID string, buf int) chan OKResult
RegisterOKWaiter starts collecting OK responses for an event being published. The returned channel is buffered for up to buf relays; UnregisterOKWaiter must be called when done.
func (*MessageRouter) RegisterSubscription ¶
func (mr *MessageRouter) RegisterSubscription(subID string, sub *Subscription)
RegisterSubscription registers a subscription for message routing
func (*MessageRouter) RemoveAuth ¶
func (mr *MessageRouter) RemoveAuth(relayURL string)
RemoveAuth forgets a relay's AUTH state (the user revoked trust).
func (*MessageRouter) RouteAuth ¶
func (mr *MessageRouter) RouteAuth(relayURL, challenge string)
RouteAuth records a relay's AUTH challenge. A new challenge clears the authed flag for that relay so the manager prompts a fresh answer.
func (*MessageRouter) RouteMessage ¶
func (mr *MessageRouter) RouteMessage(subID string, messageType string, data interface{}, relayURL string)
RouteMessage routes a message to the appropriate subscription
func (*MessageRouter) RouteOK ¶
func (mr *MessageRouter) RouteOK(eventID string, ok OKResult)
RouteOK delivers a relay's OK response to the publisher waiting on it, if any.
func (*MessageRouter) UnregisterOKWaiter ¶
func (mr *MessageRouter) UnregisterOKWaiter(eventID string)
UnregisterOKWaiter stops collecting OK responses for an event.
func (*MessageRouter) UnregisterSubscription ¶
func (mr *MessageRouter) UnregisterSubscription(subID string)
UnregisterSubscription removes a subscription from message routing
type OKResult ¶
type OKResult struct {
Relay string `json:"relay"`
Accepted bool `json:"accepted"`
Reason string `json:"reason,omitempty"`
}
OKResult is a relay's NIP-20 response to a published event: whether it was accepted and any human-readable reason.
type PoolStats ¶
type PoolStats struct {
Known int `json:"known"` // relays the client is aware of (defaults + resolved lists + connections)
Total int `json:"total"` // relays tracked in the pool (have a connection slot)
Connected int `json:"connected"` // currently connected
Pinned int `json:"pinned"` // index/seed relays kept alive
Leased int `json:"leased"` // connections with at least one active lease
}
PoolStats is a snapshot of the relay pool's connection counts, for status / observability (e.g. an "x / y relays connected" dashboard indicator).
type RelayConfig ¶ added in v0.4.12
type RelayConfig struct {
URL string `json:"url"`
Read bool `json:"read"`
Write bool `json:"write"`
}
RelayConfig represents relay configuration with permissions
type RelayConnection ¶
type RelayConnection struct {
URL string
Conn *websocket.Conn
Status ConnectionStatus
LastPing time.Time
Subscriptions map[string]bool
// contains filtered or unexported fields
}
RelayConnection represents a single relay connection
type RelayDirectory ¶
type RelayDirectory struct {
// contains filtered or unexported fields
}
RelayDirectory caches per-user relay-role resolutions with a TTL, collapsing concurrent lookups for the same pubkey into a single network fetch.
func (*RelayDirectory) Cached ¶
func (d *RelayDirectory) Cached(pubkey string) (*UserRelays, bool)
Cached returns a user's resolved relays only if a fresh entry is already in the cache, WITHOUT triggering a network resolve. For latency-sensitive paths (e.g. rendering a profile) that must not block on resolution.
func (*RelayDirectory) Invalidate ¶
func (d *RelayDirectory) Invalidate(pubkey string)
Invalidate drops a cached entry so the next Lookup re-resolves — e.g. after observing a newer relay-list event for the user.
func (*RelayDirectory) KnownRelays ¶
func (d *RelayDirectory) KnownRelays() []string
KnownRelays returns the distinct relay URLs across every resolved entry — the union of all users' outbox / inbox / DM relays the directory has seen. This is the bulk of the client's "known relays" set: relays we're aware of from indexer-seeded mailbox lists, whether or not we're connected to them.
func (*RelayDirectory) Lookup ¶
func (d *RelayDirectory) Lookup(pubkey string) *UserRelays
Lookup returns the user's relay roles, resolving and caching on a miss or stale entry. Concurrent lookups for the same pubkey share one resolution.
func (*RelayDirectory) Store ¶
func (d *RelayDirectory) Store(pubkey string, ur *UserRelays)
Store inserts or merges a resolved relay set for a pubkey directly, without a network resolve — used by bulk seeding. A NIP-65 (10002) and a NIP-17 (10050) event for the same author arrive separately, so non-empty fields merge rather than clobber.
type RelayInfo ¶
type RelayInfo struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
PubKey string `json:"pubkey,omitempty"`
Software string `json:"software,omitempty"`
Version string `json:"version,omitempty"`
SupportedNIPs []int `json:"supported_nips,omitempty"`
Icon string `json:"icon,omitempty"`
Limitation *RelayLimits `json:"limitation,omitempty"`
}
RelayInfo is the subset of a relay's NIP-11 document the relay manager shows.
type RelayLimits ¶
type RelayLimits struct {
AuthRequired bool `json:"auth_required,omitempty"`
PaymentRequired bool `json:"payment_required,omitempty"`
RestrictedWrites bool `json:"restricted_writes,omitempty"`
MaxMessageLength int `json:"max_message_length,omitempty"`
}
RelayLimits is the NIP-11 `limitation` block — the flags that matter to the UI (whether the relay requires AUTH or payment to use).
type RelayListEntry ¶
type RelayListEntry struct {
URL string `json:"url"`
Read bool `json:"read"`
Write bool `json:"write"`
}
RelayListEntry is one relay in a relay-list event. Read / Write are only meaningful for NIP-65 kind 10002 — an entry that is both (or neither) is written unmarked, meaning "read and write". The other kinds ignore the flags.
func ParseNIP65Entries ¶
func ParseNIP65Entries(event *nostr.Event) []RelayListEntry
ParseNIP65Entries parses a kind-10002 event's `r` tags into relay entries with read/write flags. An unmarked entry (`["r", url]`) is both read and write; `["r", url, "read"]` / `["r", url, "write"]` set one side. URLs are normalised; a relay listed twice has its flags OR-ed together.
type RelayListStore ¶
type RelayListStore interface {
// Get returns the stored resolution for a pubkey and whether one exists.
Get(pubkey string) (*UserRelays, bool)
// Set stores (or replaces) the resolution for a pubkey.
Set(pubkey string, ur *UserRelays)
// Delete removes any stored resolution for a pubkey.
Delete(pubkey string)
// Range calls fn for each stored entry until fn returns false. Used to build
// the union "known relays" set; iteration order is unspecified.
Range(fn func(pubkey string, ur *UserRelays) bool)
}
RelayListStore is the pluggable persistence seam for the relay directory's per-user resolutions (a user's outbox / inbox / DM relays). The default is in-memory; a consumer can plug a shared or persistent store — e.g. a database — so resolutions survive restarts or are shared across instances.
The RelayDirectory owns the TTL and single-flight logic and serializes access, so an implementation only needs to be a correct key-value map of pubkey -> *UserRelays; it does not need its own freshness or de-duplication logic. (Because the directory holds its lock across store calls, a store whose Get/Set hit slow I/O will serialize lookups — a DB-backed store that cares should keep a fast in-memory layer in front.)
type RelayLiveStatus ¶
type RelayLiveStatus struct {
Connected bool `json:"connected"`
Pinned bool `json:"pinned"`
Leased bool `json:"leased"`
}
RelayLiveStatus is the pool's live view of one relay for the known-relays browser. The zero value means "known but not currently in the pool".
type RelayPool ¶
type RelayPool struct {
// contains filtered or unexported fields
}
RelayPool manages multiple relay connections
func NewRelayPool ¶
NewRelayPool creates a new relay pool
func (*RelayPool) Acquire ¶
func (rp *RelayPool) Acquire(url string) (*RelayConnection, error)
Acquire returns a connected RelayConnection for url, dialing on demand, and takes a lease on it. Every successful Acquire must be balanced by a Release so the connection can eventually be idle-evicted. Concurrent Acquires for the same url share a single dial (single-flight) and the resulting connection.
func (*RelayPool) AuthChallenge ¶
AuthChallenge returns a relay's pending challenge ("" if none).
func (*RelayPool) AuthRequests ¶
AuthRequests returns the relays that have sent an AUTH challenge this session.
func (*RelayPool) BroadcastMessage ¶
BroadcastMessage sends a message to multiple relays
func (*RelayPool) CloseConnection ¶
CloseConnection closes a specific relay connection
func (*RelayPool) EnsureConnectedForSend ¶
EnsureConnectedForSend makes sure url is connected so the publish path can send to it, redialing a dropped target relay bounded by timeout. It ignores dial backoff because a publish is an explicit user action (unlike a background outbox dial that should fail fast), and it does not retain the lease — the caller only needs the pooled connection for the immediate send, and the sweeper evicts it later if it stays idle. Returns an error if it can't connect in time, which the broadcast surfaces as a per-relay timeout.
func (*RelayPool) GetConnectedRelays ¶
GetConnectedRelays returns a list of connected relay URLs
func (*RelayPool) GetConnection ¶
func (rp *RelayPool) GetConnection(url string) (*RelayConnection, error)
GetConnection returns a specific relay connection
func (*RelayPool) Pin ¶
Pin marks urls so the idle sweeper never evicts them — used for the index/ seed relays that must stay connected to resolve relay lists for anyone. It does not dial; Acquire still establishes the connection on demand.
func (*RelayPool) RegisterSubscription ¶
func (rp *RelayPool) RegisterSubscription(subID string, sub *Subscription)
RegisterSubscription registers a subscription for message routing
func (*RelayPool) Release ¶
Release drops one lease on url's connection. When the last lease is released the connection is marked idle (eligible for the sweeper) but not closed, so a later Acquire can reuse it. Releasing an unknown url, or one already at zero leases, is a safe no-op — this guards against the lease-counter underflow class of bug.
func (*RelayPool) RemoveAuth ¶
RemoveAuth forgets a relay's AUTH state.
func (*RelayPool) SendAuth ¶
SendAuth sends a signed NIP-42 auth event (kind 22242) to a relay and marks it authed for the session. The connection that issued the challenge must still be open (the challenge is bound to it).
func (*RelayPool) SendMessage ¶
SendMessage sends a message to a specific relay
func (*RelayPool) StartEvictionSweeper ¶
StartEvictionSweeper runs evictIdle on an interval until ctx is cancelled, bounded to the caller's lifetime like the relay health check (#93).
func (*RelayPool) Stats ¶
Stats returns a snapshot of the pool's connection counts (without Known, which the Client fills in since it spans the directory too).
func (*RelayPool) StatusOf ¶
func (rp *RelayPool) StatusOf(url string) RelayLiveStatus
StatusOf returns the pool's live status for url.
func (*RelayPool) UnregisterSubscription ¶
UnregisterSubscription removes a subscription from message routing
type Role ¶
type Role uint16
Role identifies the function a relay serves for a user under the outbox model. Roles fall into three classes (see docs/design/outbox-relay-pool.md §3):
- Per-target, event-derived: RoleOutbox, RoleInbox, RoleDMInbox — resolved from the *target* user's published NIP-65 / NIP-17 lists.
- Self-only, event-derived: RoleSearch, RoleBlocked, RoleFavorite, RolePrivateHome — the logged-in user's own NIP-51 / NIP-37 lists.
- Locally configured: RoleIndexer, RoleBroadcast, RoleProxy, RoleLocal, RoleTrusted — app config, applied across every user you interact with.
A relay may hold several roles at once (a relay can be both your outbox and your inbox), so Role is a bitmask: combine with | and test with Role.Has.
const ( RoleOutbox Role = 1 << iota // NIP-65 write (10002): you publish here; others fetch your notes here RoleInbox // NIP-65 read (10002): replies, mentions, and zaps reach you here RoleDMInbox // NIP-17 (10050): encrypted direct messages are delivered here RoleSearch // NIP-51 (10007): relays queried for NIP-50 search RoleBlocked // NIP-51 (10006): never dialed RoleFavorite // NIP-51 (10012): surfaced in UI; no routing effect RolePrivateHome // NIP-37 (10013, NIP-44 encrypted): relays for private content RoleIndexer // local config: seeds for resolving anyone's metadata / relay lists RoleBroadcast // local config: fan-out relays ("event blasters"); writes mirror here RoleProxy // local config: aggregator; when set, short-circuits outbox routing RoleLocal // local config: same-device / LAN relays, preferred for latency RoleTrusted // local config: relays the client will sign NIP-42 AUTH challenges for )
func RoleForListKind ¶
RoleForListKind maps a replaceable relay-list event kind to the role(s) its `relay`/`r` entries carry — e.g. 10002 → RoleOutbox|RoleInbox (the read/write split is per-entry markers), 10050 → RoleDMInbox. ok is false for kinds that are not relay lists.
type RouteOp ¶
type RouteOp int
RouteOp names a read-routing intent so a caller can inspect which relays an operation would use, via Client.Route, without performing it.
type SessionRelays ¶
type SessionRelays struct {
// contains filtered or unexported fields
}
SessionRelays is a user's editable, role-tagged relay configuration for one session — the set a downstream app reads and edits to inspect or steer routing (design §4.3 / §11, "expose the pool + role assignments so callers can read/edit them"). Each relay URL carries a Role bitmask. Safe for concurrent use.
It is a plain, in-memory container: a consumer seeds it from a user's resolved event-derived roles and layers local overrides on top. Wiring edits back into live routing is incremental — today the one override that affects routing is the fixed-relay pin, exposed via UserContext.PinFixedRelays.
Example ¶
Inspecting and editing the role-tagged session relay configuration.
package main
import (
"fmt"
"github.com/0ceanslim/grain/client/core"
)
func main() {
client := core.NewClient(core.DefaultConfig())
uc := client.NewUserContext("pubkey-hex")
uc.Relays().Set("wss://relay.example.com", core.RoleOutbox|core.RoleInbox)
uc.Relays().Add("wss://dm.example.com", core.RoleDMInbox)
for _, url := range uc.Relays().ByRole(core.RoleOutbox) {
fmt.Println("outbox:", url)
}
}
Output:
func (*SessionRelays) Add ¶
func (s *SessionRelays) Add(url string, roles Role)
Add OR-s roles onto url, keeping any it already holds.
func (*SessionRelays) All ¶
func (s *SessionRelays) All() map[string]Role
All returns a snapshot copy of the full url→roles map.
func (*SessionRelays) ByRole ¶
func (s *SessionRelays) ByRole(role Role) []string
ByRole returns the relay URLs carrying the given role, sorted for stability.
func (*SessionRelays) Get ¶
func (s *SessionRelays) Get(url string) Role
Get returns the roles assigned to url (the zero Role if absent).
func (*SessionRelays) Remove ¶
func (s *SessionRelays) Remove(url string)
Remove drops url and all of its roles.
func (*SessionRelays) Set ¶
func (s *SessionRelays) Set(url string, roles Role)
Set replaces the roles assigned to url. Passing the zero Role removes it.
type Signer ¶
type Signer interface {
// PublicKey returns the signer's public key as 64-char lowercase hex.
PublicKey() string
// SignEvent fills the event's PubKey, ID, and Sig fields in place: PubKey is
// set to PublicKey(), and Sig is a Schnorr signature over the NIP-01 id
// computed from the serialized event.
SignEvent(event *nostr.Event) error
}
Signer produces signatures for Nostr events on behalf of one pubkey. A library consumer supplies a Signer to publish — a local key via EventSigner, or their own implementation (NIP-46 remote signer, hardware, HSM). Read-only callers supply none.
grain's own web client signs in the browser with the user's NIP-07 / NIP-46 signer, so grain's server side carries no Signer; this seam exists for downstream Go consumers building a client on the library.
type StreamOption ¶
type StreamOption func(*streamConfig)
StreamOption configures Client.StreamEvents / Client.QueryEvents.
func WithLimit ¶
func WithLimit(n int) StreamOption
WithLimit closes the stream after n events have been delivered (0 = no limit).
func WithLive ¶
func WithLive() StreamOption
WithLive keeps the stream open past every relay's end-of-stored-events, so it keeps delivering newly published events until the context or timeout ends it. Default is a bounded fetch that closes once all stored events are in.
func WithTimeout ¶
func WithTimeout(d time.Duration) StreamOption
WithTimeout caps how long the stream runs before closing (default 10s). The stream also ends when every relay reports end-of-stored-events (unless WithLive), the limit is reached, or the context is cancelled.
type Subscription ¶
type Subscription struct {
ID string
Filters []nostr.Filter
Relays []string
Events chan *nostr.Event
Errors chan error
Done chan struct{}
EOSE chan string // NEW: Channel for EOSE messages with relay URL
// contains filtered or unexported fields
}
Subscription manages a Nostr subscription across multiple relays
func NewSubscription ¶
func NewSubscription(id string, filters []nostr.Filter, relays []string, client *Client) *Subscription
NewSubscription creates a new subscription instance
func (*Subscription) AddRelay ¶
func (s *Subscription) AddRelay(url string) error
AddRelay adds a new relay to an active subscription
func (*Subscription) Close ¶
func (s *Subscription) Close() error
Update Close to close the EOSE channel too:
func (*Subscription) GetFilters ¶
func (s *Subscription) GetFilters() []nostr.Filter
GetFilters returns a copy of the subscription filters
func (*Subscription) GetRelayCount ¶
func (s *Subscription) GetRelayCount() int
GetRelayCount returns the number of relays in this subscription
func (*Subscription) IsActive ¶
func (s *Subscription) IsActive() bool
IsActive returns whether the subscription is currently active
func (*Subscription) RemoveRelay ¶
func (s *Subscription) RemoveRelay(url string) error
RemoveRelay removes a relay from the subscription
func (*Subscription) Start ¶
func (s *Subscription) Start() error
Start begins the subscription on all specified relays
type UserContext ¶
type UserContext struct {
// contains filtered or unexported fields
}
UserContext is the per-session handle a downstream app uses to act as one user on the outbox engine: their editable relay config (SessionRelays), an optional Signer for publishing, and the shared Client that owns the connection pool. Construct it with Client.NewUserContext.
grain's own web layer (client/api, client/session, …) is a reference consumer of this surface; a CLI or bot uses it directly. Read-only callers omit the signer.
Pre-1.0 note: the network methods don't take a context.Context yet (it lands in a follow-up slice), and PublishDM is deferred until NIP-44 encryption is available. See docs/design/outbox-relay-pool.md §11.
func (*UserContext) ClearFixedRelays ¶
func (uc *UserContext) ClearFixedRelays()
ClearFixedRelays disables the override and restores outbox routing.
func (*UserContext) Client ¶
func (uc *UserContext) Client() *Client
Client returns the shared client that owns the connection pool.
func (*UserContext) FetchNotes ¶
func (uc *UserContext) FetchNotes(ctx context.Context, author string, opts ...StreamOption) []*nostr.Event
FetchNotes collects author's text notes (kind 1) from their outbox relays. Best-effort: per-relay failures are logged, so an empty result means "none found" rather than a hard error. For incremental delivery use [StreamNotes].
func (*UserContext) FixedRelaysEnabled ¶
func (uc *UserContext) FixedRelaysEnabled() bool
FixedRelaysEnabled reports whether the fixed-relay override is active.
func (*UserContext) PinFixedRelays ¶
func (uc *UserContext) PinFixedRelays(readRelays, writeRelays []string)
PinFixedRelays enables the fixed-relay override — reads come from readRelays, writes go to writeRelays — which DISABLES the outbox model. Discouraged; for users who explicitly want a fixed-/single-relay client. The override is client-wide (see Client.SetFixedRelays), not scoped to this context.
func (*UserContext) PublicKey ¶
func (uc *UserContext) PublicKey() string
PublicKey returns the context user's pubkey (hex).
func (*UserContext) Publish ¶
func (uc *UserContext) Publish(ctx context.Context, event *nostr.Event) ([]BroadcastResult, error)
Publish routes an already-signed event under the outbox model (the author's outbox ∪ each p-tagged recipient's inbox; metadata also to the indexers) and broadcasts it, returning the per-relay results.
func (*UserContext) Relays ¶
func (uc *UserContext) Relays() *SessionRelays
Relays returns the user's editable, role-tagged session relay config.
func (*UserContext) Reply ¶
func (uc *UserContext) Reply(ctx context.Context, parent *nostr.Event, content string) (*nostr.Event, []BroadcastResult, error)
Reply builds a NIP-10 kind-1 reply to parent, signs it as this user, and publishes it under the outbox model so it reaches the parent author's inbox as well as the user's own audience. It returns the signed reply and the per-relay broadcast results. Requires a signer.
Example ¶
Publishing: attach a local-key signer and post an outbox-routed reply.
package main
import (
"context"
"fmt"
"github.com/0ceanslim/grain/client/core"
nostr "github.com/0ceanslim/grain/server/types"
)
func main() {
client := core.NewClient(core.DefaultConfig())
signer, err := core.NewEventSigner("64-char-hex-private-key")
if err != nil {
return
}
uc := client.NewUserContext(signer.PublicKey(), core.WithSigner(signer))
parent := &nostr.Event{ID: "parent-id", PubKey: "parent-author", Kind: 1}
reply, results, err := uc.Reply(context.Background(), parent, "well said!")
if err != nil {
return
}
fmt.Printf("published %s to %d relays\n", reply.ID, len(results))
}
Output:
func (*UserContext) Sign ¶
func (uc *UserContext) Sign(event *nostr.Event) error
Sign signs event as this user. It requires a signer and errors if the signer's public key does not match the context user, so a caller can't accidentally sign as someone else.
func (*UserContext) SignAndPublish ¶
func (uc *UserContext) SignAndPublish(ctx context.Context, event *nostr.Event) ([]BroadcastResult, error)
SignAndPublish signs event as this user and then publishes it.
func (*UserContext) Signer ¶
func (uc *UserContext) Signer() Signer
Signer returns the attached signer, or nil for a read-only context.
func (*UserContext) StreamNotes ¶
func (uc *UserContext) StreamNotes(ctx context.Context, author string, opts ...StreamOption) <-chan *nostr.Event
StreamNotes streams author's text notes (kind 1) from their outbox relays as each relay answers — the lazy-hydration path for a profile feed. Pass options like WithLimit to bound it. Routing honours the fixed-relay override.
type UserOption ¶
type UserOption func(*UserContext)
UserOption configures a UserContext at construction.
func WithSigner ¶
func WithSigner(s Signer) UserOption
WithSigner attaches a signer so the context can publish. Without one the context is read-only and UserContext.Sign / UserContext.SignAndPublish return an error.
type UserRelayLists ¶
type UserRelayLists struct {
NIP65 []RelayListEntry `json:"nip65"` // 10002
DM []string `json:"dm"` // 10050
Blocked []string `json:"blocked"` // 10006
Search []string `json:"search"` // 10007
Favorites []string `json:"favorites"` // 10012
Private []string `json:"private"` // 10013 (NIP-37) — usually empty; entries live in the encrypted content
// Encrypted flags NIP-51/37 lists whose event carried NIP-44/NIP-04 private
// content; only the public entries are listed in the fields above.
Encrypted EncryptedFlags `json:"encrypted"`
// EncryptedContent carries the raw private content blob per list so the
// browser — which holds the user's signer — can decrypt it on demand (#100).
// grain itself never decrypts; it just passes the opaque blob through.
EncryptedContent EncryptedContent `json:"encrypted_content"`
}
UserRelayLists is a user's resolved relay lists across the kinds the relay manager edits. NIP65 carries read/write flags; the rest are plain URL lists.
type UserRelays ¶
type UserRelays struct {
Outbox []string // NIP-65 write relays — publish / fetch their notes
Inbox []string // NIP-65 read relays — deliver replies / zaps here
DMInbox []string // NIP-17 kind 10050 DM relays
FetchedAt time.Time // when this was resolved
Negative bool // user has no published lists — cached briefly
}
UserRelays holds a target user's per-target event-derived relay roles, resolved from their published NIP-65 (kind 10002) and NIP-17 (kind 10050) events. These are the only roles the directory resolves for arbitrary users; the richer self-only roles (search / blocked / favorite / private) load into the logged-in user's own session config, not here.
func (*UserRelays) ForRole ¶
func (ur *UserRelays) ForRole(role Role) []string
ForRole returns the resolved per-target relays carrying the given role: RoleOutbox, RoleInbox, or RoleDMInbox. Any other role returns nil — the rest are self-only or locally configured, not part of a per-target resolution.
Source Files
¶
- approles.go
- client.go
- clienttag.go
- config.go
- directory.go
- eventBroadcaster.go
- eventBuilder.go
- eventSerializer.go
- eventSigner.go
- filters.go
- logger.go
- mailboxes.go
- media.go
- media_servers.go
- nip42.go
- nip44.go
- nip44_signer.go
- nip44_v3.go
- pool_lease.go
- profile.go
- query.go
- relayinfo.go
- relaylist.go
- relayliststore.go
- relayping.go
- relays.go
- relayurl.go
- role.go
- routing.go
- seed.go
- sessionrelays.go
- signer.go
- subscriptions.go
- usercontext.go