Documentation
¶
Overview ¶
Package producthunt is the library behind the ph command line: the HTTP client for both planes, the offline reference layer, and the typed records read from public Product Hunt surfaces.
Product Hunt has two planes. The web plane (www.producthunt.com) is the default, but every page is fronted by Cloudflare, so the one anonymous surface that survives is the Atom feed at /feed; a walled response becomes ErrBlocked before any parser runs. The api plane (api.producthunt.com/v2/api/graphql) is the opt-in upgrade, turned on by a PRODUCTHUNT_TOKEN or a PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET in the environment, and reads reliably from anywhere. The Client below fetches both, paces and retries politely, caches on disk, and turns a walled or rejected response into a typed error the exit-code mapping understands.
Index ¶
- Constants
- Variables
- func Defaults(c *kit.Config)
- func Identity() kit.Identity
- func URLFor(kind, id string) string
- type Client
- func (c *Client) CollectionAPI(ctx context.Context, ref string) (*Collection, error)
- func (c *Client) CollectionsAPI(ctx context.Context, n int) ([]*Collection, error)
- func (c *Client) CommentsAPI(ctx context.Context, ref string, n int) ([]*Comment, error)
- func (c *Client) Feed(ctx context.Context, limit int) ([]*Post, error)
- func (c *Client) FeedPost(ctx context.Context, ref string) (*Post, error)
- func (c *Client) PostAPI(ctx context.Context, ref string) (*Post, error)
- func (c *Client) PostsAPI(ctx context.Context, n int) ([]*Post, error)
- func (c *Client) TopicAPI(ctx context.Context, ref string) (*Topic, error)
- func (c *Client) TopicsAPI(ctx context.Context, n int) ([]*Topic, error)
- func (c *Client) UserAPI(ctx context.Context, ref string) (*User, error)
- type Collection
- type Comment
- type Config
- type Domain
- type Maker
- type Media
- type Post
- type ProductLink
- type Ref
- type Topic
- type TopicLink
- type User
Constants ¶
const ( // Host is the public website, the web plane, and the host the URI driver claims. Host = "www.producthunt.com" // BaseURL is the root of the web plane. BaseURL = "https://" + Host // FeedURL is the one anonymous web surface that survives the Cloudflare wall. FeedURL = BaseURL + "/feed" // APIURL is the GraphQL endpoint of the opt-in api plane. APIURL = "https://api.producthunt.com/v2/api/graphql" // OAuthURL mints a public-scope token from an application's client credentials. OAuthURL = "https://api.producthunt.com/v2/oauth/token" // DefaultPlane is the automatic, environment-driven plane choice. DefaultPlane = "auto" // DefaultCacheTTL is how long a cached response stays fresh by default. Product // Hunt runs on a daily cadence, so a few hours is plenty. DefaultCacheTTL = 6 * time.Hour )
const DefaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
DefaultUserAgent identifies the client. It names a current browser, because the web plane is fronted by Cloudflare and an obviously scripted agent is turned away faster; it is still honest in that the tool does not forge a crawler identity it is reverse-DNS-checked against.
Variables ¶
var ( // ErrBlocked is the wall: a Cloudflare 403/503 or challenge interstitial on the // web plane, or a rejected/missing token (401, invalid_oauth_token) on the api // plane. It maps to need-auth (exit 4), with a remedy of reading the public Atom // feed or setting free Product Hunt API credentials. ErrBlocked = errors.New("blocked: Product Hunt's web pages are walled by Cloudflare here. The public Atom feed (ph feed) needs no account; for the full API set PRODUCTHUNT_TOKEN, or PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET") // ErrNeedKey is an api-only surface reached with no credentials. Maps to exit 4. ErrNeedKey = errors.New("this surface needs the Product Hunt API: set PRODUCTHUNT_TOKEN, or PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET (free credentials from https://www.producthunt.com/v2/oauth/applications)") // ErrNotFound is a missing entity (a null node for a single lookup). Maps to exit 6. ErrNotFound = errors.New("not found") // ErrRateLimited is a sustained 429 after retries, or the GraphQL complexity cap. // Maps to exit 5. ErrRateLimited = errors.New("rate limited by Product Hunt: slow down with --rate or try again later") // ErrNetwork is a transport failure that survives every retry: a connection // refused, a timeout, a DNS error, or a 5xx that never recovered. Maps to exit 8. ErrNetwork = errors.New("network error reaching Product Hunt") // ErrUsage is a bad argument the library catches (an unrecognized reference, a // bad order). Maps to exit 2. ErrUsage = errors.New("usage") )
Sentinel errors the library returns; domain.go's mapErr turns each into the kit error kind that carries the right exit code (see the spec section 4.5).
Functions ¶
func Defaults ¶ added in v0.2.0
Defaults seeds the framework baseline with producthunt's own values, so an unset --rate or --timeout uses the producthunt default rather than the generic kit one.
func Identity ¶ added in v0.2.0
Identity is the fixed description of the Product Hunt CLI, shared by the domain and the standalone composition root so help and version read the same.
func URLFor ¶ added in v0.2.0
URLFor builds an addressable URL for a (kind, id), or "" if it cannot. A page URL needs the human slug, so a slug or username rebuilds the page directly; a bare numeric post id cannot rebuild the discussion URL offline (it needs the slug), so it returns the stable /r/p/<n> redirect, the one addressable URL a numeric post id can build. A record's own url carries the discussion page after a fetch.
Types ¶
type Client ¶
Client reads public Product Hunt data over HTTP on both planes.
func ClientFromConfig ¶ added in v0.2.0
ClientFromConfig maps the framework config onto a producthunt.Config and returns a client. The credentials are read from the environment in DefaultConfig, never from a flag.
func NewClient ¶
NewClient returns a Client configured from cfg, filling unset fields with their defaults.
func (*Client) CollectionAPI ¶ added in v0.2.0
CollectionAPI reads one collection with its posts.
func (*Client) CollectionsAPI ¶ added in v0.2.0
CollectionsAPI lists collections, paging up to n.
func (*Client) CommentsAPI ¶ added in v0.2.0
CommentsAPI reads a post's comment thread, paging up to n. Each top-level comment is followed by its replies, with the parent edge set.
func (*Client) Feed ¶ added in v0.2.0
Feed fetches the Atom feed and returns up to limit recent posts, newest first as the feed orders them.
func (*Client) FeedPost ¶ added in v0.2.0
FeedPost returns the one feed post whose id or slug matches ref, or ErrNotFound when it is not in the recent feed. This is the keyless fallback for `ph post`.
func (*Client) PostsAPI ¶ added in v0.2.0
PostsAPI reads the ranked, filtered post stream, paging up to n.
type Collection ¶ added in v0.2.0
type Collection struct {
ID string `json:"id" kit:"id"`
Name string `json:"name,omitempty" table:",truncate"`
Tagline string `json:"tagline,omitempty" table:",truncate"`
Description string `json:"description,omitempty" table:",truncate" kit:"body"`
Slug string `json:"slug,omitempty"`
Followers int `json:"followers,omitempty"`
CoverImage string `json:"cover_image,omitempty" table:"-"`
CreatedAt string `json:"created_at,omitempty" table:"-"`
FeaturedAt string `json:"featured_at,omitempty" table:"-"`
Curator string `json:"curator,omitempty" table:"-"` // the owner's username
CuratorName string `json:"curator_name,omitempty" table:"-"`
Topics []TopicLink `json:"topics,omitempty" table:"-"`
URL string `json:"url"`
CuratorRef string `json:"curator_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"`
TopicRefs []string `json:"topic_refs,omitempty" table:"-" kit:"link,kind=producthunt/topic"`
PostRefs []string `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"`
}
Collection is a curated list of posts, emitted by collections and collection.
type Comment ¶ added in v0.2.0
type Comment struct {
ID string `json:"id" kit:"id"`
Post string `json:"post,omitempty" table:"-" kit:"link,kind=producthunt/post"` // the post id
Parent string `json:"parent,omitempty" table:"-"` // parent comment id, for replies
Body string `json:"body,omitempty" table:",truncate" kit:"body"`
Votes int `json:"votes,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Author string `json:"author,omitempty"` // username
AuthorName string `json:"author_name,omitempty" table:"-"`
URL string `json:"url,omitempty" table:"-"`
UserRef string `json:"user_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"`
}
Comment is one comment on a post, emitted by comments. Post is the edge back to the commented post; an author is a username and a display name, never a fabricated member node.
type Config ¶
type Config struct {
UserAgent string
Delay time.Duration // minimum gap between requests
Retries int // retries on 429/5xx
Timeout time.Duration // per-request timeout
Plane string // "web", "api", or "auto"
Token string // a ready bearer token, from PRODUCTHUNT_TOKEN only
ClientID string // an application id, from PRODUCTHUNT_CLIENT_ID only
ClientSecret string // its secret, from PRODUCTHUNT_CLIENT_SECRET only
// List shaping, shared by the read commands.
Order string // ranking, newest, votes, featured (per surface)
Topic string // a topic slug to filter posts
Featured bool // restrict to featured posts or collections
After string // postedAfter, an ISO date
Before string // postedBefore, an ISO date
BaseURL string // overridable for tests
APIURL string // overridable for tests
OAuthURL string // overridable for tests
CacheDir string
NoCache bool
CacheTTL time.Duration
Refresh bool // refetch and rewrite the cache, ignoring any hit
}
Config is the resolved settings a Client reads.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns the baseline settings and reads the credentials from the environment.
type Domain ¶ added in v0.2.0
type Domain struct{}
Domain is the producthunt driver. It carries no state; the per-run client is built by the factory Register hands kit.
func (Domain) Classify ¶ added in v0.2.0
Classify turns any accepted input into the canonical (type, id), so `ant resolve` and `ant url` touch no network.
func (Domain) Info ¶ added in v0.2.0
func (Domain) Info() kit.DomainInfo
Info describes the scheme, the hostnames a pasted link is matched against, and the identity reused for the binary's help and version.
type Maker ¶ added in v0.2.0
type Maker struct {
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
}
Maker is the embedded reference to a person who made a post.
type Media ¶ added in v0.2.0
type Media struct {
Type string `json:"type,omitempty"` // image, video
URL string `json:"url,omitempty"`
VideoURL string `json:"video_url,omitempty"`
}
Media is one image or video in a post's gallery or its thumbnail.
type Post ¶ added in v0.2.0
type Post struct {
ID string `json:"id" kit:"id"` // the numeric post id, e.g. "1173164"
Name string `json:"name,omitempty" table:",truncate"`
Tagline string `json:"tagline,omitempty" table:",truncate"`
Slug string `json:"slug,omitempty"`
Description string `json:"description,omitempty" table:",truncate" kit:"body"`
Votes int `json:"votes,omitempty"`
Comments int `json:"comments,omitempty"`
ReviewsCount int `json:"reviews_count,omitempty" table:"-"`
ReviewsRating float64 `json:"reviews_rating,omitempty" table:"-"`
Featured bool `json:"featured,omitempty" table:"-"`
FeaturedAt string `json:"featured_at,omitempty" table:"-"`
CreatedAt string `json:"created_at,omitempty"`
Website string `json:"website,omitempty" table:"-"` // the outbound product URL
Thumbnail string `json:"thumbnail,omitempty" table:"-"`
Media []Media `json:"media,omitempty" table:"-"`
ProductLinks []ProductLink `json:"product_links,omitempty" table:"-"`
Topics []TopicLink `json:"topics,omitempty" table:"-"`
Hunter string `json:"hunter,omitempty" table:"-"` // the hunter's username
HunterName string `json:"hunter_name,omitempty" table:"-"`
Makers []Maker `json:"makers,omitempty" table:"-"`
URL string `json:"url"` // the discussion page
CommentsRef string `json:"comments_ref,omitempty" table:"-" kit:"link,kind=producthunt/comments"` // = ID
HunterRef string `json:"hunter_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"` // hunter username
MakerRefs []string `json:"maker_refs,omitempty" table:"-" kit:"link,kind=producthunt/user"` // maker usernames
TopicRefs []string `json:"topic_refs,omitempty" table:"-" kit:"link,kind=producthunt/topic"` // topic slugs
}
Post is the core record: a product launch. The id is the numeric post id, the same id the website and the Atom feed use, so a web read and an API read address the same record.
type ProductLink ¶ added in v0.2.0
type ProductLink struct {
Type string `json:"type,omitempty"` // website, app_store, play_store, github, ...
URL string `json:"url,omitempty"`
}
ProductLink is one outbound link a post carries (website, app store, repo).
type Ref ¶ added in v0.2.0
type Ref struct {
Input string `json:"input"`
Kind string `json:"kind"`
ID string `json:"id"`
URL string `json:"url"`
}
Ref is the result of `ph ref id`: the canonical (kind, id) a reference resolves to, plus the URL, all without touching the network.
type Topic ¶ added in v0.2.0
type Topic struct {
ID string `json:"id" kit:"id"`
Name string `json:"name,omitempty"`
Slug string `json:"slug,omitempty"`
Description string `json:"description,omitempty" table:",truncate" kit:"body"`
Followers int `json:"followers,omitempty"`
PostsCount int `json:"posts_count,omitempty"`
Image string `json:"image,omitempty" table:"-"`
CreatedAt string `json:"created_at,omitempty" table:"-"`
URL string `json:"url"`
PostRefs []string `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"` // its top posts
}
Topic is a subject tag a post can carry, emitted by topics and topic. PostRefs is its top posts, each a walkable post edge.
type TopicLink ¶ added in v0.2.0
type TopicLink struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Slug string `json:"slug,omitempty"`
}
TopicLink is the embedded reference to a topic inside a post or a collection.
type User ¶ added in v0.2.0
type User struct {
ID string `json:"id" kit:"id"`
Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
Headline string `json:"headline,omitempty" table:",truncate"`
Twitter string `json:"twitter,omitempty" table:"-"`
Website string `json:"website,omitempty" table:"-"`
Image string `json:"image,omitempty" table:"-"`
Cover string `json:"cover,omitempty" table:"-"`
CreatedAt string `json:"created_at,omitempty" table:"-"`
URL string `json:"url"`
PostRefs []string `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"` // posts they made
}
User is a Product Hunt member, emitted by user. PostRefs is the posts they made.