package module
Version: v0.0.0-...-1811474 Latest Latest

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

Go to latest
Published: Mar 31, 2022 License: MIT Imports: 17 Imported by: 43



build status codecov Go Report Card GoDoc MIT licensed

go-itchio is a set of Go bindings to interact with the API


Licensed under MIT License, see LICENSE for details.




This section is empty.


View Source
var (
	// ErrBuildFileNotFound is returned when someone is asking for a non-existent file
	ErrBuildFileNotFound = errors.New("build file not found in storage")


func DefaultRateLimiter

func DefaultRateLimiter() *rate.Limiter

DefaultRateLimiter returns a rate.Limiter suitable for consuming the API. It is shared across all instances of Client, unless a custom limiter is set.

func GameHookFunc

func GameHookFunc(
	f reflect.Type,
	t reflect.Type,
	data interface{}) (interface{}, error)

GameHookFunc is used to transform API results from what they currently are to what we expect them to be.

func IsAPIError

func IsAPIError(err error) bool

IsAPIError returns true if an error is an API error, even if it's wrapped with

func ParseAPIResponse

func ParseAPIResponse(dst interface{}, res *http.Response) error

ParseAPIResponse unmarshals an HTTP response into one of out response data structures

func UploadHookFunc

func UploadHookFunc(
	f reflect.Type,
	t reflect.Type,
	data interface{}) (interface{}, error)

UploadHookFunc is used to transform API results from what they currently are to what we expect them to be.


type APIError

type APIError struct {
	Messages   []string `json:"messages"`
	StatusCode int      `json:"statusCode"`
	Path       string   `json:"path"`

APIError represents an API error. Some errors are just HTTP status codes, others have more detailed messages.

func AsAPIError

func AsAPIError(err error) (*APIError, bool)

AsAPIError returns an *APIError and true if the passed error (no matter how deeply wrapped it is) is an *APIError. Otherwise it returns nil, false.

func (*APIError) Error

func (ae *APIError) Error() string

type APIKey

type APIKey struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`

	// ID of the user to which the key belongs
	UserID int64 `json:"userId"`

	// Actual API key value
	Key string `json:"key"`

	CreatedAt     *time.Time `json:"createdAt"`
	UpdatedAt     *time.Time `json:"updatedAt"`
	SourceVersion string     `json:"sourceVersion"`

An APIKey grants access to the API within a certain scope.

type Architectures

type Architectures string

Architectures describes a set of processor architectures (mostly 32-bit vs 64-bit)

const (
	// ArchitecturesAll represents any processor architecture
	ArchitecturesAll Architectures = "all"
	// Architectures386 represents 32-bit processor architectures
	Architectures386 Architectures = "386"
	// ArchitecturesAmd64 represents 64-bit processor architectures
	ArchitecturesAmd64 Architectures = "amd64"

type Build

type Build struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`
	// Identifier of the build before this one on the same channel,
	// or 0 if this is the initial build.
	ParentBuildID int64 `json:"parentBuildId"`
	// State of the build: started, processing, etc.
	State BuildState `json:"state"`

	// Automatically-incremented version number, starting with 1
	Version int64 `json:"version"`
	// Value specified by developer with `--userversion` when pushing a build
	// Might not be unique across builds of a given channel.
	UserVersion string `json:"userVersion"`

	// Files associated with this build - often at least an archive,
	// a signature, and a patch. Some might be missing while the build
	// is still processing or if processing has failed.
	Files []*BuildFile `json:"files"`

	// User who pushed the build
	User *User `json:"user"`
	// Timestamp the build was created at
	CreatedAt *time.Time `json:"createdAt"`
	// Timestamp the build was last updated at
	UpdatedAt *time.Time `json:"updatedAt"`

Build contains information about a specific build

type BuildEvent

type BuildEvent struct {
	Type    BuildEventType `json:"type"`
	Message string         `json:"message"`
	Data    BuildEventData `json:"data"`

A BuildEvent describes something that happened while we were processing a build.

type BuildEventData

type BuildEventData map[string]interface{}

BuildEventData is a JSON object associated with a build event

type BuildEventType

type BuildEventType string

BuildEventType specifies what kind of event a build event is - could be a log message, etc.

const (
	// BuildEventLog is for build events of type log message
	BuildEventLog BuildEventType = "log"

type BuildFile

type BuildFile struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`
	// Size of this build file
	Size int64 `json:"size"`
	// State of this file: created, uploading, uploaded, etc.
	State BuildFileState `json:"state"`
	// Type of this build file: archive, signature, patch, etc.
	Type BuildFileType `json:"type"`
	// Subtype of this build file, usually indicates compression
	SubType BuildFileSubType `json:"subType"`

	// Date this build file was created at
	CreatedAt *time.Time `json:"createdAt"`
	// Date this build file was last updated at
	UpdatedAt *time.Time `json:"updatedAt"`

BuildFile contains information about a build's "file", which could be its archive, its signature, its patch, etc.

func FindBuildFile

func FindBuildFile(fileType BuildFileType, files []*BuildFile) *BuildFile

FindBuildFile looks for an uploaded file of the right type in a list of file. Returns nil if it can't find one.

func FindBuildFileEx

func FindBuildFileEx(fileType BuildFileType, fileSubType BuildFileSubType, files []*BuildFile) *BuildFile

FindBuildFileEx looks for an uploaded file of the right type and subtype in a list of file. Returns nil if it can't find one.

type BuildFileState

type BuildFileState string

BuildFileState describes the state of a specific file for a build

const (
	// BuildFileStateCreated means the file entry exists on
	BuildFileStateCreated BuildFileState = "created"
	// BuildFileStateUploading means the file is currently being uploaded to storage
	BuildFileStateUploading BuildFileState = "uploading"
	// BuildFileStateUploaded means the file is ready
	BuildFileStateUploaded BuildFileState = "uploaded"
	// BuildFileStateFailed means the file failed uploading
	BuildFileStateFailed BuildFileState = "failed"

type BuildFileSubType

type BuildFileSubType string

BuildFileSubType describes the subtype of a build file: mostly its compression level. For example, rediff'd patches are "optimized", whereas initial patches are "default"

const (
	// BuildFileSubTypeDefault describes default compression (rsync patches)
	BuildFileSubTypeDefault BuildFileSubType = "default"
	// BuildFileSubTypeGzip is reserved
	BuildFileSubTypeGzip BuildFileSubType = "gzip"
	// BuildFileSubTypeOptimized describes optimized compression (rediff'd / bsdiff patches)
	BuildFileSubTypeOptimized BuildFileSubType = "optimized"

type BuildFileType

type BuildFileType string

BuildFileType describes the type of a build file: patch, archive, signature, etc.

const (
	// BuildFileTypePatch describes wharf patch files (.pwr)
	BuildFileTypePatch BuildFileType = "patch"
	// BuildFileTypeArchive describes canonical archive form (.zip)
	BuildFileTypeArchive BuildFileType = "archive"
	// BuildFileTypeSignature describes wharf signature files (.pws)
	BuildFileTypeSignature BuildFileType = "signature"
	// BuildFileTypeManifest is reserved
	BuildFileTypeManifest BuildFileType = "manifest"
	// BuildFileTypeUnpacked describes the single file that is in the build (if it was just a single file)
	BuildFileTypeUnpacked BuildFileType = "unpacked"

type BuildState

type BuildState string

BuildState describes the state of a build, relative to its initial upload, and its processing.

const (
	// BuildStateStarted is the state of a build from its creation until the initial upload is complete
	BuildStateStarted BuildState = "started"
	// BuildStateProcessing is the state of a build from the initial upload's completion to its fully-processed state.
	// This state does not mean the build is actually being processed right now, it's just queued for processing.
	BuildStateProcessing BuildState = "processing"
	// BuildStateCompleted means the build was successfully processed. Its patch hasn't necessarily been
	// rediff'd yet, but we have the holy (patch,signature,archive) trinity.
	BuildStateCompleted BuildState = "completed"
	// BuildStateFailed means something went wrong with the build. A failing build will not update the channel
	// head and can be requeued by the team, although if a new build is pushed before they do,
	// that new build will "win".
	BuildStateFailed BuildState = "failed"

type Channel

type Channel struct {
	// Name of the channel, usually something like `windows-64-beta` or `osx-universal`
	Name string `json:"name"`
	Tags string `json:"tags"`

	Upload  *Upload `json:"upload"`
	Head    *Build  `json:"head"`
	Pending *Build  `json:"pending"`

Channel contains information about a channel and its current status

type Client

type Client struct {
	Key              string
	HTTPClient       *http.Client
	BaseURL          string
	RetryPatterns    []time.Duration
	UserAgent        string
	AcceptedLanguage string
	Limiter          *rate.Limiter
	// contains filtered or unexported fields

A Client allows consuming the API

func ClientWithKey

func ClientWithKey(key string) *Client

ClientWithKey creates a new API client with a given API key

func (*Client) CreateBuild

func (c *Client) CreateBuild(ctx context.Context, p CreateBuildParams) (*CreateBuildResponse, error)

CreateBuild creates a new build for a given user/game:channel, with an optional user version

func (*Client) CreateBuildEvent

CreateBuildEvent associates a new build event to a build

func (*Client) CreateBuildFailure

CreateBuildFailure marks a given build as failed. We get to specify an error message and if it's a fatal error (if not, the build can be retried after a bit)

func (*Client) CreateBuildFile

CreateBuildFile creates a new build file for a build.

func (*Client) CreateRediffBuildFailure

CreateRediffBuildFailure marks a given build as having failed to rediff (optimize)

func (*Client) CreateUserGameSession

CreateUserGameSession creates a session for a user/game. It can be later updated.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do performs a request (any method). It takes care of JWT or API key authentication, sets the proper user agent, has built-in retry,

func (*Client) FinalizeBuildFile

FinalizeBuildFile marks the end of the upload for a build file. It validates that the size of the file in storage is the same we pass to this API call.

func (*Client) Get

func (c *Client) Get(ctx context.Context, url string) (*http.Response, error)

Get performs an HTTP GET request to the API

func (*Client) GetBuild

func (c *Client) GetBuild(ctx context.Context, p GetBuildParams) (*GetBuildResponse, error)

GetBuild retrieves info about a single build, by ID.

func (*Client) GetBuildScannedArchive

func (c *Client) GetBuildScannedArchive(ctx context.Context, p GetBuildScannedArchiveParams) (*GetScannedArchiveResponse, error)

func (*Client) GetBuildUpgradePath

GetBuildUpgradePath returns the complete list of builds one needs to go through to go from one version to another. It only works when upgrading (at the time of this writing).

func (*Client) GetChannel

func (c *Client) GetChannel(ctx context.Context, target string, channel string) (*GetChannelResponse, error)

GetChannel returns information about a given channel for a given game

func (*Client) GetCollection

func (c *Client) GetCollection(ctx context.Context, params GetCollectionParams) (*GetCollectionResponse, error)

GetCollection retrieves a single collection by ID.

func (*Client) GetCollectionGames

func (c *Client) GetCollectionGames(ctx context.Context, params GetCollectionGamesParams) (*GetCollectionGamesResponse, error)

GetCollectionGames retrieves a page of a collection's games.

func (*Client) GetGame

func (c *Client) GetGame(ctx context.Context, p GetGameParams) (*GetGameResponse, error)

GetGame retrieves a single game by ID.

func (*Client) GetGameSessionsSummary

func (c *Client) GetGameSessionsSummary(ctx context.Context, gameID int64) (*GetGameSessionsSummaryResponse, error)

GetGameSessionsSummary returns a summary of game sessions for a given game.

func (*Client) GetProfile

func (c *Client) GetProfile(ctx context.Context) (*GetProfileResponse, error)

GetProfile returns information about the user the current credentials belong to

func (*Client) GetResponse

func (c *Client) GetResponse(ctx context.Context, url string, dst interface{}) error

GetResponse performs an HTTP GET request and parses the API response.

func (*Client) GetUpload

func (c *Client) GetUpload(ctx context.Context, params GetUploadParams) (*GetUploadResponse, error)

GetUpload retrieves information about a single upload, by ID.

func (*Client) GetUploadScannedArchive

func (c *Client) GetUploadScannedArchive(ctx context.Context, p GetUploadScannedArchiveParams) (*GetScannedArchiveResponse, error)

func (*Client) GetUser

func (c *Client) GetUser(ctx context.Context, p GetUserParams) (*GetUserResponse, error)

GetUser retrieves info about a single user, by ID.

func (*Client) ListBuildEvents

func (c *Client) ListBuildEvents(ctx context.Context, buildID int64) (*ListBuildEventsResponse, error)

ListBuildEvents returns a series of events associated with a given build

func (*Client) ListBuildFiles

func (c *Client) ListBuildFiles(ctx context.Context, buildID int64) (*ListBuildFilesResponse, error)

ListBuildFiles returns a list of files associated to a build

func (*Client) ListChannels

func (c *Client) ListChannels(ctx context.Context, target string) (*ListChannelsResponse, error)

ListChannels returns a list of the channels for a game

func (*Client) ListGameUploads

ListGameUploads lists the uploads for a game that we have access to with our API key and game credentials.

func (*Client) ListProfileCollections

func (c *Client) ListProfileCollections(ctx context.Context) (*ListProfileCollectionsResponse, error)

ListProfileCollections lists the collections associated to a profile.

func (*Client) ListProfileGames

func (c *Client) ListProfileGames(ctx context.Context) (*ListProfileGamesResponse, error)

ListProfileGames lists the games one develops (ie. can edit)

func (*Client) ListProfileOwnedKeys

ListProfileOwnedKeys lists the download keys the account with the current API key owns.

func (*Client) ListUploadBuilds

func (c *Client) ListUploadBuilds(ctx context.Context, params ListUploadBuildsParams) (*ListUploadBuildsResponse, error)

ListUploadBuilds lists recent builds for a given upload, by ID.

func (*Client) LoginWithPassword

func (c *Client) LoginWithPassword(ctx context.Context, params LoginWithPasswordParams) (*LoginWithPasswordResponse, error)

LoginWithPassword attempts to log a user into with their username (or e-mail) and password. The response may indicate that a TOTP code is needed (for two-factor auth), or a recaptcha challenge is needed (an unfortunate remedy for an unfortunate ailment).

func (*Client) MakeBuildDownloadURL

func (c *Client) MakeBuildDownloadURL(p MakeBuildDownloadURLParams) string

MakeBuildDownloadURL generates as download URL for a specific build

func (*Client) MakeBuildFileDownloadURL

func (c *Client) MakeBuildFileDownloadURL(p MakeBuildFileDownloadURLParams) string

MakeBuildFileDownloadURL returns a download URL for a given build file

func (*Client) MakePath

func (c *Client) MakePath(format string, a ...interface{}) string

MakePath crafts an API url from our configured base URL

func (*Client) MakeUploadDownloadURL

func (c *Client) MakeUploadDownloadURL(p MakeUploadDownloadURLParams) string

MakeUploadDownloadURL generates a download URL for an upload

func (*Client) MakeValuesPath

func (c *Client) MakeValuesPath(values url.Values, format string, a ...interface{}) string

MakeValuesPath crafts an API url from our configured base URL

func (*Client) NewDownloadSession

NewDownloadSession creates a new download session. It is used for more accurate download analytics. Downloading multiple patch and signature files may all be part of the same "download session": upgrading a game to its latest version. It should only count as one download.

func (*Client) OnOutgoingRequest

func (c *Client) OnOutgoingRequest(cb OnOutgoingRequest)

OnOutgoingRequest allows registering a function that gets called every time the client makes an HTTP request

func (*Client) OnRateLimited

func (c *Client) OnRateLimited(cb OnRateLimited)

OnRateLimited allows registering a function that gets called every time the server responds with 503

func (*Client) PostForm

func (c *Client) PostForm(ctx context.Context, url string, data url.Values) (*http.Response, error)

PostForm performs an HTTP POST request to the API, with url-encoded parameters

func (*Client) PostFormResponse

func (c *Client) PostFormResponse(ctx context.Context, url string, data url.Values, dst interface{}) error

PostFormResponse performs an HTTP POST request to the API *and* parses the API response.

func (*Client) SearchGames

func (c *Client) SearchGames(ctx context.Context, params SearchGamesParams) (*SearchGamesResponse, error)

SearchGames performs a text search for games (or any project type). The games must be published, and not deindexed. There are a bunch of subtleties about visibility and ranking, but that's internal.

func (*Client) SearchUsers

func (c *Client) SearchUsers(ctx context.Context, params SearchUsersParams) (*SearchUsersResponse, error)

SearchUsers performs a text search for users.

func (*Client) SetServer

func (c *Client) SetServer(itchioServer string) *Client

SetServer allows changing the server to which we're making API requests (which defaults to the reference server)

func (*Client) Subkey

func (c *Client) Subkey(ctx context.Context, params SubkeyParams) (*SubkeyResponse, error)

Subkey creates a scoped-down, temporary offspring of the main API key this client was created with. It is useful to automatically grant some access to games being launched.

func (*Client) TOTPVerify

func (c *Client) TOTPVerify(ctx context.Context, params TOTPVerifyParams) (*TOTPVerifyResponse, error)

TOTPVerify sends a user-entered TOTP token to the server for verification (and to complete login).

func (*Client) UpdateUserGameSession

UpdateUserGameSession updates an existing user+game session with a new duration and timestamp.

func (*Client) WharfStatus

func (c *Client) WharfStatus(ctx context.Context) (*WharfStatusResponse, error)

WharfStatus requests the status of the wharf infrastructure

type Collection

type Collection struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`

	// Human-friendly title for collection, for example `Couch coop games`
	Title string `json:"title"`

	// Date this collection was created at
	CreatedAt *time.Time `json:"createdAt"`
	// Date this collection was last updated at (item added, title set, etc.)
	UpdatedAt *time.Time `json:"updatedAt"`

	// Number of games in the collection. This might not be accurate
	// as some games might not be accessible to whoever is asking (project
	// page deleted, visibility level changed, etc.)
	GamesCount int64 `json:"gamesCount"`

	// Games in this collection, with additional info
	CollectionGames []*CollectionGame `json:"collectionGames,omitempty"`

	UserID int64 `json:"userId"`
	User   *User `json:"user,omitempty"`

A Collection is a set of games, curated by humans.

type CollectionGame

type CollectionGame struct {
	CollectionID int64       `json:"collectionId" hades:"primary_key"`
	Collection   *Collection `json:"collection,omitempty"`

	GameID int64 `json:"gameId" hades:"primary_key"`
	Game   *Game `json:"game,omitempty"`

	Position int64 `json:"position"`

	CreatedAt *time.Time `json:"createdAt"`
	UpdatedAt *time.Time `json:"updatedAt"`

	Blurb  string `json:"blurb"`
	UserID int64  `json:"userId"`

CollectionGame represents a game's membership for a collection.

type Cookie map[string]string

Cookie represents, well, multiple key=value pairs that should be set to obtain a logged-in browser session for the user who just logged in.

type CreateBuildEventParams

type CreateBuildEventParams struct {
	BuildID int64
	Type    BuildEventType
	Message string
	Data    BuildEventData

CreateBuildEventParams : params for CreateBuildEvent

type CreateBuildEventResponse

type CreateBuildEventResponse struct{}

CreateBuildEventResponse is what the API responds with when you create a new build event

type CreateBuildFailureParams

type CreateBuildFailureParams struct {
	BuildID int64
	Message string
	Fatal   bool

CreateBuildFailureParams : params for CreateBuildFailure

type CreateBuildFailureResponse

type CreateBuildFailureResponse struct{}

CreateBuildFailureResponse : response for CreateBuildFailure

type CreateBuildFileParams

type CreateBuildFileParams struct {
	BuildID        int64
	Type           BuildFileType
	SubType        BuildFileSubType
	FileUploadType FileUploadType
	Filename       string

CreateBuildFileParams : params for CreateBuildFile

type CreateBuildFileResponse

type CreateBuildFileResponse struct {
	File *FileUploadSpec `json:"file"`

CreateBuildFileResponse : response for CreateBuildFile

type CreateBuildParams

type CreateBuildParams struct {
	Target      string
	Channel     string
	UserVersion string

CreateBuildParams : params for CreateBuild

type CreateBuildResponse

type CreateBuildResponse struct {
	Build struct {
		ID          int64 `json:"id"`
		UploadID    int64 `json:"uploadId"`
		ParentBuild struct {
			ID int64 `json:"id"`
		} `json:"parentBuild"`

CreateBuildResponse : response for CreateBuild

type CreateRediffBuildFailureParams

type CreateRediffBuildFailureParams struct {
	BuildID int64
	Message string

CreateRediffBuildFailureParams : params for CreateRediffBuildFailure

type CreateRediffBuildFailureResponse

type CreateRediffBuildFailureResponse struct{}

CreateRediffBuildFailureResponse : response for CreateRediffBuildFailure

type CreateUserGameSessionParams

type CreateUserGameSessionParams struct {
	// ID of the game this session is for
	GameID int64
	// Time the game has run (so far), in seconds
	SecondsRun int64
	// End of the session (so far). This is not the same
	// as the request time, because the session may be "uploaded"
	// later than it is being recorded. This happens especially
	// if the session was recorded when offline.
	LastRunAt *time.Time
	// Upload being run this session
	UploadID int64
	// Optional (if the upload is not wharf-enabled): build being run this session
	BuildID int64

	Platform     SessionPlatform
	Architecture SessionArchitecture

	// Download key etc., in case this is a paid game
	Credentials GameCredentials

CreateUserGameSessionParams : params for CreateUserGameSession

type CreateUserGameSessionResponse

type CreateUserGameSessionResponse struct {
	// A summary of interactions for this user+game
	Summary *UserGameInteractionsSummary `json:"summary"`
	// The freshly-created game session
	UserGameSession *UserGameSession `json:"userGameSession"`

CreateUserGameSessionResponse : response for CreateUserGameSession

type DownloadBuildFileResponse

type DownloadBuildFileResponse struct {
	URL string `json:"url"`

DownloadBuildFileResponse is what the API responds with when we ask to download an upload

type DownloadKey

type DownloadKey struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`

	// Identifier of the game to which this download key grants access
	GameID int64 `json:"gameId"`

	// Game to which this download key grants access
	Game *Game `json:"game,omitempty"`

	// Date this key was created at (often coincides with purchase time)
	CreatedAt *time.Time `json:"createdAt"`
	// Date this key was last updated at
	UpdatedAt *time.Time `json:"updatedAt"`

	// Identifier of the user to which this key belongs
	OwnerID int64 `json:"ownerId"`

A DownloadKey is often generated when a purchase is made, it allows downloading uploads for a game that are not available for free. It can also be generated by other means.

type DownloadUploadBuildResponse

type DownloadUploadBuildResponse struct {
	// Patch is the download info for the wharf patch, if any
	Patch *DownloadUploadBuildResponseItem `json:"patch"`
	// Signature is the download info for the wharf signature, if any
	Signature *DownloadUploadBuildResponseItem `json:"signature"`
	// Manifest is reserved
	Manifest *DownloadUploadBuildResponseItem `json:"manifest"`
	// Archive is the download info for the .zip archive, if any
	Archive *DownloadUploadBuildResponseItem `json:"archive"`
	// Unpacked is the only file of the build, if it's a single file
	Unpacked *DownloadUploadBuildResponseItem `json:"unpacked"`

DownloadUploadBuildResponse is what the API responds when we want to download a build

type DownloadUploadBuildResponseItem

type DownloadUploadBuildResponseItem struct {
	URL string `json:"url"`

DownloadUploadBuildResponseItem contains download information for a specific build file

type FileUploadSpec

type FileUploadSpec struct {
	ID            int64             `json:"id"`
	UploadURL     string            `json:"uploadUrl"`
	UploadParams  map[string]string `json:"uploadParams"`
	UploadHeaders map[string]string `json:"uploadHeaders"`

FileUploadSpec contains the info needed to upload one specific build file

type FileUploadType

type FileUploadType string

FileUploadType describes which strategy is used for uploading to storage some types allow for uploading in blocks (which is resumable), some expect the whole payload in one request.

const (
	// FileUploadTypeMultipart lets you send metadata + all the content in a single request
	FileUploadTypeMultipart FileUploadType = "multipart"
	// FileUploadTypeResumable lets you send blocks of N*128KB at a time. The upload session is
	// started from the API server, so the ingest point will be anchored wherever the API server is.
	FileUploadTypeResumable FileUploadType = "resumable"
	// FileUploadTypeDeferredResumable also lets you send blocks of N*128KB at a time, but it
	// lets you start the upload session from the client, which means you might get a closer ingest point.
	FileUploadTypeDeferredResumable FileUploadType = "deferred_resumable"

type FinalizeBuildFileParams

type FinalizeBuildFileParams struct {
	BuildID int64
	FileID  int64
	Size    int64

FinalizeBuildFileParams : params for FinalizeBuildFile

type FinalizeBuildFileResponse

type FinalizeBuildFileResponse struct{}

FinalizeBuildFileResponse : response for FinalizeBuildFile

type Game

type Game struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`
	// Canonical address of the game's page on
	URL string `json:"url,omitempty"`

	// Human-friendly title (may contain any character)
	Title string `json:"title,omitempty"`
	// Human-friendly short description
	ShortText string `json:"shortText,omitempty"`
	// Downloadable game, html game, etc.
	Type GameType `json:"type,omitempty"`
	// Classification: game, tool, comic, etc.
	Classification GameClassification `json:"classification,omitempty"`

	// Configuration for embedded (HTML5) games
	// @optional
	Embed *GameEmbedData `json:"embed,omitempty"`

	// Cover url (might be a GIF)
	CoverURL string `json:"coverUrl,omitempty"`
	// Non-gif cover url, only set if main cover url is a GIF
	StillCoverURL string `json:"stillCoverUrl,omitempty"`

	// Date the game was created
	CreatedAt *time.Time `json:"createdAt,omitempty"`
	// Date the game was published, empty if not currently published
	PublishedAt *time.Time `json:"publishedAt,omitempty"`

	// Price in cents of a dollar
	MinPrice int64 `json:"minPrice,omitempty"`

	// Are payments accepted?
	CanBeBought bool `json:"canBeBought,omitempty"`

	// Does this game have a demo available?
	HasDemo bool `json:"hasDemo,omitempty"`

	// Is this game part of the press system?
	InPressSystem bool `json:"inPressSystem,omitempty"`

	// Platforms this game is available for
	Platforms Platforms `json:"platforms" hades:"squash"`

	// The user account this game is associated to
	// @optional
	User *User `json:"user,omitempty"`

	// ID of the user account this game is associated to
	UserID int64 `json:"userId,omitempty"`

	// The best current sale for this game
	// @optional
	Sale *Sale `json:"sale,omitempty"`

	ViewsCount     int64 `json:"viewsCount,omitempty" hades:"-"`
	DownloadsCount int64 `json:"downloadsCount,omitempty" hades:"-"`
	PurchasesCount int64 `json:"purchasesCount,omitempty" hades:"-"`

	Published bool `json:"published,omitempty" hades:"-"`

Game represents a page on, it could be a game, a tool, a comic, etc.

type GameClassification

type GameClassification string

GameClassification is the creator-picked classification for a page

const (
	// GameClassificationGame is something you can play
	GameClassificationGame GameClassification = "game"
	// GameClassificationTool includes all software pretty much
	GameClassificationTool GameClassification = "tool"
	// GameClassificationAssets includes assets: graphics, sounds, etc.
	GameClassificationAssets GameClassification = "assets"
	// GameClassificationGameMod are game mods (no link to game, purely creator tagging)
	GameClassificationGameMod GameClassification = "game_mod"
	// GameClassificationPhysicalGame is for a printable / board / card game
	GameClassificationPhysicalGame GameClassification = "physical_game"
	// GameClassificationSoundtrack is a bunch of music files
	GameClassificationSoundtrack GameClassification = "soundtrack"
	// GameClassificationOther is anything that creators think don't fit in any other category
	GameClassificationOther GameClassification = "other"
	// GameClassificationComic is a comic book (pdf, jpg, specific comic formats, etc.)
	GameClassificationComic GameClassification = "comic"
	// GameClassificationBook is a book (pdf, jpg, specific e-book formats, etc.)
	GameClassificationBook GameClassification = "book"

type GameCredentials

type GameCredentials struct {
	DownloadKeyID int64  `json:"downloadKeyId,omitempty"`
	Password      string `json:"password,omitempty"`
	Secret        string `json:"secret,omitempty"`

GameCredentials is your one-stop shop for all the things that allow access to a game or its uploads, such as: a download key, a password (for restricted pages), a secret (for private pages).

type GameEmbedData

type GameEmbedData struct {
	// Game this embed info is for
	GameID int64 `json:"gameId" hades:"primary_key"`

	// width of the initial viewport, in pixels
	Width int64 `json:"width"`

	// height of the initial viewport, in pixels
	Height int64 `json:"height"`

	// for website, whether or not a fullscreen button should be shown
	Fullscreen bool `json:"fullscreen"`

GameEmbedData contains presentation information for embed games

type GameType

type GameType string

GameType is the type of an game page, mostly related to how it should be presented on web (downloadable or embed)

const (
	// GameTypeDefault is downloadable games
	GameTypeDefault GameType = "default"
	// GameTypeFlash is for .swf (legacy)
	GameTypeFlash GameType = "flash"
	// GameTypeUnity is for .unity3d (legacy)
	GameTypeUnity GameType = "unity"
	// GameTypeJava is for .jar (legacy)
	GameTypeJava GameType = "java"
	// GameTypeHTML is for .html (thriving)
	GameTypeHTML GameType = "html"

type GetBuildParams

type GetBuildParams struct {
	BuildID int64

	// Optional
	Credentials GameCredentials

GetBuildParams : params for GetBuild

type GetBuildResponse

type GetBuildResponse struct {
	Build *Build `json:"build"`

GetBuildResponse : response for GetBuild

type GetBuildScannedArchiveParams

type GetBuildScannedArchiveParams struct {
	BuildID int64

	// Optional
	Credentials GameCredentials

type GetBuildUpgradePathParams

type GetBuildUpgradePathParams struct {
	CurrentBuildID int64
	TargetBuildID  int64

	// Optional
	Credentials GameCredentials

GetBuildUpgradePathParams : params for GetBuildUpgradePath

type GetBuildUpgradePathResponse

type GetBuildUpgradePathResponse struct {
	UpgradePath *UpgradePath `json:"upgradePath"`

GetBuildUpgradePathResponse : response for GetBuildUpgradePath

type GetChannelResponse

type GetChannelResponse struct {
	Channel *Channel `json:"channel"`

GetChannelResponse is what the API responds with when we ask info about a channel

type GetCollectionGamesParams

type GetCollectionGamesParams struct {
	CollectionID int64
	Page         int64

GetCollectionGamesParams : params for GetCollectionGames

type GetCollectionGamesResponse

type GetCollectionGamesResponse struct {
	Page            int64             `json:"page"`
	PerPage         int64             `json:"perPage"`
	CollectionGames []*CollectionGame `json:"collectionGames"`

GetCollectionGamesResponse : response for GetCollectionGames

type GetCollectionParams

type GetCollectionParams struct {
	CollectionID int64 `json:"collectionId"`

GetCollectionParams : params for GetCollection

type GetCollectionResponse

type GetCollectionResponse struct {
	Collection *Collection `json:"collection"`

GetCollectionResponse : response for GetCollection

type GetGameParams

type GetGameParams struct {
	GameID int64

	Credentials GameCredentials

GetGameParams : params for GetGame

type GetGameResponse

type GetGameResponse struct {
	Game *Game `json:"game"`

GetGameResponse : response for GetGame

type GetGameSessionsSummaryResponse

type GetGameSessionsSummaryResponse struct {
	Summary *UserGameInteractionsSummary `json:"summary"`

type GetProfileResponse

type GetProfileResponse struct {
	User *User `json:"user"`

GetProfileResponse is what the API server responds when we ask for the user's profile

type GetScannedArchiveResponse

type GetScannedArchiveResponse struct {
	ScannedArchive ScannedArchive `json:"scannedArchive"`

type GetUploadParams

type GetUploadParams struct {
	UploadID int64

	// Optional
	Credentials GameCredentials

GetUploadParams : params for GetUpload

type GetUploadResponse

type GetUploadResponse struct {
	Upload *Upload `json:"upload"`

GetUploadResponse : response for GetUpload

type GetUploadScannedArchiveParams

type GetUploadScannedArchiveParams struct {
	UploadID int64

	// Optional
	Credentials GameCredentials

type GetUserGameSessionsParams

type GetUserGameSessionsParams struct {
	GameID int64

	Credentials GameCredentials

GetUserGameSessionsParams : params for GetUserGameSessions

type GetUserParams

type GetUserParams struct {
	UserID int64

GetUserParams : params for GetUser

type GetUserResponse

type GetUserResponse struct {
	User *User `json:"user"`

GetUserResponse is what the API server responds when we ask for a user's info

type ListBuildEventsResponse

type ListBuildEventsResponse struct {
	Events []*BuildEvent `json:"events"`

ListBuildEventsResponse is what the API responds with when we ask for the list of events for a build

type ListBuildFilesResponse

type ListBuildFilesResponse struct {
	Files []*BuildFile `json:"files"`

ListBuildFilesResponse : response for ListBuildFiles

type ListChannelsResponse

type ListChannelsResponse struct {
	Channels map[string]*Channel `json:"channels"`

ListChannelsResponse is what the API responds with when we ask for all the channels of a particular game

type ListGameUploadsParams

type ListGameUploadsParams struct {
	GameID int64

	// Optional
	Credentials GameCredentials

ListGameUploadsParams : params for ListGameUploads

type ListGameUploadsResponse

type ListGameUploadsResponse struct {
	Uploads []*Upload `json:"uploads"`

ListGameUploadsResponse : response

type ListProfileCollectionsResponse

type ListProfileCollectionsResponse struct {
	Collections []*Collection `json:"collections"`

ListProfileCollectionsResponse : response for ListProfileCollections

type ListProfileGamesResponse

type ListProfileGamesResponse struct {
	Games []*Game `json:"games"`

ListProfileGamesResponse is what the API server answers when we ask for what games an account develops.

type ListProfileOwnedKeysParams

type ListProfileOwnedKeysParams struct {
	Page int64

ListProfileOwnedKeysParams : params for ListProfileOwnedKeys

type ListProfileOwnedKeysResponse

type ListProfileOwnedKeysResponse struct {
	Page      int64          `json:"page"`
	PerPage   int64          `json:"perPage"`
	OwnedKeys []*DownloadKey `json:"ownedKeys"`

ListProfileOwnedKeysResponse : response for ListProfileOwnedKeys

type ListUploadBuildsParams

type ListUploadBuildsParams struct {
	UploadID int64

	// Optional
	Credentials GameCredentials

ListUploadBuildsParams : params for ListUploadBuilds

type ListUploadBuildsResponse

type ListUploadBuildsResponse struct {
	Builds []*Build `json:"builds"`

ListUploadBuildsResponse : response for ListUploadBuilds

type LoginWithPasswordParams

type LoginWithPasswordParams struct {
	Username          string
	Password          string
	RecaptchaResponse string
	ForceRecaptcha    bool

LoginWithPasswordParams : params for LoginWithPassword

type LoginWithPasswordResponse

type LoginWithPasswordResponse struct {
	RecaptchaNeeded bool   `json:"recaptchaNeeded"`
	RecaptchaURL    string `json:"recaptchaUrl"`
	TOTPNeeded      bool   `json:"totpNeeded"`
	Token           string `json:"token"`

	Key    *APIKey `json:"key"`
	Cookie Cookie  `json:"cookie"`

LoginWithPasswordResponse : response for LoginWithPassword

type MakeBuildDownloadURLParams

type MakeBuildDownloadURLParams struct {
	BuildID int64
	Type    BuildFileType

	// Optional: Defaults to BuildFileSubTypeDefault
	SubType BuildFileSubType

	// Optional
	UUID string

	// Optional
	Credentials GameCredentials

MakeBuildDownloadURLParams : params for MakeBuildDownloadURL

type MakeBuildFileDownloadURLParams

type MakeBuildFileDownloadURLParams struct {
	BuildID int64
	FileID  int64

MakeBuildFileDownloadURLParams : params for MakeBuildFileDownloadURL

type MakeUploadDownloadURLParams

type MakeUploadDownloadURLParams struct {
	UploadID int64

	// Optional
	UUID string

	// Optional
	Credentials GameCredentials

MakeUploadDownloadURLParams : params for MakeUploadDownloadURL

type NewDownloadSessionParams

type NewDownloadSessionParams struct {
	GameID int64

	Credentials GameCredentials

NewDownloadSessionParams : params for NewDownloadSession

type NewDownloadSessionResponse

type NewDownloadSessionResponse struct {
	UUID string `json:"uuid"`

NewDownloadSessionResponse : response for NewDownloadSession

type OnOutgoingRequest

type OnOutgoingRequest func(req *http.Request)

OnRateLimited is the callback type for rate limiting events

type OnRateLimited

type OnRateLimited func(req *http.Request, res *http.Response)

OnRateLimited is the callback type for rate limiting events

type Platforms

type Platforms struct {
	Windows Architectures `json:"windows,omitempty"`
	Linux   Architectures `json:"linux,omitempty"`
	OSX     Architectures `json:"osx,omitempty"`

Platforms describes which OS/architectures a game or upload is compatible with.

type Query

type Query struct {
	Client *Client
	Path   string
	Values url.Values

A Query represents an HTTP request made to the API, whether it's GET or POST.

func NewQuery

func NewQuery(c *Client, format string, a ...interface{}) *Query

NewQuery creates a new query with a given formatted path, attached to a specific client (for http transport, retry logic, credentials)

func (*Query) AddAPICredentials

func (q *Query) AddAPICredentials()

AddAPICredentials adds the api_key= parameter from the client's key

func (*Query) AddBoolIfTrue

func (q *Query) AddBoolIfTrue(key string, value bool)

AddBoolIfTrue adds the parameter key=true only if value is true.

func (*Query) AddGameCredentials

func (q *Query) AddGameCredentials(gc GameCredentials)

AddGameCredentials adds the download_key_id, password, and secret parameters if they're non-empty in the passed GameCredentials.

func (*Query) AddInt64

func (q *Query) AddInt64(key string, value int64)

AddInt64 adds param key=value, even if value is 0

func (*Query) AddInt64IfNonZero

func (q *Query) AddInt64IfNonZero(key string, value int64)

AddInt64IfNonZero adds param key=value, only if value is non-zero

func (*Query) AddString

func (q *Query) AddString(key string, value string)

AddString adds the parameter key=value even if the value is empty.

func (*Query) AddStringIfNonEmpty

func (q *Query) AddStringIfNonEmpty(key string, value string)

AddStringIfNonEmpty adds the parameter key=value only if the value is non-empty.

func (*Query) AddTime

func (q *Query) AddTime(key string, value time.Time)

AddTime adds param key=value, with value formatted as RFC-3339 Nano

func (*Query) AddTimePtr

func (q *Query) AddTimePtr(key string, value *time.Time)

AddTimePtr adds param key=value only if value is not a nil pointer

func (*Query) AddValues

func (q *Query) AddValues(values url.Values)

AddValues adds all parameters of values to this query

func (*Query) Get

func (q *Query) Get(ctx context.Context, r interface{}) error

Get performs this query as an HTTP GET request with the tied client. Params are URL-encoded and added to the path, see URL().

func (*Query) Post

func (q *Query) Post(ctx context.Context, r interface{}) error

Post performs this query as an HTTP POST request with the tied client. Parameters are URL-encoded and passed as the body of the POST request.

func (*Query) URL

func (q *Query) URL() string

URL returns the full path for this query, as if it was a GET request (all parameters are encoded into the request URL)

type Sale

type Sale struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`

	// Game this sale is for
	GameID int64 `json:"gameId"`

	// Discount rate in percent.
	// Can be negative, see
	Rate float64 `json:"rate"`
	// Timestamp the sale started at
	StartDate time.Time `json:"startDate"`
	// Timestamp the sale ends at
	EndDate time.Time `json:"endDate"`

Sale describes a discount for a game.

type ScannedArchive

type ScannedArchive struct {
	ObjectID   int64
	ObjectType ScannedArchiveObjectType

	ExtractedSize int64
	LaunchTargets *json.RawMessage
	Manifest      *json.RawMessage

type ScannedArchiveObjectType

type ScannedArchiveObjectType = string
const (
	ScannedArchiveObjectTypeUpload ScannedArchiveObjectType = "upload"
	ScannedArchiveObjectTypeBuild  ScannedArchiveObjectType = "build"

type SearchGamesParams

type SearchGamesParams struct {
	Query string
	Page  int64

SearchGamesParams : params for SearchGames

type SearchGamesResponse

type SearchGamesResponse struct {
	Page    int64   `json:"page"`
	PerPage int64   `json:"perPage"`
	Games   []*Game `json:"games"`

SearchGamesResponse : response for SearchGames

type SearchUsersParams

type SearchUsersParams struct {
	Query string
	Page  int64

SearchUsersParams : params for SearchUsers

type SearchUsersResponse

type SearchUsersResponse struct {
	Page    int64   `json:"page"`
	PerPage int64   `json:"perPage"`
	Users   []*User `json:"users"`

SearchUsersResponse : response for SearchUsers

type SessionArchitecture

type SessionArchitecture string
const (
	SessionArchitecture386   SessionArchitecture = "386"
	SessionArchitectureAmd64 SessionArchitecture = "amd64"

type SessionPlatform

type SessionPlatform string
const (
	SessionPlatformLinux   SessionPlatform = "linux"
	SessionPlatformMacOS   SessionPlatform = "macos"
	SessionPlatformWindows SessionPlatform = "windows"

type Spec

type Spec struct {
	Target  string
	Channel string

A Spec points to a given game, optionally to a specific channel

func ParseSpec

func ParseSpec(specIn string) (*Spec, error)

ParseSpec parses something of the form `user/page:channel` and returns `user/page` and `channel` separately

func (*Spec) EnsureChannel

func (spec *Spec) EnsureChannel() error

EnsureChannel returns an error if this spec is missing a channel

func (*Spec) String

func (spec *Spec) String() string

type SubkeyParams

type SubkeyParams struct {
	GameID int64
	Scope  string

SubkeyParams : params for Subkey

type SubkeyResponse

type SubkeyResponse struct {
	Key       string `json:"key"`
	ExpiresAt string `json:"expiresAt"`

SubkeyResponse : params for Subkey

type TOTPVerifyParams

type TOTPVerifyParams struct {
	Token string
	Code  string

TOTPVerifyParams : params for TOTPVerify

type TOTPVerifyResponse

type TOTPVerifyResponse struct {
	Key    *APIKey `json:"key"`
	Cookie Cookie  `json:"cookie"`

TOTPVerifyResponse : response for TOTPVerify

type UpdateUserGameSessionParams

type UpdateUserGameSessionParams struct {
	// The ID of the session to update. It must already exist.
	SessionID int64

	SecondsRun int64
	LastRunAt  *time.Time
	Crashed    bool

UpdateUserGameSessionParams : params for UpdateUserGameSession Note that upload_id and build_id are fixed on creation, so they can't be updated.

type UpdateUserGameSessionResponse

type UpdateUserGameSessionResponse struct {
	Summary         *UserGameInteractionsSummary `json:"summary"`
	UserGameSession *UserGameSession             `json:"userGameSession"`

UpdateUserGameSessionResponse : response for UpdateUserGameSession

type UpgradePath

type UpgradePath struct {
	Builds []*Build `json:"builds"`

UpgradePath is a series of builds for which a (n,n+1) patch exists,

type Upload

type Upload struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`
	// Storage (hosted, external, etc.)
	Storage UploadStorage `json:"storage"`
	// Host (if external storage)
	Host string `json:"host,omitempty"`
	// Original file name (example: ``)
	Filename string `json:"filename"`
	// Human-friendly name set by developer (example: `Overland for Windows 64-bit`)
	DisplayName string `json:"displayName"`
	// Size of upload in bytes. For wharf-enabled uploads, it's the archive size.
	Size int64 `json:"size"`
	// Name of the wharf channel for this upload, if it's a wharf-enabled upload
	ChannelName string `json:"channelName"`
	// Latest build for this upload, if it's a wharf-enabled upload
	Build *Build `json:"build"`
	// ID of the latest build for this upload, if it's a wharf-enabled upload
	BuildID int64 `json:"buildId,omitempty"`

	// Upload type: default, soundtrack, etc.
	Type UploadType `json:"type"`

	// Is this upload a pre-order placeholder?
	Preorder bool `json:"preorder"`

	// Is this upload a free demo?
	Demo bool `json:"demo"`

	// Platforms this upload is compatible with
	Platforms Platforms `json:"platforms" hades:"squash"`

	// Date this upload was created at
	CreatedAt *time.Time `json:"createdAt"`
	// Date this upload was last updated at (order changed, display name set, etc.)
	UpdatedAt *time.Time `json:"updatedAt"`

An Upload is a downloadable file. Some are wharf-enabled, which means they're actually a "channel" that may contain multiple builds, pushed with <>

type UploadDownloadResponse

type UploadDownloadResponse struct {
	URL string `json:"url"`

UploadDownloadResponse is what the API replies to when we ask to download an upload

type UploadStorage

type UploadStorage string

UploadStorage describes where an upload file is stored.

const (
	// UploadStorageHosted is a classic upload (web) - no versioning
	UploadStorageHosted UploadStorage = "hosted"
	// UploadStorageBuild is a wharf upload (butler)
	UploadStorageBuild UploadStorage = "build"
	// UploadStorageExternal is an external upload - alllllllll bets are off.
	UploadStorageExternal UploadStorage = "external"

type UploadType

type UploadType string

UploadType describes what's in an upload - an executable, a web game, some music, etc.

const (
	// UploadTypeDefault is for executables
	UploadTypeDefault UploadType = "default"

	// UploadTypeFlash is for .swf files
	UploadTypeFlash UploadType = "flash"
	// UploadTypeUnity is for .unity3d files
	UploadTypeUnity UploadType = "unity"
	// UploadTypeJava is for .jar files
	UploadTypeJava UploadType = "java"
	// UploadTypeHTML is for .html files
	UploadTypeHTML UploadType = "html"

	// UploadTypeSoundtrack is for archives with .mp3/.ogg/.flac/etc files
	UploadTypeSoundtrack UploadType = "soundtrack"
	// UploadTypeBook is for books (epubs, pdfs, etc.)
	UploadTypeBook UploadType = "book"
	// UploadTypeVideo is for videos
	UploadTypeVideo UploadType = "video"
	// UploadTypeDocumentation is for documentation (pdf, maybe uhh doxygen?)
	UploadTypeDocumentation UploadType = "documentation"
	// UploadTypeMod is a bunch of loose files with no clear instructions how to apply them to a game
	UploadTypeMod UploadType = "mod"
	// UploadTypeAudioAssets is a bunch of .ogg/.wav files
	UploadTypeAudioAssets UploadType = "audio_assets"
	// UploadTypeGraphicalAssets is a bunch of .png/.svg/.gif files, maybe some .objs thrown in there
	UploadTypeGraphicalAssets UploadType = "graphical_assets"
	// UploadTypeSourcecode is for source code. No further comments.
	UploadTypeSourcecode UploadType = "sourcecode"
	// UploadTypeOther is for literally anything that isn't an existing category,
	// or for stuff that isn't tagged properly.
	UploadTypeOther UploadType = "other"

type User

type User struct {
	// Site-wide unique identifier generated by
	ID int64 `json:"id"`

	// The user's username (used for login)
	Username string `json:"username"`
	// The user's display name: human-friendly, may contain spaces, unicode etc.
	DisplayName string `json:"displayName"`

	// Has the user opted into creating games?
	Developer bool `json:"developer" hades:"-"`
	// Is the user part of's press program?
	PressUser bool `json:"pressUser" hades:"-"`

	// The address of the user's page on
	URL string `json:"url"`
	// User's avatar, may be a GIF
	CoverURL string `json:"coverUrl"`
	// Static version of user's avatar, only set if the main cover URL is a GIF
	StillCoverURL string `json:"stillCoverUrl"`

User represents an account, with basic profile info

type UserGameInteractionsSummary

type UserGameInteractionsSummary struct {
	SecondsRun int64      `json:"secondsRun"`
	LastRunAt  *time.Time `json:"lastRunAt"`

UserGameInteractionsSummary gives the latest "run at" timestamp and the sum of seconds run for all sessions.

type UserGameSession

type UserGameSession struct {
	// ID is the global identifier for this session
	ID int64 `json:"id"`
	// SecondsRun is the number of seconds the game has run during this session.
	SecondsRun int64 `json:"secondsRun"`
	// LastRunAt is the time this session ended.
	LastRunAt *time.Time `json:"lastRunAt"`

UserGameSession represents a single continuous run for a game.

type WharfStatusResponse

type WharfStatusResponse struct {
	Success bool `json:"success"`

WharfStatusResponse is what the API responds with when we ask for the status of the wharf infrastructure


Path Synopsis

Jump to

Keyboard shortcuts

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