cosmo

package
v0.0.0-...-cb8056f Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: GPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package cosmo is the API client for api.cosmo.fans: auth/token lifecycle, login, and the room/live/talk feature endpoints. It talks plain Bearer-JWT HTTP (plus one AES-encrypted body on login/refresh) and needs no device, proxy, or the app itself.

Index

Constants

View Source
const (
	// AbstractChainID is Abstract mainnet's chain id (0xab5).
	AbstractChainID = 2741

	// AbstractPaymaster is Cosmo's gas-sponsoring paymaster contract on
	// Abstract. The address is constant; GasStationAbstract returns only the
	// signed, per-request paymasterInput (which carries a fresh deadline).
	AbstractPaymaster = "0xac54d831fd7f32737191515eec0a16f35013f8e5"
)
View Source
const (
	GravityEvent = "event-gravity"
	GravityGrand = "grand-gravity"

	PollSingle      = "single-poll"
	PollCombination = "combination-poll"
)

Gravity types and poll types. A gravity is either a standalone event or a multi-day "grand" (battle-royale) series whose days are separate gravity records; its polls are single-choice or the legacy combination form. The combination form only appears on old Polygon-era gravities.

View Source
const (
	ObjektSortNewest = "newest"
	ObjektSortOldest = "oldest"
)

ObjektSortNewest and ObjektSortOldest are the only accepted order values.

View Source
const AllMembersID = 0

AllMembersID is the VodsByMember bucket holding the group's whole replay history, each clip once; no member has APIID 0.

View Source
const CodeInvalidArtist = "CHANNEL_MESSAGE_INVALID_ARTIST"

CodeInvalidArtist is the API error code the channels endpoints return for artists that have no Talk feature (currently everyone but tripleS).

View Source
const MaxFavoritedObjekts = 9

MaxFavoritedObjekts is the server's cap on pinned collections. Going over it is not an error: the API answers 201 and silently unpins whichever collection has the oldest favoritedAt, with nothing in the response to say so. The app warns before evicting, so callers enforce the cap themselves rather than let a pin disappear.

View Source
const TransferSingleTopic = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"

TransferSingleTopic is the ERC-1155 TransferSingle(address,address,address, uint256,uint256) event signature. COMO is an ERC-1155 token, so a gravity vote logs this on the COMO contract with four topics (signature, operator, from, to) and the id/amount in the data.

View Source
const TransferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

TransferTopic is the ERC-721/ERC-20 Transfer(address,address,uint256) event signature — keccak-256 of the canonical event string. An ERC-721 transfer logs it on the token contract with four topics (signature, from, to, tokenId); the three-topic version on the L2 base-token contract is the gas fee movement.

View Source
const (

	// UserAgent: some Cosmo endpoints reject the default Go UA, so we pose as
	// the app's OkHttp stack. Exported so media downloaders (internal/hls) send
	// the same UA when pulling from the CDN.
	UserAgent = "okhttp/4.12.0"
)

Variables

View Source
var ErrNeedsLogin = errors.New("no valid credentials; log in again")

ErrNeedsLogin means we have no usable refresh token; the user must log in.

View Source
var ErrNoMembership = errors.New("no membership for this member")

ErrNoMembership means the member's live clip is gated behind a membership the account doesn't hold (the API returns a null videoUrl).

Functions

func Basename

func Basename(rawURL string) string

Basename returns the filename portion of a media URL (used to name and dedupe downloads).

func ErrorCode

func ErrorCode(err error) string

ErrorCode returns the API error code carried by err, or "" when err is not an APIError or its body carried no code.

func ExtractCredentials

func ExtractCredentials(node any) (config.Credentials, error)

ExtractCredentials pulls an accessToken/refreshToken pair out of an API response. Cosmo nests them under a "credentials" object, so we search recursively and accept snake_case (mirrors lib/auth.py extract_credentials).

func LocalDate

func LocalDate(iso string) string

LocalDate renders the YYYY-MM-DD of an ISO-8601 timestamp in the system's local timezone (the API sends UTC or +09:00 instants). Falls back to the raw date prefix if the string doesn't parse.

func LocalDateTime

func LocalDateTime(iso string) string

LocalDateTime renders "YYYY-MM-DD HH:MM" of an ISO-8601 instant in the system's local timezone. Falls back to the raw string if it doesn't parse.

func LoginByPrivy

func LoginByPrivy(ctx context.Context, privyToken string) (config.Credentials, error)

LoginByPrivy exchanges a Privy access token for Cosmo account credentials.

func SendCode

func SendCode(ctx context.Context, email string) error

SendCode asks Privy to email a one-time login code to the address.

Types

type APIError

type APIError struct {
	Status  int
	Code    string
	Message string
	Body    string
}

APIError describes a non-2xx API response. Code and Message are parsed from the API's {"error":{"code","message"}} JSON body when present (e.g. CHANNEL_MESSAGE_INVALID_ARTIST); Body always keeps the raw text.

func (*APIError) Error

func (e *APIError) Error() string

Error is a single line suitable for a status bar: the parsed API message (or code) when the body was the standard JSON shape, otherwise just the status. Gateway errors (502/504) carry multi-line HTML bodies, so the raw Body is never included; it stays on the struct for debugging.

type AbstractGas

type AbstractGas struct {
	Standard       int64  `json:"standard"`
	PaymasterInput string `json:"paymasterInput"`
}

AbstractGas is GET /gas-station/abstract: the sponsored-gas parameters for one send. Standard is the maxFeePerGas to use (wei); PaymasterInput is the signed 0x-hex blob to place in the transaction's paymaster params.

type AbstractLog

type AbstractLog struct {
	Address string   // the contract that emitted it
	Topics  []string // topic[0] is the event signature
}

AbstractLog is one event a transaction emitted.

type AbstractReceipt

type AbstractReceipt struct {
	Status AbstractTxStatus
	Logs   []AbstractLog
}

AbstractReceipt is a broadcast transaction's outcome. Logs are what the transaction actually did, and are the only trustworthy proof that an objekt changed hands: a state read (ownerOf) lags the receipt through Cosmo's RPC proxy by long enough to report a completed transfer as not having happened.

func (AbstractReceipt) MovedComo

func (r AbstractReceipt) MovedComo(comoAddress, recipient string) bool

MovedComo reports whether the receipt records COMO leaving comoAddress for recipient - the ERC-1155 TransferSingle a gravity vote is supposed to cause. As with MovedToken, this is the trustworthy proof that a vote landed: a transaction can mine successfully having moved nothing, and Cosmo's own /gravities/{id}/status stays at zero until the poll is revealed, so it cannot be used to confirm a vote either.

func (AbstractReceipt) MovedToken

func (r AbstractReceipt) MovedToken(tokenAddress string, tokenID int64, recipient string) bool

MovedToken reports whether the receipt records tokenID leaving the token contract for recipient — the ERC-721 Transfer event the transfer was supposed to cause. A transaction can be mined successfully having done nothing at all (transferFrom to an address with no matching code does not revert), and this is what tells the two apart.

type AbstractTxStatus

type AbstractTxStatus int

AbstractTxStatus is what the chain knows about a broadcast transaction.

const (
	TxPending  AbstractTxStatus = iota // accepted, not yet mined (no receipt)
	TxSuccess                          // mined, receipt status 0x1
	TxReverted                         // mined, receipt status 0x0
)

type Artist

type Artist struct {
	ID          string             `json:"id"`
	Name        string             `json:"name"`
	Title       string             `json:"title"`
	FandomName  string             `json:"fandomName"`
	ComoTokenID int64              `json:"comoTokenId"`
	Members     []ArtistMember     `json:"artistMembers"`
	SNSLink     map[string]SNSLink `json:"snsLink"`
}

Artist is a group's public card. Title is the display casing ("ARTMS"), while ID/Name are the API identifier ("artms"). ComoTokenID is the artist's COMO id within the shared ERC-1155 COMO contract on Abstract (tripleS 1, artms 2, idntt 3). Gravity votes transfer that id, so it must come from here rather than be assumed: spending id 1 on an artms gravity would move the wrong artist's COMO.

type ArtistMember

type ArtistMember struct {
	ID       int    `json:"id"`
	Name     string `json:"name"`
	Alias    string `json:"alias"`
	Units    string `json:"units"`
	Order    int    `json:"order"`
	ColorHex string `json:"primaryColorHex"`
}

ArtistMember is one roster entry. Alias is the short codename ("S1", "id9"); for artists without codenames it equals Name. Units is comma-separated and may be empty.

type Author

type Author struct {
	ID       int    `json:"id"`
	Nickname string `json:"nickname"`
}

Author is the member who wrote a room post. nickname maps to a member name.

type BodyBlock

type BodyBlock struct {
	Type              string
	Text              string
	Align             string
	ImageURL          string
	VideoURL          string
	ThumbnailImageURL string
	Height            float64
}

BodyBlock is one block of a gravity's rich description. Type is one of heading, text, image, video, or spacing; the relevant fields are set per type (Text/Align for heading|text, ImageURL for image, VideoURL/ThumbnailImageURL for video, Height for spacing and media sizing).

type Channel

type Channel struct {
	MemberID        int
	MemberName      string
	OriginalName    string
	ProfileImageURL string
	IsConnected     bool
	UnreadCount     int
	LastMessage     string
	LastMessageAt   string
	// LastMessageType is the newest message's Type. Attachment messages carry
	// no content, so it is the only thing that tells a channel whose newest
	// message is a photo apart from one with no messages at all.
	LastMessageType string
}

Channel is a member's Talk channel as it appears in the list. MemberName is the artist-set nickname when one is set; OriginalName is always the member's real name (empty on API versions that predate nicknames).

type Choice

type Choice struct {
	ID          string
	Title       string
	Description string
	ImageURL    string
}

Choice is one candidate in a poll.

type Client

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

Client is an authenticated Cosmo API client. It owns the token lifecycle: every request refreshes the access token first if it is stale, persisting the new tokens via the save callback.

func New

func New(creds config.Credentials, save func(config.Credentials) error) *Client

New builds a client from stored credentials. save is called whenever tokens are refreshed (typically config.Save); it may be nil to skip persistence.

func (*Client) AbstractEstimateGas

func (c *Client) AbstractEstimateGas(ctx context.Context, from, to, data string) (uint64, error)

AbstractEstimateGas estimates the gas limit for a call (eth_estimateGas). data is 0x-hex calldata.

func (*Client) AbstractNonce

func (c *Client) AbstractNonce(ctx context.Context, addr string) (uint64, error)

AbstractNonce returns the next transaction nonce for addr (eth_getTransactionCount at the latest block).

func (*Client) AbstractOwnerOf

func (c *Client) AbstractOwnerOf(ctx context.Context, tokenAddress string, tokenID int64) (string, error)

AbstractOwnerOf asks a token contract who holds a tokenId (ERC-721 ownerOf). An address that does not answer — no such function, or no token — reports an empty string rather than an error, so a caller probing several candidate contracts can simply take the one that answers.

This is not a formality: Cosmo has been observed reporting a tokenAddress that does not hold the copy at all (a Cream02 Unit objekt whose token really lives on the main objekt contract). Calling transferFrom on such an address does not revert — there is no matching code to revert — so the transaction is mined successfully having moved nothing. Asking the chain who owns the token is the only reliable check.

func (*Client) AbstractRPC

func (c *Client) AbstractRPC(ctx context.Context, method string, params ...any) (json.RawMessage, error)

AbstractRPC makes one JSON-RPC call against Cosmo's Abstract proxy and returns the raw result. A JSON-RPC-level error (200 with an {error} body) is surfaced as a Go error.

func (*Client) AbstractSendRawTx

func (c *Client) AbstractSendRawTx(ctx context.Context, rawHex string) (string, error)

AbstractSendRawTx broadcasts a signed raw transaction (0x-hex) and returns its hash (eth_sendRawTransaction).

func (*Client) AbstractTxState

func (c *Client) AbstractTxState(ctx context.Context, hash string) (AbstractReceipt, error)

AbstractTxState fetches a transaction's receipt (eth_getTransactionReceipt). A transaction still in the mempool has no receipt at all, which is TxPending rather than an error.

func (*Client) AllObjektCollections

func (c *Client) AllObjektCollections(ctx context.Context, artist, order string) (ObjektCollections, error)

AllObjektCollections fetches a user's complete owned collection for an artist, paging on objektPageSize until CollectionCount entries are gathered (or a short page ends the run). order must be ObjektSortNewest or ObjektSortOldest.

func (*Client) AllPastGravities

func (c *Client) AllPastGravities(ctx context.Context, artist string) ([]Gravity, error)

AllPastGravities fetches an artist's complete past-gravity list, newest first, paging on gravityPastPageSize until PastCount entries are gathered (or a short page ends the run). It mirrors AllObjektCollections: one call for the whole browsable history, with a gentle pause between pages.

func (*Client) Artist

func (c *Client) Artist(ctx context.Context, artist string) (Artist, error)

Artist fetches a group's info card.

func (*Client) ComoBalance

func (c *Client) ComoBalance(ctx context.Context, artist string) (int64, error)

ComoBalance returns the user's spendable COMO for an artist (/comos), the figure the vote screens budget against. Profile() also reports it, but that aggregates several endpoints; a vote only needs this one number.

func (*Client) Download

func (c *Client) Download(ctx context.Context, rawURL, destPath string) error

Download streams a media URL to destPath, writing to a .part file first and renaming into place on success so an interrupted transfer can't be mistaken for a complete file. Media lives on a CDN and needs no auth header.

func (*Client) DownloadPost

func (c *Client) DownloadPost(ctx context.Context, p Post, dir string) (downloaded, skipped int, err error)

DownloadPost saves every media file on a post into dir, skipping files whose basename already exists. Returns the count downloaded and skipped.

func (*Client) EnsureToken

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

EnsureToken returns a valid access token, refreshing and persisting it when stale. Safe to call before every request and from concurrent goroutines: the client mutex covers the whole check-refresh-save sequence, so parallel callers of a stale client wait for one refresh instead of each redeeming the refresh token (which could invalidate the others' if redemption is one-shot).

func (*Client) FabricateVote

func (c *Client) FabricateVote(ctx context.Context, pollID int64, choiceID string, comoAmount int64) ([]byte, error)

FabricateVote asks Cosmo to authorize a ballot, returning the opaque voteData payload to embed in the on-chain COMO transfer. The payload carries the poll id, the chosen candidate, and a server signature over both, so a client cannot construct a valid vote for a candidate on its own - this call is a required step, not a convenience.

It does not spend anything by itself; the COMO only moves when the transfer carrying this payload is broadcast.

func (*Client) FetchMessages

func (c *Client) FetchMessages(ctx context.Context, memberID int, take int, before, after int64) ([]Message, error)

FetchMessages fetches a page of a member's messages. before/after are epoch-ms cursors (see Message.CreatedMs); 0 means unset. Pass before to page into older history.

func (*Client) GasStationAbstract

func (c *Client) GasStationAbstract(ctx context.Context) (AbstractGas, error)

GasStationAbstract fetches the current sponsored-gas parameters.

func (*Client) GravityDetail

func (c *Client) GravityDetail(ctx context.Context, id int64) (Gravity, error)

GravityDetail fetches one gravity's full record (/v3/gravities/{id}): the structured Body, its polls, and - once finished - the Result and Leaderboard.

func (*Client) GravityStatus

func (c *Client) GravityStatus(ctx context.Context, id int64) (GravityStatus, error)

GravityStatus fetches the signed-in user's participation in a gravity (/v3/gravities/{id}/status): overall rank and per-poll COMO spend.

func (*Client) ListChannels

func (c *Client) ListChannels(ctx context.Context, group string) ([]Channel, error)

ListChannels lists the group's member channels with last message + unread.

func (*Client) LiveMemberIDs

func (c *Client) LiveMemberIDs(ctx context.Context, group string) (map[int]bool, error)

LiveMemberIDs reports which of a group's members are streaming right now, as a set of artistMemberIds (== members.Member.APIID). One page suffices: a group is never 30 sessions live at once, and an undercount only costs a marker.

func (*Client) MarkRead

func (c *Client) MarkRead(ctx context.Context, memberID int, messageID string) error

MarkRead marks a member's channel read up to messageID.

func (*Client) MediaOriginals

func (c *Client) MediaOriginals(ctx context.Context, memberID, take int, after string) (map[string]string, error)

MediaOriginals maps message id -> full-resolution originalUrl for a member's media. The history/SSE feeds only carry thumbnails; this is the only source of the original (mirrors talk_api.media_originals).

after ("" for the newest page) pages into older media. Unlike the messages endpoint, whose cursors are epoch-ms timestamps, this endpoint's cursors are message ids: afterCursor returns media at-or-older than that id, and any message id works (a non-media id returns the media strictly older than it).

take is capped at 30 server-side (as it is on the messages endpoint) and larger values are silently truncated rather than rejected, so reaching further back means another call with a cursor, not a bigger take.

func (*Client) NoticeDetail

func (c *Client) NoticeDetail(ctx context.Context, id int) (NoticeDetail, error)

NoticeDetail fetches a single announcement by id.

func (*Client) Notices

func (c *Client) Notices(ctx context.Context, group string) ([]Notice, error)

Notices fetches the announcement list for a group, newest first (API order).

func (*Client) Notifications

func (c *Client) Notifications(ctx context.Context, group string) ([]Notification, error)

Notifications fetches the user's notification feed for a group, newest first (API order). artistId is required: the endpoint 400s without one, and an unknown group yields an empty feed rather than an error.

func (*Client) ObjektCollectionsPage

func (c *Client) ObjektCollectionsPage(ctx context.Context, artist, order string, page, size int) (ObjektCollections, error)

ObjektCollectionsPage fetches one page of a user's owned collections for an artist. order must be ObjektSortNewest or ObjektSortOldest; page is 1-based.

func (*Client) OngoingGravities

func (c *Client) OngoingGravities(ctx context.Context, artist string) (GravityLists, error)

OngoingGravities fetches what is open for voting now plus what is scheduled next for an artist. Past is dropped; PastCount carries the artist's full history count.

It asks for category=all, not category=ongoing: the ongoing category always returns an empty upcoming[], however many gravities are queued up. Verified on 2026-07-25, when tripleS had three upcoming gravities (196/197/198) that category=ongoing omitted entirely and category=all returned. take=1 is the smallest page the endpoint accepts - take=0 is a 400 - so a single past entry rides along on the response and is discarded here.

func (*Client) OwnedObjektDetail

func (c *Client) OwnedObjektDetail(ctx context.Context, objektID int64) (ObjektOwnership, error)

OwnedObjektDetail fetches the live ownership record for one owned objekt copy, used to re-check transferability and resolve the token contract right before a send.

func (*Client) PastGravities

func (c *Client) PastGravities(ctx context.Context, artist string, skip, take int) (GravityLists, error)

PastGravities fetches one page of an artist's past gravities, newest first (/v4/gravities?category=all&sortBy=endDate&sort=desc). skip/take paginate; take<=0 uses gravityPastPageSize. The returned PastCount is the total, for deciding whether more pages remain.

func (*Client) PollDetail

func (c *Client) PollDetail(ctx context.Context, pollID int64) (Poll, error)

PollDetail fetches a poll's candidate list (/v3/polls/{id}), used to show the choices for an ongoing poll that the gravity payload leaves empty.

func (*Client) Profile

func (c *Client) Profile(ctx context.Context, group string) (Profile, error)

Profile fetches the aggregated account profile for a group (artistId). The calls are sequential because the public-card and stats endpoints need the numeric user id that only /users/me returns.

func (*Client) RoomPostComments

func (c *Client) RoomPostComments(ctx context.Context, postID string, artistOnly bool) ([]Comment, error)

RoomPostComments fetches a post's full comment thread, newest first. When artistOnly is set the API returns only artist comments and fan comments that received an artist reply. The take is large enough to return every comment in a single request (the endpoint has no server-side cap).

func (*Client) RoomPostTranslation

func (c *Client) RoomPostTranslation(ctx context.Context, postID string) (Translation, error)

RoomPostTranslation fetches the fixed-English auto-translation of a post's text (mirrors the app's "See translation"; same no-target-language design as Client.Translate for Talk messages).

func (*Client) RoomPosts

func (c *Client) RoomPosts(ctx context.Context, group string) ([]Post, error)

RoomPosts fetches every "post"-kind room post for a group, newest first (the API's order). Callers filter by member client-side via Author.Nickname, which maps exactly to member names.

func (*Client) ScheduleDetail

func (c *Client) ScheduleDetail(ctx context.Context, id int) (ScheduleDetail, error)

ScheduleDetail fetches a single schedule by id.

func (*Client) Schedules

func (c *Client) Schedules(ctx context.Context, group string) ([]Schedule, error)

Schedules fetches upcoming schedules for a group.

func (*Client) SearchUsers

func (c *Client) SearchUsers(ctx context.Context, nickname string) ([]UserSearchResult, error)

SearchUsers looks up users by nickname (GET /users/search). The match is a prefix search ranked with the closest match first; a nil/empty slice means no user was found. Not an id lookup - the API offers no numeric-id search.

func (*Client) SendReply

func (c *Client) SendReply(ctx context.Context, memberID int, messageID, content string) (Message, error)

SendReply replies to a specific artist message. Fans reply to a message rather than composing freely; sending is rate-limited server-side.

func (*Client) SetObjektFavorite

func (c *Client) SetObjektFavorite(ctx context.Context, col ObjektCollection, favorite bool) error

SetObjektFavorite pins or unpins a collection (pins show on the app loading screen and float to the top of /objekt-summaries under either sort order). Pinning an already-pinned collection is a no-op; one the user does not own fails with OBJEKT_SUMMARY_NOT_FOUND. The endpoint answers 201 with an empty body, so there is nothing to decode. Callers must keep the pin count within MaxFavoritedObjekts.

A 409 conflict is retried (see favoriteConflictTries): the user's favorite list takes one write at a time, so back-to-back pins have to be spaced out rather than failed.

func (*Client) StreamChannelList

func (c *Client) StreamChannelList(parent context.Context, group string) *Stream

StreamChannelList subscribes to realtime events for the whole channel list.

func (*Client) StreamMessages

func (c *Client) StreamMessages(parent context.Context, memberID int) *Stream

StreamMessages subscribes to realtime events for one member's chat.

func (*Client) Translate

func (c *Client) Translate(ctx context.Context, memberID int, messageID string) (Translation, error)

Translate auto-translates one message (mirrors the app's "See translation").

func (*Client) UserProfile

func (c *Client) UserProfile(ctx context.Context, group string, userID int) (Profile, error)

UserProfile fetches another user's public profile for a group. It mirrors Profile but keys on the given user id and skips /users/me, so wallet totals (COMO) are unavailable; the per-section privacy flags in profile-visibility decide which of the stats/objekt/channel blocks that user exposes, and hidden sections are not fetched.

func (*Client) VideoURL

func (c *Client) VideoURL(ctx context.Context, videoID string) (string, error)

VideoURL resolves a replay's playable/downloadable stream URL, returning ErrNoMembership when the clip is gated. Stays on /bff/v3: v4 has no live-clips detail endpoint.

func (*Client) VodsByMember

func (c *Client) VodsByMember(ctx context.Context, group string) (map[int][]Vod, error)

VodsByMember fetches a group's whole replay history in one request and buckets it per member APIID. A multi-member live is credited only to the member whose channel broadcast it, so each replay appears in exactly one member's list. Bucket AllMembersID holds every clip once. Lists are newest-first, matching the wire order.

type ClipChannel

type ClipChannel struct {
	ID          int    `json:"id"` // == the member's artistMemberId
	Name        string `json:"name"`
	IsConnected bool   `json:"isConnected"`
}

ClipChannel is one member participating in a live clip (v4 lists every member of a multi-member live). IsConnected reports whether this account holds that member's membership.

type Comment

type Comment struct {
	ID        flexString     `json:"id"`
	Author    Author         `json:"author"`
	Content   string         `json:"content"`
	CreatedAt string         `json:"createdAt"`
	IsArtist  bool           `json:"isArtist"`
	IsDeleted bool           `json:"isDeleted"`
	IsBlinded bool           `json:"isBlinded"`
	Replies   []CommentReply `json:"replies"`
}

Comment is a top-level comment on a room post. IsArtist marks a comment written by a group member (vs. a fan).

type CommentReply

type CommentReply struct {
	ID        flexString `json:"id"`
	Author    Author     `json:"author"`
	Content   string     `json:"content"`
	CreatedAt string     `json:"createdAt"`
}

CommentReply is a nested reply under a Comment. (The API omits isArtist here.)

type DailyStats

type DailyStats struct {
	ObjektCount        int `json:"objektCount"`
	JoinedLiveCount    int `json:"joinedLiveCount"`
	JoinedGravityCount int `json:"joinedGravityCount"`
	OfflineBadgeCount  int `json:"offlineBadgeCount"`
	CompletedGridCount int `json:"completedGridCount"`
}

DailyStats are the headline activity counters (GET /user-stats-daily).

type Gravity

type Gravity struct {
	ID              int64
	Type            string // GravityEvent | GravityGrand
	PollType        string // PollSingle | PollCombination
	Artist          string
	Title           string
	Description     string
	StartDate       string // entireStartDate (ISO-8601)
	EndDate         string // entireEndDate (ISO-8601)
	BannerImageURL  string
	ContractOutlink string // block explorer link (abscan for Abstract, polygonscan for legacy)
	Body            []BodyBlock
	Polls           []Poll
	Result          *GravityResult     // overall winner, once finalized
	Leaderboard     []LeaderboardEntry // top COMO spenders, once finalized
}

Gravity is one voting event. Result and Leaderboard are populated only for finished gravities; Polls carries the poll(s) - one for single/grand events, and each poll's own Result once it is finalized. Choices on a poll are filled separately by PollDetail (the list/detail payloads omit them for ongoing polls).

func (Gravity) CountingPoll

func (g Gravity) CountingPoll() (Poll, bool)

CountingPoll returns a poll whose voting is over but whose result is not published yet, or false when none is. It spans both halves of that gap - still counting, and counted but unrevealed - because the deadline they share is the same RevealDate, which is all the callers want from it.

func (Gravity) NextPoll

func (g Gravity) NextPoll() (Poll, bool)

NextPoll returns the poll due to open next, or false when none is scheduled. It is what a gravity shows before its voting starts.

func (Gravity) OpenPoll

func (g Gravity) OpenPoll() (Poll, bool)

OpenPoll returns the poll currently accepting votes, or false when none is. A poll outside its window does not count, in either direction: a ballot fabricated against one that has not opened, or that has closed and is waiting to be revealed, would be rejected. Finalized and Result are both refused on top of the window check - either one means the vote is over regardless of what the dates say, and this is the gate a real COMO spend passes through.

func (Gravity) PollContract

func (g Gravity) PollContract() (addr string, onAbstract bool)

PollContract returns the on-chain contract a vote for this gravity is cast to, parsed from ContractOutlink. onAbstract reports whether the link points at Abstract (abscan): pre-migration gravities link to Polygon instead, and those are long closed, so a vote can only ever be cast against an Abstract one.

func (Gravity) State

func (g Gravity) State() GravityState

State reports the gravity's lifecycle state. For a grand gravity, whose days are separate polls, the most live state wins: a day still open makes the whole gravity StateVoting, and a day awaiting its reveal outranks one merely scheduled.

func (Gravity) Votable

func (g Gravity) Votable() bool

Votable reports whether a vote can actually be cast on this gravity: a poll must be accepting votes right now (not scheduled, not closed and counting), it must be a single-choice poll (the legacy combination form is only found on closed Polygon-era events), and it must live on Abstract.

type GravityLists

type GravityLists struct {
	Upcoming  []Gravity
	Ongoing   []Gravity
	Past      []Gravity
	PastCount int
}

GravityLists is the upcoming/ongoing/past split returned by /v4/gravities. PastCount is the server's total count of past gravities, for paging Past.

type GravityResult

type GravityResult struct {
	TotalComoUsed  int64
	ResultTitle    string
	ResultImageURL string
}

GravityResult is a finished gravity's overall winner.

type GravityState

type GravityState int

GravityState is where a gravity sits in its lifecycle. Neither Finalized nor the poll window tells them apart alone: Finalized flips partway through the post-voting gap, and the window says nothing about whether the tally has been published. Only the arrival of a poll's Result marks a gravity as finished.

const (
	// StateFinished: every poll's result is in.
	StateFinished GravityState = iota
	// StateUpcoming: voting has not opened yet.
	StateUpcoming
	// StateCounted: the tally is locked (Finalized) but the result has not been
	// published. This is the second half of the gap below, and on the wire it is
	// the one shape that never occurs anywhere else: across tripleS's whole
	// 102-gravity history every finalized poll carries a result, so a finalized
	// poll without one can only be a reveal that has not landed yet.
	StateCounted
	// StateCounting: voting has closed, but the result is not revealed yet.
	// Cosmo leaves a gap here on purpose - poll 235 closed at 01:00 UTC and
	// revealed at 04:00 - and throughout it the poll refuses votes. Finalized
	// does not span the whole gap: 235 was still unfinalized at 01:00 and had
	// flipped by 03:07, an hour before its reveal, with no result attached.
	StateCounting
	// StateVoting: a poll is accepting votes right now.
	StateVoting
)

func (GravityState) String

func (s GravityState) String() string

type GravityStatus

type GravityStatus struct {
	Rank          int
	TotalComoUsed int64
	Votes         []PollVoteStatus
}

GravityStatus is the signed-in user's own participation in a gravity: their overall rank and per-poll COMO spend.

type LeaderboardEntry

type LeaderboardEntry struct {
	Rank     int
	Nickname string
	Address  string
	ComoUsed int64
}

LeaderboardEntry is one user's total COMO spend on a gravity (top 10).

type Media

type Media struct {
	URL  string `json:"url"`
	Kind string `json:"kind"` // "image" | "video"
}

Media is one attachment on a room post.

type MembershipChannel

type MembershipChannel struct {
	ID           int    `json:"id"` // artistMemberId (matches members.APIID)
	Name         string `json:"name"`
	IsConnected  bool   `json:"isConnected"`
	DaysTogether int    `json:"daysTogether"`
}

MembershipChannel is one member's subscription channel for the current user. IsConnected reports whether the user holds that member's membership (the flag that gates replay downloads and the talk feature); DaysTogether is the per-member streak. Distinct from talk.go's Channel (a messaging conversation).

type Message

type Message struct {
	ID         string
	Type       string
	SenderType string
	SenderID   int
	Content    string
	CreatedAt  string
	SenderName string
	Reply      *Reply
	Cursor     string
	MediaURL   string
	// DurationMs is how long an audio/video attachment runs, in milliseconds;
	// 0 for stills and for feeds that omit the metadata. Photos carry no
	// comparable field — their metadata is only an aspect ratio.
	DurationMs int
	// StickerURL is the image for a sticker message (type "sticker"). Stickers
	// are shared app assets, not per-member media: the URL arrives complete on
	// the message and has no original to upgrade to.
	StickerURL string
}

Message is one Talk message. SenderType is "artistMember" or "user" (you).

func MessageFromEvent

func MessageFromEvent(data string, isArtist bool) (Message, bool)

MessageFromEvent builds a Message from an SSE user-message.* payload. These compact payloads use artistMemberId and omit senderType, so the caller infers the sender from the event name and passes isArtist. Returns false if the payload has no id.

func (Message) Attachment

func (m Message) Attachment() string

Attachment is the file the message carries — its media or its sticker image — or "" when it carries neither. This is what opening and downloading act on.

func (Message) CreatedMs

func (m Message) CreatedMs() int64

CreatedMs is CreatedAt as an epoch-millisecond int (the cursor unit the API uses); 0 if unparseable.

func (Message) IsArtist

func (m Message) IsArtist() bool

IsArtist reports whether the message came from the artist.

func (Message) IsMedia

func (m Message) IsMedia() bool

IsMedia reports whether the message carries an image, video, or voice message.

func (Message) IsSticker

func (m Message) IsSticker() bool

IsSticker reports whether the message is a sticker.

type Notice

type Notice struct {
	ID       int    `json:"id"`
	Category string `json:"category"`
	Title    string `json:"title"`
	ActiveAt string `json:"activeAt"`
}

Notice is one announcement list entry. Category is "Notice" or "Content".

type NoticeDetail

type NoticeDetail struct {
	ID           int      `json:"id"`
	Category     string   `json:"category"`
	Title        string   `json:"title"`
	Content      string   `json:"content"`
	ImageURLList []string `json:"imageUrlList"`
	ActiveAt     string   `json:"activeAt"`
}

NoticeDetail is a full announcement. Content is plain text with newlines.

type Notification

type Notification struct {
	ID       string `json:"id"`
	Category string `json:"category"`
	Title    string `json:"title"`
	Content  string `json:"content"`
	SentAt   string `json:"sentAt"`
}

Notification is one entry of the notification feed: a live start, a replay becoming available, a new story, a new announcement, a shop drop. Content is the whole notification (there is no by-id detail call), and SentAt is RFC-3339 in UTC, unlike Schedule's KST offsets. Category is open-ended - "Room", "Etc" and "Shop" are what the API serves today, so render it verbatim rather than switching on it.

The wire also carries "url" (a cosmo:// deep link into the app, e.g. cosmo://tripleS/notice?id=262) and "isRead"; neither is modeled because nothing renders them yet.

type ObjektCollection

type ObjektCollection struct {
	CollectionNo string // "101Z"
	Season       string // "Binary02"
	Class        string // "First" | "Special" | "Welcome" | ...
	Member       string // "ChaeWon"
	ArtistName   string // "tripleS"
	ComoAmount   int
	Transferable bool // transferable by default (a specific copy may still be locked)
	Gridable     bool
	AccentColor  string // hex, e.g. "#75FB4C"
	FrontImage   string
	BackImage    string
	FavoritedAt  string // ISO-8601; empty when not pinned
}

ObjektCollection is the card design shared by every copy of a collection: its identity (Season/CollectionNo/Class/Member), the COMO it grants, and the accent color the app tints the card with. FavoritedAt is non-empty when the user has pinned the collection (the pin shows on the app loading screen).

func (ObjektCollection) Favorited

func (c ObjektCollection) Favorited() bool

Favorited reports whether the collection is pinned.

func (ObjektCollection) GeneratesComo

func (c ObjektCollection) GeneratesComo() bool

GeneratesComo reports whether each copy of this collection drops COMO monthly. When true, ComoAmount is the monthly rate per copy and OwnedObjekt.MintedAtDay is the day of the month it drops.

type ObjektCollections

type ObjektCollections struct {
	CollectionCount int
	FavoritedCount  int
	Collections     []OwnedCollection
}

ObjektCollections is a user's full objekt collection for one artist. CollectionCount is the number of distinct collections; FavoritedCount is how many objekts are pinned.

type ObjektGroup

type ObjektGroup struct {
	Name  string `json:"name"`
	Count int    `json:"count"`
	Color string `json:"color"`
}

ObjektGroup is one bucket in an owned-objekt breakdown (by class, member, …).

type ObjektOwnership

type ObjektOwnership struct {
	TokenAddress string
	Transferable bool
	Owner        string
	ObjektID     int64
}

ObjektOwnership is GET /owned-by/me/{objektId}: the on-chain facts the send flow needs about one owned copy. Owner is the AGW smart-account address that holds it (the transfer's from), and ObjektID equals the on-chain tokenId.

type OwnedCollection

type OwnedCollection struct {
	Collection ObjektCollection
	Count      int
	Objekts    []OwnedObjekt
}

OwnedCollection is a collection the user owns plus every copy of it (Count == len(Objekts)).

type OwnedObjekt

type OwnedObjekt struct {
	ObjektNo     int64 // serial number
	ObjektID     int64 // inventory objektId; the on-chain tokenId (== TokenID) the send flow transfers
	TokenID      int64
	TokenAddress string // the ERC-721 contract holding this copy (transferFrom target)
	Transferable bool
	Status       string // inventory status, e.g. "minted"
	AcquiredAt   string // ISO-8601
	MintedAt     string // ISO-8601
	MintedAtDay  int    // day of month COMO drops (for generating classes)
	UsedForGrid  bool
	Owner        string // on-chain address
}

OwnedObjekt is one owned copy of a collection. ObjektNo is its serial number; Transferable is this copy's own flag (e.g. false while used for a grid or if it was a challenge reward). Owner is the on-chain wallet holding it.

type Poll

type Poll struct {
	ID             int64
	Type           string
	GravityID      int64
	Title          string
	StartDate      string
	EndDate        string
	RevealDate     string
	Finalized      bool
	IndexInGravity int
	Choices        []Choice
	Result         *PollResult
}

Poll is one round of a gravity. Choices is the candidate list (filled by PollDetail); Result is the ranked outcome, present once the reveal lands - which trails Finalized, so the two are not interchangeable (see StateCounted).

func (Poll) Ended

func (p Poll) Ended() bool

Ended reports whether the poll's voting window has closed. A poll can be ended and still unfinalized: that is the counting gap before the reveal.

func (Poll) Started

func (p Poll) Started() bool

Started reports whether the poll's voting window has opened.

type PollResult

type PollResult struct {
	TotalComoUsed int64
	Results       []VoteResult
}

PollResult is a finalized poll's ranked tally.

type PollVoteStatus

type PollVoteStatus struct {
	PollID   int64
	ComoUsed int64
	Votes    []Vote
}

PollVoteStatus is the user's spend on one poll of a gravity, plus each vote they cast in it. Votes is populated once a poll's result is revealed; during an open vote Cosmo reports ComoUsed 0 and no votes even for a ballot that is already on-chain, so an empty record does not mean the user did not vote.

type Post

type Post struct {
	ID               flexString `json:"id"`
	Content          string     `json:"content"`
	CreatedAt        string     `json:"createdAt"`
	Author           Author     `json:"author"`
	Media            []Media    `json:"media"`
	MediaAspectRatio string     `json:"mediaAspectRatio"`
	Comments         []Comment  `json:"comments"`
	VideoItem        *VideoItem `json:"videoItem"`
}

Post is one room post. For kind=post it carries Media; for kind=live-clip it carries VideoItem instead. The id arrives as a JSON number (flexString).

type PrivySession

type PrivySession struct {
	AccessToken  string
	RefreshToken string
	EOA          string
}

PrivySession is the Privy side of the login handshake, kept so the caller can provision the embedded-wallet signing key (see internal/wallet). AccessToken authorizes the Privy recovery calls; EOA is the embedded-wallet address they are keyed by (empty if the response carried no ethereum wallet account).

func Login

func Login(ctx context.Context, email, code string) (config.Credentials, PrivySession, error)

Login runs the full flow after the user has the emailed code, returning the Cosmo credentials plus the Privy session (for wallet provisioning).

func VerifyCode

func VerifyCode(ctx context.Context, email, code string) (PrivySession, error)

VerifyCode exchanges the emailed code for a Privy session (access token plus the embedded-wallet address the recovery flow needs).

type Profile

type Profile struct {
	ID                 int
	Nickname           string
	Address            string // wallet address
	StatusMessage      string // bio; empty when unset
	FandomName         string
	FollowDurationDays int
	CurrentStreak      int
	CreatedAt          string // ISO-8601; render via LocalDate

	TotalComo   int
	TotalObjekt int

	Stats          DailyStats
	ObjektsByClass []ObjektGroup
	Channels       []MembershipChannel

	// Self is true for the logged-in user's own profile (fetched via /users/me),
	// which always exposes every section and carries wallet totals. For another
	// user (fetched via UserProfile) it is false: COMO is unavailable (HasComo
	// false) and Visibility governs which sections that user has made public.
	Self       bool
	HasComo    bool
	Visibility ProfileVisibility
}

Profile is the aggregated account profile for one artist (group), assembled from the several endpoints the app's Profile tab loads: /users/me (identity), /comos (per-artist COMO balance), /users/{id}?artistId (the public card: bio, fandom, follow duration), /user-stats-daily (activity counters, incl. the objekt count), and /activities/my-objekts (owned-objekt breakdown).

func (Profile) ConnectedChannels

func (p Profile) ConnectedChannels() []MembershipChannel

ConnectedChannels returns only the channels the user has joined.

func (Profile) ShowChannels

func (p Profile) ShowChannels() bool

ShowChannels reports whether the joined-channels block should render.

func (Profile) ShowObjektStats

func (p Profile) ShowObjektStats() bool

ShowObjektStats reports whether the owned-objekt breakdown should render.

func (Profile) ShowOverview

func (p Profile) ShowOverview() bool

ShowOverview reports whether the headline stats block should render (always for one's own profile; per the visibility flag for others).

type ProfileVisibility

type ProfileVisibility struct {
	Activity         bool `json:"activity"`
	FavoritedObjekt  bool `json:"favoritedObjekt"`
	Overview         bool `json:"overview"`
	ConnectedChannel bool `json:"connectedChannel"`
	Badge            bool `json:"badge"`
	Ranking          bool `json:"ranking"`
	ObjektStatistics bool `json:"objektStatistics"`
}

ProfileVisibility is GET /users/{id}/profile-visibility: the per-section privacy flags another user has set. Own profiles ignore it (all sections show). Distinct sections map to the profile view's blocks.

type Reply

type Reply struct {
	ID       string
	SenderID int
	Content  string
	// HasSticker reports whether the quoted message carried a sticker. Fans can
	// send one with no text at all, which would otherwise quote as a bare arrow.
	// Only the fact is kept, not the URL: the quoted sticker is not what the
	// open/save keys act on, and holding its URL here would only blur that.
	HasSticker bool
}

Reply is the fan message an artist message quotes.

type SNSLink struct {
	Address string `json:"address"`
}

SNSLink is one official social link, keyed by platform in Artist.SNSLink.

type SSEEvent

type SSEEvent struct {
	Event string
	Data  string
}

SSEEvent is one decoded event from the feed.

type Schedule

type Schedule struct {
	ID      int    `json:"id"`
	Title   string `json:"title"`
	StartAt string `json:"startAt"`
	EndAt   string `json:"endAt"`
}

Schedule is one schedule list entry. StartAt/EndAt are ISO-8601 with a KST (+09:00) offset.

type ScheduleDetail

type ScheduleDetail struct {
	ID      int              `json:"id"`
	Title   string           `json:"title"`
	Content string           `json:"content"`
	StartAt string           `json:"startAt"`
	EndAt   string           `json:"endAt"`
	Place   string           `json:"place"`
	Members []ScheduleMember `json:"members"`
}

ScheduleDetail is a full schedule with place and participating members.

type ScheduleMember

type ScheduleMember struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

ScheduleMember is one member attached to a schedule.

type Stream

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

Stream is a running SSE subscription. Read events from Events(); call Close() to stop. Events() is closed when the stream is Closed or its parent context is cancelled.

func (*Stream) Close

func (s *Stream) Close()

Close stops the stream and releases its connection.

func (*Stream) Events

func (s *Stream) Events() <-chan SSEEvent

Events returns the channel of decoded events (plus synthetic "reconnected").

type Translation

type Translation struct {
	MessageID              string
	TranslatedContent      string
	DetectedSourceLanguage string
	ReplyTranslatedContent string
}

Translation is the auto-translation of one message.

type UserSearchResult

type UserSearchResult struct {
	ID       int    `json:"id"`
	Nickname string `json:"nickname"`
	Address  string `json:"address"`
}

UserSearchResult is one hit from GET /users/search (nickname lookup).

type VideoItem

type VideoItem struct {
	ID           flexString    `json:"id"`
	Duration     int           `json:"duration"`   // seconds
	AccessType   string        `json:"accessType"` // "connected" (membership-gated) | "all"
	ThumbnailURL string        `json:"thumbnailUrl"`
	Channels     []ClipChannel `json:"channels"`
}

VideoItem is the video payload on a live-clip post (replay list). The id arrives as a JSON number, so it uses flexString (see talk.go).

type Vod

type Vod struct {
	VideoID      string
	Duration     int // seconds
	Title        string
	Member       string // the streaming member (the post's author)
	Date         string // YYYY-MM-DD in the system's local timezone
	Playable     bool   // false when the clip is gated behind a membership we lack
	ThumbnailURL string // public CDN, viewable even without membership
}

Vod is one live-stream replay in a member's history.

type Vote

type Vote struct {
	ChoiceID   string
	ChoiceName string // "Trust No One"
	ComoUsed   int64
	At         string // ISO-8601
}

Vote is one ballot the user cast: which candidate, how much COMO, and when.

type VoteResult

type VoteResult struct {
	Rank           int
	ChoiceName     string
	ChoiceImageURL string
	ComoUsed       int64
}

VoteResult is one candidate's standing in a finalized poll.

Jump to

Keyboard shortcuts

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