wavvon

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

Wavvon SDK for Go

Community / experimental. This SDK is maintained in-house only on a best-effort basis until an outside maintainer volunteers to own it. It still runs the shared conformance suite in CI, so every signed byte it produces is checked against the hub's own vectors, but expect its API to change and its releases to lag the TypeScript and Rust SDKs. Want to maintain it? Open an issue.

Write programs that talk to a Wavvon hub: bots, apps with slash commands and buttons, event consumers, mini-app hosts, and receivers for outgoing webhooks.

A program on Wavvon is a client like any other: an Ed25519 keypair, the same challenge-response login, the same permissions. There is no bot account. What makes a program an app is a role carrying apps.register. The design this module implements is sdk.md (tier 1).

  • Go 1.23 or later. context.Context on every call, errors as values.
  • Depends on github.com/coder/websocket, golang.org/x/crypto (HKDF) and github.com/oapi-codegen/runtime. Ed25519, HMAC and SHA-256 come from the standard library.
  • Apache-2.0, so a closed-source program can use it.

Install

go get github.com/Wavvon/Wavvon-sdk-go

Quick start

client, err := wavvon.NewClient(wavvon.ClientOptions{
	HubURL:     "https://hub.example",
	Seed:       seed,                        // 32-byte Ed25519 seed. Never sent anywhere.
	InviteCode: os.Getenv("WAVVON_INVITE"), // only needed the first time, on an invite-only hub
})
info, err := client.Info(ctx, false)

go http.ListenAndServe(":8080", &wavvon.DispatchHandler{
	HubPublicKey: *info.PublicKey,
	OnSlashCommand: func(ctx context.Context, inv *wavvon.SlashInvocation) (*wavvon.AppResponse, error) {
		return &wavvon.AppResponse{Reply: &wavvon.Reply{Body: "you said: " + inv.ArgsRaw}}, nil
	},
})

hook := "https://bot.example/hook"
err = client.SetAppProfile(ctx, wavvon.AppProfile{Name: "Echo", WebhookUrl: &hook})
err = client.SetAppCommands(ctx, []wavvon.AppCommand{{Name: "echo", Description: "Repeat after me"}})

events := client.Connect(wavvon.EventStreamOptions{}) // after SetAppProfile
defer events.Close()
for {
	ev, err := events.Next(ctx)
	if err != nil {
		break
	}
	switch e := ev.(type) {
	case *wavvon.MessageEvent:
		fmt.Println(*e.Message.Sender, *e.Message.Content)
	case *wavvon.UnknownEvent:
		fmt.Println("new frame type:", e.RawType)
	}
}

A fuller one is examples/echobot.

What is in it

Piece Entry point
Login (challenge, sign, verify) that renews itself NewClient, Client.Login, Authenticate, IdentityFromSeed
Every REST route, typed from openapi.yaml client.Rest.<OperationId>WithResponse, Check
Retry with backoff on 429, 5xx, timeouts built into client.Rest; DoWithRetry
Capability checks Client.HasCapability, Client.RequireCapability
Keyset paging to the end of a list Paginate (an iter.Seq2)
WebSocket events with resume after reconnect Client.Connect -> *EventStream, Next
HTTP polling instead of a socket Client.PollEvents, Client.AckEvents
App registration SetAppProfile, SetAppCommands, SetAppSubscriptions
Slash command and button webhooks DispatchHandler (an http.Handler), VerifyDispatch
Outgoing webhook receiver VerifyOutgoingWebhook
Mini-app host EventStream.AnnounceApp, DismissApp, SendMiniAppMessage

Errors are typed: *HubError (a definitive 4xx), *HubUnavailableError (the retry budget ran out; not an answer), *UnsupportedByHubError (also matches errors.Is(err, wavvon.ErrUnsupportedByHub)), *AuthSchemeError, *WebhookVerificationError. Use errors.As.

Capabilities each feature needs

Hubs say what they support in GET /info capabilities. The SDK checks those strings and never compares version numbers. A feature the hub does not advertise returns *UnsupportedByHubError.

Feature Capability
SetAppProfile, SetAppCommands, SetAppSubscriptions, embeds, game cards apps.register
VerifyDispatch / VerifyOutgoingWebhook (they require X-Wavvon-Signature-V1) webhooks.signedtimestamp
Paging GET /users, /conversations/{id}/messages, /admin/reports list.cursor
Paging bans, mutes, invites, pending, conversations, pins, polls list.cursor.lists
ping / pong over the socket ws.ping

Login, REST, the socket and polling need no capability.

Behaviour you can rely on

  • A transient failure is never an answer. 429, 5xx, timeouts and dropped connections are retried with capped exponential backoff and jitter. If the budget runs out you get *HubUnavailableError, not an empty list or a 404. Set RetryOptions.MaxAttempts negative to wait until the context ends.
  • Sessions renew themselves. A 401 triggers one fresh login and a retry. Client.OnStateChange and EventStream.OnStateChange show when it happens.
  • Unknown is delivered. A socket frame or dispatch type this version does not model reaches you as *UnknownEvent / *UnknownDispatch with the raw bytes.
  • Missed events are replayed. The event stream remembers the last hub_event seq and asks the hub to resume from it after every reconnect. If the gap is older than the hub keeps (72 hours) you get *ReplayUnavailable. Register the app profile before Connect: the hub only pushes and replays events on a socket opened by a registered app.
  • Polling is implemented, not relied on. Today's hub never writes the GET /me/events queue (Wavvon-server#170); use the socket.
  • The seed stays with you. No call sends a seed or private key anywhere.

Conformance

Every signed byte sequence is checked against the shared wire vectors in Wavvon-server (crates/identity/wire-vectors.json): the auth challenge, the Ed25519 dispatch signature, and the outgoing-webhook HKDF + HMAC. CI reads them from Wavvon-server at commit eb11d75657ee8362dd42a7982fc5b13a3dcc4e68.

The REST client was generated from Wavvon-docs at the commit in rest/SPEC_COMMIT.

Licence

Apache-2.0. See LICENSE and NOTICE.

Documentation

Overview

Package wavvon is the Go SDK for programs (bots and apps) that talk to a Wavvon hub: login that renews itself, the generated REST client, capability checks, keyset paging, the WebSocket event stream with resume, app registration, and verification of slash-command dispatches and outgoing webhooks. The design is sdk.md (tier 1) in Wavvon-docs.

Index

Constants

View Source
const AuthChallengeLen = 32

AuthChallengeLen is the only length a raw (untagged) challenge is signed at.

View Source
const AuthChallengeScheme = "tagged-v1"

AuthChallengeScheme is the only named challenge scheme this SDK signs.

View Source
const MaxPageSize = 500

MaxPageSize is what the hub clamps limit to; asking for more would look like a short page and end the walk early.

View Source
const Version = "0.1.0"

Version is this SDK's release. release.yml tags v<Version> when it reaches main, and the Go module proxy serves the module at that tag.

Variables

View Source
var ErrCursorIgnored = errors.New("wavvon: the hub returned the same page twice: it does not honour the cursor on this endpoint")

ErrCursorIgnored is returned by Paginate when the hub serves the same page twice: the endpoint does not honour the cursor, and looping would never end.

View Source
var ErrStreamClosed = errors.New("wavvon: event stream closed")

ErrStreamClosed is returned by EventStream.Next and Send after Close.

View Source
var ErrUnsupportedByHub = errors.New("wavvon: unsupported by this hub")

ErrUnsupportedByHub matches, via errors.Is, every UnsupportedByHubError.

Functions

func AuthChallengeSigningBytes

func AuthChallengeSigningBytes(challenge []byte, scheme *string) ([]byte, error)

AuthChallengeSigningBytes returns what a client signs to answer a challenge (wire-format.md, "Auth challenge answer"). scheme is nil when the hub's response has no scheme field. "tagged-v1" is the domain tag plus the challenge. No scheme (an older hub) is the raw challenge, and only at exactly 32 bytes, so this can never sign arbitrary bytes. Any other scheme, the empty string included, is refused rather than guessed at.

func Check

func Check[R interface{ StatusCode() int }](res R, err error) (R, error)

Check passes a generated REST call's result through, turning a non-2xx answer into *HubError carrying the hub's own words:

res, err := wavvon.Check(client.Rest.ListChannelsWithResponse(ctx))

func DecodeWebhookSecret

func DecodeWebhookSecret(shown string) ([]byte, error)

DecodeWebhookSecret decodes an outgoing-webhook secret as the admin was shown it (base64url, no padding) into its raw bytes.

func DoWithRetry

func DoWithRetry(ctx context.Context, opts RetryOptions, attempt func(context.Context) (*http.Response, error)) (*http.Response, error)

DoWithRetry runs attempt until it yields a response that is not transient. The hub sends no Retry-After, so the wait is capped exponential backoff with jitter. When the budget runs out it returns *HubUnavailableError rather than the last 429/5xx: a transient failure is never an answer.

Each attempt gets its own timeout context, which stays alive until the returned body is closed. attempt may make several requests (login does) and return whichever response decides the outcome.

func IsTransientStatus

func IsTransientStatus(status int) bool

IsTransientStatus reports whether a status means "not now": 429, or a 5xx from the hub or a proxy in front of it.

func OutgoingWebhookSigningKey

func OutgoingWebhookSigningKey(rawSecret []byte) []byte

OutgoingWebhookSigningKey derives the HMAC key from the secret's raw bytes: HKDF-SHA256, salt "wavvon-webhook-signing", empty info, 32 bytes. The HMAC is keyed with these bytes, not their hex.

func Paginate

func Paginate[T any](ctx context.Context, fetchPage func(ctx context.Context, cursor string, limit int) ([]T, error), key func(T) string, limit int) iter.Seq2[T, error]

Paginate walks a keyset-paged list to exhaustion. The hub's dialect is a bare array plus limit and cursor, where the cursor is the key of the previous page's last row (code for invites, public_key for users, id for most others), so key extracts it. A page shorter than limit is the end; one page is not the list.

fetchPage makes the request (cursor is "" for the first page) and must return an error on failure: an error never reads as an empty page, and it is yielded as the last pair. Check the endpoint's capability (list.cursor or list.cursor.lists) first; a hub without it ignores the cursor, which this detects and reports as ErrCursorIgnored rather than looping.

func WebhookDeliverySigningBytes

func WebhookDeliverySigningBytes(timestamp uint64, body []byte) []byte

WebhookDeliverySigningBytes is "wavvon/webhook-delivery/v1\0" || u64le(timestamp) || body.

Types

type AppClose

type AppClose struct {
	AppID     string `json:"app_id"`
	ChannelID string `json:"channel_id"`
}

AppClose ends a mini-app session.

func (*AppClose) EventType

func (*AppClose) EventType() string

type AppCommand

type AppCommand struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Args        string `json:"args,omitempty"`
	// Scope is "hub" (the default) or "channel".
	Scope      string `json:"scope,omitempty"`
	Privileged bool   `json:"privileged,omitempty"`
	// CooldownSeconds per user and command; nil means the hub default (3), 0 none.
	CooldownSeconds *int64 `json:"cooldown_seconds,omitempty"`
}

AppCommand is one slash command in PUT /me/app/commands.

type AppComponent

type AppComponent struct {
	// Type is "button" or "select".
	Type        string         `json:"type"`
	CustomID    string         `json:"custom_id"`
	Label       string         `json:"label,omitempty"`
	Style       string         `json:"style,omitempty"`
	Disabled    bool           `json:"disabled,omitempty"`
	Placeholder string         `json:"placeholder,omitempty"`
	MinValues   *int           `json:"min_values,omitempty"`
	MaxValues   *int           `json:"max_values,omitempty"`
	Options     []SelectOption `json:"options,omitempty"`
}

AppComponent is a button or select menu.

type AppLaunch

type AppLaunch struct {
	AppID       string `json:"app_id"`
	Title       string `json:"title"`
	Description string `json:"description"`
	ChannelID   string `json:"channel_id"`
}

AppLaunch is a mini-app launch card in a channel.

func (*AppLaunch) EventType

func (*AppLaunch) EventType() string

type AppOpen

type AppOpen struct {
	AppID          string `json:"app_id"`
	ChannelID      string `json:"channel_id"`
	MiniAppURL     string `json:"mini_app_url"`
	SessionToken   string `json:"session_token"`
	RequiresCamera bool   `json:"requires_camera"`
}

AppOpen tells a player's client to open a mini-app.

func (*AppOpen) EventType

func (*AppOpen) EventType() string

type AppProfile

AppProfile is the body of PUT /me/app/profile.

type AppResponse

type AppResponse struct {
	Reply *Reply `json:"reply,omitempty"`
	// Ephemeral shows the reply to the invoker only.
	Ephemeral bool       `json:"ephemeral,omitempty"`
	Reactions []Reaction `json:"reactions,omitempty"`
	// Defer says the app will post later on its own.
	Defer bool `json:"defer,omitempty"`
	// Game is attached to the reply; ignored without one.
	Game *GameLaunchCard `json:"game,omitempty"`
}

AppResponse is what a slash command handler answers.

type AppSubscription

type AppSubscription struct {
	Event    string   `json:"event"`
	Channels []string `json:"channels,omitempty"`
}

AppSubscription is one entry of PUT /me/app/subscriptions. A message.* subscription must name its channels.

type AuthOptions

type AuthOptions struct {
	// InviteCode admits the program on an invite_only hub. Ignored for members.
	InviteCode string
	Retry      RetryOptions
	// HTTPClient defaults to http.DefaultClient.
	HTTPClient *http.Client
}

AuthOptions configure Authenticate.

type AuthSchemeError

type AuthSchemeError struct {
	Reason string
}

AuthSchemeError means a challenge could not be answered safely: an unknown scheme, or a raw challenge of the wrong length.

func (*AuthSchemeError) Error

func (e *AuthSchemeError) Error() string

type AuthorInfo

type AuthorInfo struct {
	Pubkey      string `json:"pubkey"`
	DisplayName string `json:"display_name,omitempty"`
}

AuthorInfo names who invoked a command or pressed a component.

type Client

type Client struct {
	HubURL   string
	Identity *Identity
	// Rest is the client generated from openapi.yaml. Bearer, retry and
	// re-authentication on 401 are built into its transport; pass its
	// results through Check to turn non-2xx answers into errors.
	Rest *rest.ClientWithResponses
	// contains filtered or unexported fields
}

Client is a program's connection to one hub: a session that renews itself, the generated REST client, capability checks, app registration and the event stream. It is safe for concurrent use.

func NewClient

func NewClient(opts ClientOptions) (*Client, error)

NewClient builds a Client. It does not contact the hub.

func (*Client) AckEvents

func (c *Client) AckEvents(ctx context.Context, ids []string) error

AckEvents acknowledges polled events (DELETE /me/events) so they are not returned again.

func (*Client) CanonicalPubkey

func (c *Client) CanonicalPubkey() string

CanonicalPubkey is the identity the hub attributes this program's actions to, or "" before the first login.

func (*Client) Connect

func (c *Client) Connect(opts EventStreamOptions) *EventStream

Connect opens the hub WebSocket and keeps it open, re-authenticating and resuming on its own, until Close. Call SetAppProfile first: the hub only pushes and replays hub events on a socket opened by a registered app.

func (*Client) HasCapability

func (c *Client) HasCapability(ctx context.Context, name string) (bool, error)

HasCapability reports whether the hub advertises name. Capabilities, never versions.

func (*Client) Info

func (c *Client) Info(ctx context.Context, refresh bool) (*rest.HubInfo, error)

Info returns GET /info, cached after the first call; refresh re-reads it.

func (*Client) Login

func (c *Client) Login(ctx context.Context) (Session, error)

Login returns the current session, logging in if there is none.

func (*Client) OnStateChange

func (c *Client) OnStateChange(fn func(SessionState)) (unsubscribe func())

OnStateChange calls fn on every session state change and returns a function that stops it. fn runs on the goroutine that caused the change.

func (*Client) PollEvents

func (c *Client) PollEvents(ctx context.Context, since int64) ([]PolledEvent, error)

PollEvents reads GET /me/events, the transport for a program that holds no socket; since (unix seconds) returns only rows created after it, 0 all. Today's hub never writes this queue (Wavvon-server#170): prefer Connect.

func (*Client) RequireCapability

func (c *Client) RequireCapability(ctx context.Context, name string) error

RequireCapability returns *UnsupportedByHubError unless the hub advertises name.

func (*Client) SetAppCommands

func (c *Client) SetAppCommands(ctx context.Context, commands []AppCommand) error

SetAppCommands replaces this app's slash commands.

func (*Client) SetAppProfile

func (c *Client) SetAppProfile(ctx context.Context, profile AppProfile) error

SetAppProfile registers or replaces this identity's app profile. Call it before Connect: the hub decides whether a socket belongs to an app when it connects, and only an app's socket gets hub_event push and resume.

func (*Client) SetAppSubscriptions

func (c *Client) SetAppSubscriptions(ctx context.Context, subscriptions []AppSubscription) error

SetAppSubscriptions replaces this app's event subscriptions.

func (*Client) State

func (c *Client) State() SessionState

State is the current session state.

type ClientOptions

type ClientOptions struct {
	HubURL string
	// Seed is a 32-byte Ed25519 seed. It is never sent anywhere.
	Seed []byte
	// Identity is used instead of Seed when set.
	Identity *Identity
	// InviteCode admits the program on an invite_only hub.
	InviteCode string
	Retry      RetryOptions
	// HTTPClient defaults to http.DefaultClient.
	HTTPClient *http.Client
}

ClientOptions configure NewClient. Seed or Identity is required.

type ComponentInteraction

type ComponentInteraction struct {
	HubURL    string     `json:"hub_url"`
	ChannelID string     `json:"channel_id"`
	MessageID string     `json:"message_id"`
	CustomID  string     `json:"custom_id"`
	Values    []string   `json:"values"`
	User      AuthorInfo `json:"user"`
}

ComponentInteraction is a button press or select choice on an app's message.

func (*ComponentInteraction) DispatchType

func (*ComponentInteraction) DispatchType() string

type ComponentResponse

type ComponentResponse struct {
	Update         *ComponentUpdate `json:"update,omitempty"`
	EphemeralReply *EphemeralReply  `json:"ephemeral_reply,omitempty"`
	Defer          bool             `json:"defer,omitempty"`
}

ComponentResponse is what a component interaction handler answers.

type ComponentRow

type ComponentRow struct {
	Type       string         `json:"type"`
	Components []AppComponent `json:"components"`
}

ComponentRow is a row of components; Type is always "row".

func Row

func Row(components ...AppComponent) ComponentRow

Row builds a ComponentRow.

type ComponentUpdate

type ComponentUpdate struct {
	Body       *string        `json:"body,omitempty"`
	Components []ComponentRow `json:"components,omitempty"`
}

ComponentUpdate replaces the interacted message's body or components.

type ConnectionState

type ConnectionState string

ConnectionState is where an EventStream's socket is.

const (
	ConnConnecting   ConnectionState = "connecting"
	ConnOpen         ConnectionState = "open"
	ConnReconnecting ConnectionState = "reconnecting"
	ConnClosed       ConnectionState = "closed"
)

Connection states.

type DMEvent

type DMEvent struct {
	ConversationID string  `json:"conversation_id"`
	Sender         string  `json:"sender"`
	SenderName     *string `json:"sender_name"`
	Content        string  `json:"content"`
	Timestamp      int64   `json:"timestamp"`
}

DMEvent is a direct message notification.

func (*DMEvent) EventType

func (*DMEvent) EventType() string

type Dispatch

type Dispatch interface {
	DispatchType() string
}

Dispatch is a verified webhook call from the hub: *SlashInvocation, *ComponentInteraction, or *UnknownDispatch for a type this SDK does not know.

func VerifyDispatch

func VerifyDispatch(h http.Header, body []byte, hubPublicKey string, opts VerifyOptions) (Dispatch, error)

VerifyDispatch verifies a slash command or component dispatch and decodes it. hubPublicKey is the hub's key as the program pinned it (GET /info public_key); the X-Wavvon-Hub-Pubkey header is only compared against it, never trusted. Pass the raw request bytes: re-serialised JSON will not verify.

type DispatchHandler

type DispatchHandler struct {
	// HubPublicKey is the hub's key, hex, from GET /info.
	HubPublicKey   string
	OnSlashCommand func(context.Context, *SlashInvocation) (*AppResponse, error)
	OnComponent    func(context.Context, *ComponentInteraction) (*ComponentResponse, error)
	// OnUnknown handles a verified dispatch of a type this SDK does not know.
	OnUnknown func(context.Context, *UnknownDispatch) (any, error)
	Verify    VerifyOptions
}

DispatchHandler is an http.Handler for an app's webhook_url. It answers anything that does not verify with 401 and a verified dispatch with the handler's JSON. The hub waits 5 s for the answer. A nil handler answers {}.

func (*DispatchHandler) ServeHTTP

func (d *DispatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type Embed

type Embed struct {
	Title        string       `json:"title,omitempty"`
	URL          string       `json:"url,omitempty"`
	Description  string       `json:"description,omitempty"`
	Color        string       `json:"color,omitempty"`
	Fields       []EmbedField `json:"fields,omitempty"`
	ThumbnailURL string       `json:"thumbnail_url,omitempty"`
	ImageURL     string       `json:"image_url,omitempty"`
	Footer       *EmbedFooter `json:"footer,omitempty"`
}

Embed is a rich embed on an app's message.

type EmbedField

type EmbedField struct {
	Name   string `json:"name"`
	Value  string `json:"value"`
	Inline bool   `json:"inline,omitempty"`
}

EmbedField is one name/value pair of an Embed.

type EmbedFooter

type EmbedFooter struct {
	Text string `json:"text"`
}

EmbedFooter is an Embed's footer line.

type EphemeralReply

type EphemeralReply struct {
	Body string `json:"body"`
}

EphemeralReply is a reply only the interacting user sees.

type ErrorEvent

type ErrorEvent struct {
	Context string `json:"context"`
	Message string `json:"message"`
}

ErrorEvent reports a failed client frame.

func (*ErrorEvent) EventType

func (*ErrorEvent) EventType() string

type Event

type Event interface {
	// EventType is the frame's "type" field ("binary" for a binary frame).
	EventType() string
}

Event is one server-to-client WebSocket frame (ws-protocol.md, checked against Wavvon-server crates/hub/src/routes/chat_models.rs). Type-switch on the concrete types below; a frame this version does not model arrives as *UnknownEvent, never dropped.

func ParseFrame

func ParseFrame(data []byte) Event

ParseFrame turns one text frame into a typed event. A frame of an unknown type, or one whose fields do not decode, is an *UnknownEvent.

type EventStream

type EventStream struct {
	// contains filtered or unexported fields
}

EventStream is one hub WebSocket that reconnects on its own. It tracks the last hub_event seq and sends resume after every reconnect, so an app misses nothing inside the hub's 72-hour window; ReplayUnavailable is delivered like any other event when it falls outside. Events a replay repeats (seq not above the last one seen) are dropped.

func (*EventStream) AnnounceApp

func (s *EventStream) AnnounceApp(ctx context.Context, channelID, title, description string) error

AnnounceApp is the mini-app host showing a launch card in a channel. Players' app_join is answered by the hub, not by the host.

func (*EventStream) Close

func (s *EventStream) Close()

Close stops the stream and waits for its goroutine. Next then returns ErrStreamClosed once buffered events are drained.

func (*EventStream) DismissApp

func (s *EventStream) DismissApp(ctx context.Context, channelID string) error

DismissApp is the mini-app host closing the session; clients dismiss their webviews.

func (*EventStream) LastSeq

func (s *EventStream) LastSeq() (seq int64, ok bool)

LastSeq is the highest hub_event seq seen (or the hub's live seq at first connect); persist it and pass it as SinceSeq to resume after a restart.

func (*EventStream) Next

func (s *EventStream) Next(ctx context.Context) (Event, error)

Next blocks until the next event, ctx ends, or the stream is closed (ErrStreamClosed).

func (*EventStream) OnStateChange

func (s *EventStream) OnStateChange(fn func(ConnectionState)) (unsubscribe func())

OnStateChange calls fn on every state change and returns a function that stops it. fn runs on the stream's goroutine; keep it short.

func (*EventStream) Send

func (s *EventStream) Send(ctx context.Context, frame any) error

Send writes one client frame (any JSON-encodable value with a "type"). It fails unless the socket is open.

func (*EventStream) SendMiniAppMessage

func (s *EventStream) SendMiniAppMessage(ctx context.Context, appID, channelID, toPubkey, payload string) error

SendMiniAppMessage relays an opaque payload to one joined player's mini-app. Players' messages arrive as *MiniAppMessage with FromPubkey.

func (*EventStream) State

func (s *EventStream) State() ConnectionState

State is the socket's current state.

func (*EventStream) Subscribe

func (s *EventStream) Subscribe(ctx context.Context, channelID string) error

Subscribe asks for a channel's live frames (messages, typing).

type EventStreamOptions

type EventStreamOptions struct {
	// SinceSeq replays hub events after this seq on the first connect (an app
	// persisting its position across restarts). Nil starts from now.
	SinceSeq *int64
	// Buffer is how many events wait for Next before reading pauses. Default 256.
	Buffer int
}

EventStreamOptions configure Connect.

type GameLaunchCard

type GameLaunchCard struct {
	EntryURL     string `json:"entry_url"`
	Name         string `json:"name"`
	Description  string `json:"description,omitempty"`
	ThumbnailURL string `json:"thumbnail_url,omitempty"`
}

GameLaunchCard is a "Play" card attached to a reply.

type Hello

type Hello struct {
	LiveSeq int64 `json:"live_seq"`
}

Hello is the first frame on every connection.

func (*Hello) EventType

func (*Hello) EventType() string

type HubError

type HubError struct {
	Status int
	// Body is the hub's own words, usually plain text.
	Body string
}

HubError is a definitive refusal from the hub: a 4xx other than 429.

func (*HubError) Error

func (e *HubError) Error() string

type HubEvent

type HubEvent struct {
	Seq int64 `json:"seq"`
	// Event is e.g. "member.joined", "message.created".
	Event   string         `json:"event"`
	HubURL  string         `json:"hub_url"`
	At      int64          `json:"at"`
	Payload map[string]any `json:"payload"`
	// Replay only, and only when the audit row has them.
	ActorPubkey  string `json:"actor_pubkey,omitempty"`
	TargetPubkey string `json:"target_pubkey,omitempty"`
	ChannelID    string `json:"channel_id,omitempty"`
	Replayed     bool   `json:"replayed,omitempty"`
}

HubEvent is the one envelope for live push, replay and outgoing webhooks (apps.md §5).

func (*HubEvent) EventType

func (*HubEvent) EventType() string

type HubUnavailableError

type HubUnavailableError struct {
	// LastStatus is the last HTTP status seen, or 0 for a network error or timeout.
	LastStatus int
	Attempts   int
	// Err is the last network error, if the last attempt failed with one.
	Err error
}

HubUnavailableError means the hub could not be reached or kept saying "not now" (429, 5xx, timeout, network) until the retry budget ran out. It is not an answer: nothing about the resource can be concluded from it.

func (*HubUnavailableError) Error

func (e *HubUnavailableError) Error() string

func (*HubUnavailableError) Unwrap

func (e *HubUnavailableError) Unwrap() error

type Identity

type Identity struct {
	// PublicKey is the Ed25519 public key, lowercase hex.
	PublicKey string
	// contains filtered or unexported fields
}

Identity is a keypair the program holds. The private key never leaves it: only signatures do.

func IdentityFromHexSeed

func IdentityFromHexSeed(seed string) (*Identity, error)

IdentityFromHexSeed is IdentityFromSeed for a seed written as 64 hex characters.

func IdentityFromSeed

func IdentityFromSeed(seed []byte) (*Identity, error)

IdentityFromSeed builds an Identity from a 32-byte Ed25519 seed that the operator keeps in a file or environment variable.

func (*Identity) Sign

func (id *Identity) Sign(message []byte) []byte

Sign signs message with the identity's key.

type MessageEvent

type MessageEvent struct {
	Type      string       `json:"type"`
	ChannelID string       `json:"channel_id"`
	Message   rest.Message `json:"message"`
}

MessageEvent is a new ("message") or edited ("message_edited") channel message.

func (*MessageEvent) EventType

func (e *MessageEvent) EventType() string

type MessageRefEvent

type MessageRefEvent struct {
	Type      string `json:"type"`
	ChannelID string `json:"channel_id"`
	MessageID string `json:"message_id"`
}

MessageRefEvent is "message_deleted", "message_pinned" or "message_unpinned".

func (*MessageRefEvent) EventType

func (e *MessageRefEvent) EventType() string

type MiniAppMessage

type MiniAppMessage struct {
	AppID      string `json:"app_id"`
	ChannelID  string `json:"channel_id"`
	Payload    string `json:"payload"`
	FromPubkey string `json:"from_pubkey,omitempty"`
}

MiniAppMessage is an opaque payload relayed between a mini-app host and a player; FromPubkey names the player on the host side.

func (*MiniAppMessage) EventType

func (*MiniAppMessage) EventType() string

type OutgoingWebhookEvent

type OutgoingWebhookEvent struct {
	Type string `json:"type"`
	HubEvent
	WebhookID string `json:"webhook_id,omitempty"`
	Truncated bool   `json:"truncated,omitempty"`
}

OutgoingWebhookEvent is an outgoing-webhook delivery: a hub_event plus delivery fields.

func VerifyOutgoingWebhook

func VerifyOutgoingWebhook(h http.Header, body []byte, secret string, opts VerifyOptions) (*OutgoingWebhookEvent, error)

VerifyOutgoingWebhook verifies an outgoing-webhook delivery (HMAC-SHA256) and decodes its envelope. secret is what the admin copied when the webhook was created or rotated (base64url, no padding).

type PolledEvent

type PolledEvent struct {
	ID        string
	EventType string
	// Payload is the event's JSON. A payload that is not JSON arrives as a JSON string.
	Payload   json.RawMessage
	CreatedAt int64
}

PolledEvent is one row of GET /me/events.

type Pong

type Pong struct {
	Nonce           int64    `json:"nonce"`
	OutboundLossPct *float64 `json:"outbound_loss_pct,omitempty"`
}

Pong answers a ping (capability ws.ping).

func (*Pong) EventType

func (*Pong) EventType() string

type Reaction

type Reaction struct {
	MessageID string `json:"message_id"`
	Emoji     string `json:"emoji"`
}

Reaction is an emoji the app adds to a message.

type ReactionsUpdated

type ReactionsUpdated struct {
	ChannelID string `json:"channel_id"`
	MessageID string `json:"message_id"`
	Reactions []struct {
		Emoji string `json:"emoji"`
		Count int    `json:"count"`
		Me    bool   `json:"me"`
	} `json:"reactions"`
}

ReactionsUpdated carries a message's full reaction list.

func (*ReactionsUpdated) EventType

func (*ReactionsUpdated) EventType() string

type ReplayComplete

type ReplayComplete struct {
	Replayed    int64 `json:"replayed"`
	LiveFromSeq int64 `json:"live_from_seq"`
}

ReplayComplete ends a resume replay.

func (*ReplayComplete) EventType

func (*ReplayComplete) EventType() string

type ReplayUnavailable

type ReplayUnavailable struct {
	EarliestSeq int64 `json:"earliest_seq"`
	EarliestAt  int64 `json:"earliest_at"`
}

ReplayUnavailable means the resume point is older than the hub keeps (72 hours): events between it and EarliestSeq are lost to this program.

func (*ReplayUnavailable) EventType

func (*ReplayUnavailable) EventType() string

type Reply

type Reply struct {
	Body       string         `json:"body"`
	Embeds     []Embed        `json:"embeds,omitempty"`
	Components []ComponentRow `json:"components,omitempty"`
	ReplyTo    string         `json:"reply_to,omitempty"`
}

Reply is the message an app posts in answer to a command.

type RetryInfo

type RetryInfo struct {
	Attempt int
	Delay   time.Duration
	// Status is the HTTP status, or 0 for a network error or timeout.
	Status int
	Err    error
}

RetryInfo describes one failed attempt about to be retried.

type RetryOptions

type RetryOptions struct {
	// MaxAttempts before giving up with HubUnavailableError. 0 means 8; a
	// negative value keeps trying until the context ends.
	MaxAttempts int
	// BaseDelay is the first backoff ceiling. Default 500 ms.
	BaseDelay time.Duration
	// MaxDelay caps the backoff ceiling. Default 30 s.
	MaxDelay time.Duration
	// Timeout per attempt; a timeout counts as transient. Default 15 s.
	Timeout time.Duration
	// OnRetry is called before each wait, so a program can show it is retrying.
	OnRetry func(RetryInfo)
	// contains filtered or unexported fields
}

RetryOptions tune the backoff used for every request. The zero value is the default policy.

func (RetryOptions) Backoff

func (o RetryOptions) Backoff(attempt int) time.Duration

Backoff is the full-jitter delay before retry number attempt (1-based): uniform in [0, min(MaxDelay, BaseDelay * 2^(attempt-1))].

type SelectOption

type SelectOption struct {
	Label       string `json:"label"`
	Value       string `json:"value"`
	Description string `json:"description,omitempty"`
}

SelectOption is one choice of a select AppComponent.

type Session

type Session struct {
	Token string
	// Scope is "member", or "lobby" on a hub that holds newcomers back.
	Scope string
	// CanonicalPubkey is the identity the hub attributes this session's actions to.
	CanonicalPubkey string
}

Session is a logged-in session on one hub.

func Authenticate

func Authenticate(ctx context.Context, hubURL string, id *Identity, opts AuthOptions) (Session, error)

Authenticate runs challenge, sign, verify against hubURL. A transient failure at either step restarts from a fresh challenge, because the hub drops a pending challenge as soon as verify reads it.

type SessionState

type SessionState string

SessionState is where the client's REST session is.

const (
	StateSignedOut      SessionState = "signed-out"
	StateAuthenticating SessionState = "authenticating"
	StateReady          SessionState = "ready"
)

Session states. StateAuthenticating after StateReady is the reconnecting state of REST: a session died and is being re-established.

type SlashInvocation

type SlashInvocation struct {
	HubURL        string     `json:"hub_url"`
	ChannelID     string     `json:"channel_id"`
	MessageIDHint string     `json:"message_id_hint"`
	Author        AuthorInfo `json:"author"`
	Command       string     `json:"command"`
	ArgsRaw       string     `json:"args_raw"`
	ArgsTokens    []string   `json:"args_tokens"`
}

SlashInvocation is a slash command a member ran.

func (*SlashInvocation) DispatchType

func (*SlashInvocation) DispatchType() string

type TypingEvent

type TypingEvent struct {
	ChannelID   string  `json:"channel_id"`
	PublicKey   string  `json:"public_key"`
	DisplayName *string `json:"display_name"`
	Typing      bool    `json:"typing"`
}

TypingEvent says someone started or stopped typing in a channel.

func (*TypingEvent) EventType

func (*TypingEvent) EventType() string

type UnknownDispatch

type UnknownDispatch struct {
	RawType string
	Raw     json.RawMessage
}

UnknownDispatch is a verified dispatch whose type this SDK does not know. Delivered, never dropped.

func (*UnknownDispatch) DispatchType

func (u *UnknownDispatch) DispatchType() string

type UnknownEvent

type UnknownEvent struct {
	// RawType is the frame's own type, "binary" for a binary frame, "" if unparseable.
	RawType string
	Raw     []byte
}

UnknownEvent is a frame of a type this SDK does not model, or one that did not decode. Never swallowed.

func (*UnknownEvent) EventType

func (e *UnknownEvent) EventType() string

type UnsupportedByHubError

type UnsupportedByHubError struct {
	Capability string
}

UnsupportedByHubError means the hub does not advertise a capability the call needs. It is never the result of a version comparison.

func (*UnsupportedByHubError) Error

func (e *UnsupportedByHubError) Error() string

func (*UnsupportedByHubError) Unwrap

func (e *UnsupportedByHubError) Unwrap() error

Unwrap makes errors.Is(err, ErrUnsupportedByHub) true.

type VerifyOptions

type VerifyOptions struct {
	// Now defaults to the clock.
	Now time.Time
	// Tolerance is the maximum |now - X-Wavvon-Timestamp|. Default 300 s.
	Tolerance time.Duration
}

VerifyOptions tune timestamp checking.

type WebhookVerificationError

type WebhookVerificationError struct {
	Reason string
}

WebhookVerificationError means a webhook request failed verification. Answer it with 401 and act on nothing in it.

func (*WebhookVerificationError) Error

func (e *WebhookVerificationError) Error() string

Directories

Path Synopsis
cmd
smoke command
Command smoke runs the SDK against a real hub: it authenticates as a fresh identity, registers a slash command, posts a message and sees it come back over the socket, invokes the command, and checks that the verified invocation's reply lands in the channel.
Command smoke runs the SDK against a real hub: it authenticates as a fresh identity, registers a slash command, posts a message and sees it come back over the socket, invokes the command, and checks that the verified invocation's reply lands in the channel.
examples
echobot command
Command echobot echoes every message in the channels it can read and answers /echo.
Command echobot echoes every message in the channels it can read and answers /echo.
Package rest provides primitives to interact with the openapi HTTP API.
Package rest provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL