api

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package api is the LinkedIn Voyager client core. Voyager is LinkedIn's INTERNAL, unofficial web API (the same endpoints linkedin.com's SPA calls). Auth is a borrowed browser session: the li_at session cookie + the JSESSIONID cookie, with a csrf-token header derived from JSESSIONID. Plain net/http is sufficient for read Voyager — no TLS-fingerprint spoofing. The client sends the exact header set the web client sends, paces requests for ban-safety, and never retries a throttle signal.

Index

Constants

View Source
const (
	// DefaultVoyagerBaseURL is the authenticated internal API base.
	DefaultVoyagerBaseURL = "https://www.linkedin.com/voyager/api"
	// DefaultWebBaseURL is the host for the unauthenticated jobs-guest geo typeahead.
	DefaultWebBaseURL = "https://www.linkedin.com"

	// DefaultUserAgent is a CURRENT desktop Chrome UA. Voyager rejects a bare Go UA, and a
	// STALE UA is itself a fingerprint that looks automated — keep this single constant fresh.
	// Overridable via LINKEDIN_USER_AGENT.
	DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
		"(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
)

Default hosts. Both are overridable (config/flags, and tests point them at one httptest server that routes by path).

View Source
const GeoIDPlaceholder = "<GEO_ID>"

GeoIDPlaceholder stands in for a resolved geoId during a --location dry-run, where the geo typeahead can't run offline. It is kept literal (not percent-encoded) in the previewed job-search curl so a human sees locationUnion:(geoId:<GEO_ID>).

View Source
const StatusSoftBlock = 999

StatusSoftBlock is LinkedIn's non-standard "999" soft-block status. It is not a real HTTP status; LinkedIn returns it when it throttles or challenges an unofficial client. It must NEVER be retried — hammering a soft-block is exactly how an account gets flagged.

Variables

This section is empty.

Functions

func ParseSinceSeconds

func ParseSinceSeconds(value string) (int, error)

ParseSinceSeconds converts a --since window into seconds for timePostedRange:List(r<secs>). It accepts a relative shorthand Nh/Nd/Nw (e.g. 24h, 7d, 2w) or the named windows day/week/month. An empty value returns 0 (any time). The three canonical LinkedIn windows are r86400 (24h), r604800 (7d) and r2592000 (30d).

Types

type APIError

type APIError struct {
	StatusCode int
	Message    string
	Body       []byte
	// Challenge is set when LinkedIn returned a checkpoint/challenge (a security-verification
	// interstitial), signalling the session needs human re-verification in a browser.
	Challenge bool
}

APIError is a LinkedIn Voyager error with an actionable, ban-aware hint keyed by status.

func (*APIError) Error

func (e *APIError) Error() string

type Bool

type Bool bool

Bool accepts a real bool OR "true"/"1"/"yes". LinkedIn's workRemoteAllowed is a real bool, but some flags arrive as strings.

func (*Bool) UnmarshalJSON

func (v *Bool) UnmarshalJSON(b []byte) error

type Client

type Client struct {

	// DryRun prints the equivalent curl to DryRunOut instead of sending the request.
	DryRun    bool
	DryRunOut io.Writer
	// ShowToken reveals the session cookies in dry-run output (redacted by default).
	ShowToken bool

	Verbose    bool
	VerboseOut io.Writer
	// contains filtered or unexported fields
}

Client is a LinkedIn Voyager HTTP client.

func New

func New(voyagerBase, webBase string, opts ...Option) *Client

New builds a Voyager client. Empty bases fall back to the defaults.

func NewClientWithBaseURL

func NewClientWithBaseURL(base string, opts ...Option) *Client

NewClientWithBaseURL points both hosts at the same base URL. Tests drive every endpoint against one httptest server (routing by path); it also backs a single-host --base-url override.

func (*Client) Do

func (c *Client) Do(ctx context.Context, path string, q url.Values) (int, json.RawMessage, error)

Do sends one raw authenticated Voyager request and returns status, headers, and body — the escape hatch behind `linkedin api`. Only GET is ever sent for read Voyager; method is validated by the caller. A dry-run returns status 0.

func (*Client) GetCompany

func (c *Client) GetCompany(ctx context.Context, slug string) (json.RawMessage, error)

GetCompany fetches an organization by its universalName (the slug in a company URL, linkedin.com/company/<slug>) and returns the trustworthy Company entity's raw JSON.

func (*Client) GetJob

func (c *Client) GetJob(ctx context.Context, id string, now time.Time) (json.RawMessage, error)

GetJob fetches one job posting's full detail and returns the trustworthy JobPosting entity's raw JSON. It CHARGES the daily job-detail cap first (ban-safety): if today's budget is spent, it refuses rather than fetching. now is injected for deterministic tests.

func (*Client) Pacer

func (c *Client) Pacer() *Pacer

Pacer returns the installed pacer (may be nil).

func (*Client) ResolveGeo

func (c *Client) ResolveGeo(ctx context.Context, name string) ([]GeoHit, error)

ResolveGeo resolves a location name to LinkedIn geo hits via the unauthenticated jobs-guest typeahead. It returns every hit (the first is the best match). "remote" is NOT a geo — it is a workplaceType filter — so callers must not route "remote" through here.

func (*Client) SearchJobs

func (c *Client) SearchJobs(ctx context.Context, f SearchFilters, start, count int) (*SearchResult, error)

SearchJobs runs one page of a job search (start/count) and returns the parsed page + raw JSON.

func (*Client) SearchJobsAll

func (c *Client) SearchJobsAll(ctx context.Context, f SearchFilters, count, limit int, all bool) ([]voyager.JobCard, error)

SearchJobsAll walks pages until it collects `limit` cards (limit<=0 && !all → one page), pacing every request via the ban-safety Pacer. It de-duplicates by job id as a safety net.

func (*Client) VoyagerBaseURL

func (c *Client) VoyagerBaseURL() string

VoyagerBaseURL returns the resolved Voyager host.

func (*Client) WebBaseURL

func (c *Client) WebBaseURL() string

WebBaseURL returns the resolved web (typeahead) host.

type CookieFunc

type CookieFunc func(ctx context.Context) (liAt, jsessionID string, err error)

CookieFunc supplies the borrowed session cookies per request: li_at (session) and JSESSIONID (whose value looks like `"ajax:1234567890"` WITH the surrounding quotes). It may be nil for the unauthenticated typeahead path.

type GeoCache

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

GeoCache is a name→geoId cache persisted in the config dir, so a repeated --location resolves without another typeahead round-trip (fewer requests = safer). Keys are lowercased names.

func NewGeoCache

func NewGeoCache(dir string) *GeoCache

NewGeoCache loads (or starts) the cache at dir/geocache.json.

func (*GeoCache) Get

func (g *GeoCache) Get(name string) (string, bool)

Get returns a cached geoId for name (and whether it was present).

func (*GeoCache) Put

func (g *GeoCache) Put(name, geoID string)

Put stores name→geoId and persists the cache atomically.

type GeoHit

type GeoHit struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

GeoHit is one geo typeahead result.

type ID

type ID string

ID unmarshals from a JSON string OR number and always marshals as a string, so ids render consistently and never lose precision above 2^53.

func (ID) MarshalJSON

func (id ID) MarshalJSON() ([]byte, error)

func (ID) String

func (id ID) String() string

func (*ID) UnmarshalJSON

func (id *ID) UnmarshalJSON(b []byte) error

type Int

type Int int64

Int accepts a JSON number OR a numeric string, decoding int64 before float64 so ids above 2^53 keep precision. NaN/Inf and malformed numbers are rejected.

func (Int) Int64

func (n Int) Int64() int64

func (*Int) UnmarshalJSON

func (n *Int) UnmarshalJSON(b []byte) error

type Option

type Option func(*Client)

Option configures a Client.

func WithCookies

func WithCookies(f CookieFunc) Option

WithCookies sets the borrowed-session cookie source.

func WithDryRun

func WithDryRun(dry bool, out io.Writer) Option

WithDryRun enables curl-printing mode.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient overrides the HTTP transport (tests point it at httptest servers).

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries overrides the transient-error retry budget (tests set 0 for speed).

func WithPacer

func WithPacer(p *Pacer) Option

WithPacer installs the ban-safety pacer. Without one, requests are not paced (tests default to no pacer for speed).

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent overrides the default User-Agent (empty keeps the default).

type Pacer

type Pacer struct {
	MinDelay  time.Duration // lower bound of the inter-request jitter
	MaxDelay  time.Duration // upper bound of the inter-request jitter
	PerRunCap int           // max live requests in one process run (0 = unlimited)
	DailyCap  int           // max job-detail fetches charged per calendar day (0 = unlimited)
	StatePath string        // JSON file persisting the daily counter (config/state dir)
	// contains filtered or unexported fields
}

Pacer enforces the ban-safety posture that is ON BY DEFAULT (not an option): one request in flight at a time, a jittered human-paced delay between requests, a per-run request cap, and a persisted per-DAY cap on job-detail fetches. These defaults exist because this CLI drives LinkedIn's unofficial Voyager API against a real personal account — the biggest risk isn't a bug, it's an account restriction from looking automated. See DECISIONS.md.

func DefaultPacer

func DefaultPacer(statePath string) *Pacer

DefaultPacer returns the shipped ban-safety defaults: a 3–15s jittered delay, no per-run cap, and ~30 job-detail fetches/day. statePath is where the daily counter persists.

func (*Pacer) ChargeDaily

func (p *Pacer) ChargeDaily(now time.Time) error

ChargeDaily records one job-detail fetch against today's cap, persisting the counter. It returns an error (and does NOT increment) when the cap is already reached, so `jobs get` refuses to exceed the daily budget rather than silently blowing past it. now is injected for deterministic tests.

func (*Pacer) DailyRemaining

func (p *Pacer) DailyRemaining(now time.Time) int

DailyRemaining reports how many job-detail fetches are left today (for doctor/status).

func (*Pacer) Wait

func (p *Pacer) Wait(ctx context.Context) error

Wait blocks for a jittered delay before a live request and enforces the per-run cap. It is a no-op the FIRST time (no delay before the first request of a run) so a single command stays snappy; the pacing applies between successive requests. Returns an error if the per-run cap is exceeded, so a runaway loop stops rather than pounding LinkedIn.

type SearchFilters

type SearchFilters struct {
	Keywords   string   // free-text role/skill match
	GeoID      string   // resolved location geoId (see ResolveGeo); "" = anywhere
	Remote     bool     // → workplaceType:List(2)
	SinceSecs  int      // → timePostedRange:List(r<secs>); 0 = any time
	JobType    []string // → jobType:List(F,C,…)
	Experience []string // → experience:List(2,3,…)
}

SearchFilters are the user-facing job-search filters. They compile into LinkedIn's Rest.li `query=(...)` blob (see buildSearchQuery). Zero-valued fields are omitted.

type SearchResult

type SearchResult struct {
	*voyager.SearchResult
	Raw json.RawMessage
}

SearchResult is one parsed page plus the raw envelope (for -o json).

type StringOrSlice

type StringOrSlice []string

StringOrSlice accepts a single string OR an array of strings — LinkedIn's workplaceTypes is an array, but adjacent fields are sometimes a scalar.

func (*StringOrSlice) UnmarshalJSON

func (s *StringOrSlice) UnmarshalJSON(b []byte) error

Jump to

Keyboard shortcuts

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