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
- Variables
- func Basename(rawURL string) string
- func ErrorCode(err error) string
- func ExtractCredentials(node any) (config.Credentials, error)
- func LocalDate(iso string) string
- func LocalDateTime(iso string) string
- func LoginByPrivy(ctx context.Context, privyToken string) (config.Credentials, error)
- func SendCode(ctx context.Context, email string) error
- type APIError
- type AbstractGas
- type AbstractLog
- type AbstractReceipt
- type AbstractTxStatus
- type Artist
- type ArtistMember
- type Author
- type BodyBlock
- type Channel
- type Choice
- type Client
- func (c *Client) AbstractEstimateGas(ctx context.Context, from, to, data string) (uint64, error)
- func (c *Client) AbstractNonce(ctx context.Context, addr string) (uint64, error)
- func (c *Client) AbstractOwnerOf(ctx context.Context, tokenAddress string, tokenID int64) (string, error)
- func (c *Client) AbstractRPC(ctx context.Context, method string, params ...any) (json.RawMessage, error)
- func (c *Client) AbstractSendRawTx(ctx context.Context, rawHex string) (string, error)
- func (c *Client) AbstractTxState(ctx context.Context, hash string) (AbstractReceipt, error)
- func (c *Client) AllObjektCollections(ctx context.Context, artist, order string) (ObjektCollections, error)
- func (c *Client) AllPastGravities(ctx context.Context, artist string) ([]Gravity, error)
- func (c *Client) Artist(ctx context.Context, artist string) (Artist, error)
- func (c *Client) ComoBalance(ctx context.Context, artist string) (int64, error)
- func (c *Client) Download(ctx context.Context, rawURL, destPath string) error
- func (c *Client) DownloadPost(ctx context.Context, p Post, dir string) (downloaded, skipped int, err error)
- func (c *Client) EnsureToken(ctx context.Context) (string, error)
- func (c *Client) FabricateVote(ctx context.Context, pollID int64, choiceID string, comoAmount int64) ([]byte, error)
- func (c *Client) FetchMessages(ctx context.Context, memberID int, take int, before, after int64) ([]Message, error)
- func (c *Client) GasStationAbstract(ctx context.Context) (AbstractGas, error)
- func (c *Client) GravityDetail(ctx context.Context, id int64) (Gravity, error)
- func (c *Client) GravityStatus(ctx context.Context, id int64) (GravityStatus, error)
- func (c *Client) ListChannels(ctx context.Context, group string) ([]Channel, error)
- func (c *Client) LiveMemberIDs(ctx context.Context, group string) (map[int]bool, error)
- func (c *Client) MarkRead(ctx context.Context, memberID int, messageID string) error
- func (c *Client) MediaOriginals(ctx context.Context, memberID, take int, after string) (map[string]string, error)
- func (c *Client) NoticeDetail(ctx context.Context, id int) (NoticeDetail, error)
- func (c *Client) Notices(ctx context.Context, group string) ([]Notice, error)
- func (c *Client) Notifications(ctx context.Context, group string) ([]Notification, error)
- func (c *Client) ObjektCollectionsPage(ctx context.Context, artist, order string, page, size int) (ObjektCollections, error)
- func (c *Client) OngoingGravities(ctx context.Context, artist string) (GravityLists, error)
- func (c *Client) OwnedObjektDetail(ctx context.Context, objektID int64) (ObjektOwnership, error)
- func (c *Client) PastGravities(ctx context.Context, artist string, skip, take int) (GravityLists, error)
- func (c *Client) PollDetail(ctx context.Context, pollID int64) (Poll, error)
- func (c *Client) Profile(ctx context.Context, group string) (Profile, error)
- func (c *Client) RoomPostComments(ctx context.Context, postID string, artistOnly bool) ([]Comment, error)
- func (c *Client) RoomPostTranslation(ctx context.Context, postID string) (Translation, error)
- func (c *Client) RoomPosts(ctx context.Context, group string) ([]Post, error)
- func (c *Client) ScheduleDetail(ctx context.Context, id int) (ScheduleDetail, error)
- func (c *Client) Schedules(ctx context.Context, group string) ([]Schedule, error)
- func (c *Client) SearchUsers(ctx context.Context, nickname string) ([]UserSearchResult, error)
- func (c *Client) SendReply(ctx context.Context, memberID int, messageID, content string) (Message, error)
- func (c *Client) SetObjektFavorite(ctx context.Context, col ObjektCollection, favorite bool) error
- func (c *Client) StreamChannelList(parent context.Context, group string) *Stream
- func (c *Client) StreamMessages(parent context.Context, memberID int) *Stream
- func (c *Client) Translate(ctx context.Context, memberID int, messageID string) (Translation, error)
- func (c *Client) UserProfile(ctx context.Context, group string, userID int) (Profile, error)
- func (c *Client) VideoURL(ctx context.Context, videoID string) (string, error)
- func (c *Client) VodsByMember(ctx context.Context, group string) (map[int][]Vod, error)
- type ClipChannel
- type Comment
- type CommentReply
- type DailyStats
- type Gravity
- type GravityLists
- type GravityResult
- type GravityState
- type GravityStatus
- type LeaderboardEntry
- type Media
- type MembershipChannel
- type Message
- type Notice
- type NoticeDetail
- type Notification
- type ObjektCollection
- type ObjektCollections
- type ObjektGroup
- type ObjektOwnership
- type OwnedCollection
- type OwnedObjekt
- type Poll
- type PollResult
- type PollVoteStatus
- type Post
- type PrivySession
- type Profile
- type ProfileVisibility
- type Reply
- type SNSLink
- type SSEEvent
- type Schedule
- type ScheduleDetail
- type ScheduleMember
- type Stream
- type Translation
- type UserSearchResult
- type VideoItem
- type Vod
- type Vote
- type VoteResult
Constants ¶
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" )
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.
const ( ObjektSortNewest = "newest" ObjektSortOldest = "oldest" )
ObjektSortNewest and ObjektSortOldest are the only accepted order values.
const AllMembersID = 0
AllMembersID is the VodsByMember bucket holding the group's whole replay history, each clip once; no member has APIID 0.
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).
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.
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.
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.
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 ¶
var ErrNeedsLogin = errors.New("no valid credentials; log in again")
ErrNeedsLogin means we have no usable refresh token; the user must log in.
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 ¶
Basename returns the filename portion of a media URL (used to name and dedupe downloads).
func ErrorCode ¶
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 ¶
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 ¶
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 ¶
LoginByPrivy exchanges a Privy access token for Cosmo account credentials.
Types ¶
type APIError ¶
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 ¶
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 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 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 ¶
AbstractEstimateGas estimates the gas limit for a call (eth_estimateGas). data is 0x-hex calldata.
func (*Client) AbstractNonce ¶
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 ¶
AbstractSendRawTx broadcasts a signed raw transaction (0x-hex) and returns its hash (eth_sendRawTransaction).
func (*Client) AbstractTxState ¶
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 ¶
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) ComoBalance ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ListChannels lists the group's member channels with last message + unread.
func (*Client) LiveMemberIDs ¶
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) 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 ¶
NoticeDetail fetches a single announcement by id.
func (*Client) Notices ¶
Notices fetches the announcement list for a group, newest first (API order).
func (*Client) Notifications ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ScheduleDetail fetches a single schedule by id.
func (*Client) SearchUsers ¶
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 ¶
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 ¶
StreamChannelList subscribes to realtime events for the whole channel list.
func (*Client) StreamMessages ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
LeaderboardEntry is one user's total COMO spend on a gravity (top 10).
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 ¶
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 ¶
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 ¶
CreatedMs is CreatedAt as an epoch-millisecond int (the cursor unit the API uses); 0 if unparseable.
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 ¶
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).
type PollResult ¶
type PollResult struct {
TotalComoUsed int64
Results []VoteResult
}
PollResult is a finalized poll's ranked tally.
type PollVoteStatus ¶
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 ¶
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 ¶
ShowChannels reports whether the joined-channels block should render.
func (Profile) ShowObjektStats ¶
ShowObjektStats reports whether the owned-objekt breakdown should render.
func (Profile) ShowOverview ¶
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 ¶
type SNSLink struct {
Address string `json:"address"`
}
SNSLink is one official social link, keyed by platform in Artist.SNSLink.
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 ¶
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.
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.