package module
Version: v0.0.0-...-0fae78a Latest Latest

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

Go to latest
Published: Mar 24, 2017 License: MIT Imports: 13 Imported by: 0




Go bindings for use with the WHMCS external API. Right now, the various API calls are being added on an as-needed basis, but PR's are most welcome.

The project is very much in the earliest of beginning stages, so if you want to use it expect to get your hands dirty.

High priorities are adding unit tests and filling out the API a bit more.



Package whmcs provides Go bindings to interact with the WHMCS external API. Right now it's under heavy development, so feel free to jump in and help out.



View Source
const (
	// ErrClientNotFound is the current WHMCS response when the client does not exist.
	ErrClientNotFound string = "Client Not Found"


View Source
var (
	ErrNoAPIURL      = errors.New("THe WHMCS API URL endpoint is empty")
	ErrNoAPIUsername = errors.New("THe WHMCS API username is empty")

Errors which prevent the API from connecting.

View Source
var (
	ErrNoClientID        = errors.New("Empty client ID")
	ErrNoPID             = errors.New("Empty PID")
	ErrNoDomain          = errors.New("Empty domain")
	ErrNoBillingCycle    = errors.New("Empty billing cycle")
	ErrNoDomainType      = errors.New("Empty domain type")
	ErrInvalidDomainType = errors.New("Invalid domain type")
	ErrNoRegPeriod       = errors.New("Empty registration period")
	ErrNoNameserver      = errors.New("Empty nameserver")
	ErrNoPaymentMethod   = errors.New("Empty payment method")

Errors which are returned when validating orders for completeness/accuracy.

View Source
var (
	ErrTransactionAmountsEmpty       = errors.New("Transaction amounts empty")
	ErrTransactionPaymentMethodEmpty = errors.New("Transaction payment method empty")
	ErrTransactionDateEmpty          = errors.New("Transaction date empty")

Various error responses

View Source
var (
	// DateFormat is the Go date format string that must match the WHMCS install's
	// date format to support WHMCS date fields which must be formatted according
	// to the system date/time format.00
	DateFormat = "01/02/2006"
View Source
var (
	ErrNoClientDetails = errors.New("Client must have either ClientEmail or ClientID")

Various errors returned

View Source
var (
	ErrNoProductID = errors.New("No pid specified")


This section is empty.


type API

type API struct {
	Endpoint, Username, Password string

API is the main structure from which API calls are sent.

func NewAPI

func NewAPI(url, user, pwd string) (api *API, err error)

func (*API) AcceptOrder

func (a *API) AcceptOrder(o *AcceptOrderRequest) (err error)

AcceptOrder accepts the WHMCS order with the matching parameters in `o`

func (*API) AddClient

func (a *API) AddClient(c *NewClient) (r *AddClientResult, err error)

AddClient creates a new WHMCS client.

func (*API) AddOrder

func (a *API) AddOrder(o *Order) (r *OrderResponse, err error)

AddOrder calls the "addorder" action of the WHMCS API.

func (*API) AddTransaction

func (a *API) AddTransaction(t *Transaction) error

AddTransaction creates the given transaction, returing error if it fails.

func (*API) ClientExists

func (a *API) ClientExists(email string) (bool, error)

ClientExists returns true if the client already exists.

func (*API) CreateInvoice

func (a *API) CreateInvoice(i *CreateInvoiceRequest) (r *CreateInvoiceResponse, err error)

CreateInvoice creates a new invoice with the given paramaters in `r`.

func (*API) Do

func (a *API) Do(cmd string, data interface{}) ([]byte, error)

func (*API) UpdateClientDomain

func (a *API) UpdateClientDomain(d *ClientDomainUpdateReq) (r *ClientDomainUpdateResp, err error)

UpdateClientDomain sends the updateclientdomain command with the given params.

func (*API) UpdateClientProduct

func (a *API) UpdateClientProduct(p *ClientProduct) (r *UpdateClientProductResult, err error)

func (*API) UpdateExistingClient

func (a *API) UpdateExistingClient(c *ExistingClient) (r *UpdateClientResult, err error)

UpdateExistingClient updates an existing client.

func (*API) UpdateInvoice

func (a *API) UpdateInvoice(i *UpdateInvoiceRequest) (r *UpdateInvoiceResponse, err error)

UpdateInvoice updates the invoice with the given parameters of `r`.

func (*API) ValidateLogin

func (a *API) ValidateLogin(v *ValidateLogin) (r *ValidateLoginResult, err error)

func (*API) Whois

func (a *API) Whois(domain string) (w WhoisInfo, err error)

Whois returns the WHOIS data for the given domain.

type APIBasicResponse

type APIBasicResponse struct {
	Result string `json:"result" xml:"result"`

func (*APIBasicResponse) Success

func (r *APIBasicResponse) Success() bool

type APIResponse

type APIResponse struct {
	Message string `json:"message" xml:"message"`
	Result  string `json:"result" xml:"result"`

func (*APIResponse) Error

func (r *APIResponse) Error() error

type AcceptOrderRequest

type AcceptOrderRequest struct {
	OrderID int64 `json:"orderid,string"`

	// Optional attributes
	ServerID        int64  `json:"serverid,string,omitempty"`
	ServiceUsername string `json:"serviceusername,omitempty"`
	ServicePassword string `json:"servicepassword,omitempty"`
	AutoSetup       bool   `json:"autosetup,string,omitempty"`
	Registrar       string `json:"registrar,omitempty"`
	SendRegistrar   bool   `json:"sendregistrar,string,omitempty"`
	SendEmail       bool   `json:"sendemail,string,omitempty"`

AcceptOrderRequest is a struct of available parameters to accept an order.

func (*AcceptOrderRequest) Error

func (o *AcceptOrderRequest) Error() error

type AddClientResult

type AddClientResult struct {
	ClientID int64  `json:"clientid"`
	Result   string `json:"result"`
	Message  string `json:"message"`

AddClientResult is the WHMCS response when adding a client.

type ClientDetailsReq

type ClientDetailsReq struct {
	ClientID string `json:"clientid,omitempty"`
	Email    string `json:"email,omitempty"`
	Stats    bool   `json:"stats,omitempty"`

ClientDetailsReq is the struct of parameters available to retrieve client details.

type ClientDomainUpdateReq

type ClientDomainUpdateReq struct {
	DomainID int64 `json:"domainid"`

	DNSManagement      bool    `json:"dnsmanagement,omitempty"`
	EmailForwarding    bool    `json:"emailforwarding,omitempty"`
	IDProtection       bool    `json:"idprotection,omitempty"`
	DoNotRenew         bool    `json:"donotrenew,omitempty"`
	Type               string  `json:"type,omitempty"`
	RegDate            string  `json:"regdate,omitempty"`     // Y-m-d
	NextDueDate        string  `json:"nextduedate,omitempty"` // Y-m-d
	ExpiryDate         string  `json:"expirydate,omitempty"`  // Y-m-d
	Domain             string  `json:"domain,omitempty"`
	FirstPaymentAmount float64 `json:"firstpaymentamount,omitempty"`
	Registrar          string  `json:"registrar,omitempty"`
	RegPeriod          int64   `json:"regperiod,omitempty"`
	PaymentMethod      string  `json:"paymentmethod,omitempty"`
	SubscriptionID     string  `json:"subscriptionid,omitempty"`
	Status             string  `json:"status,omitempty"`
	Notes              string  `json:"notes,omitempty"`
	PromoID            int64   `json:"promoid,omitempty"`
	AutoRecalc         bool    `json:"autorecalc,omitempty"`
	UpdateNS           bool    `json:"updatens,omitempty"`
	NS1                string  `json:"ns1,omitempty"`
	NS2                string  `json:"ns2,omitempty"`
	NS3                string  `json:"ns3,omitempty"`
	NS4                string  `json:"ns4,omitempty"`
	NS5                string  `json:"ns5,omitempty"`

func (*ClientDomainUpdateReq) SetExpiryDate

func (c *ClientDomainUpdateReq) SetExpiryDate(t time.Time)

SetExpiryDate sets the ExpiryDate in the format expected by WHMCS using the given time t

func (*ClientDomainUpdateReq) SetNextDueDate

func (c *ClientDomainUpdateReq) SetNextDueDate(t time.Time)

SetNextDueDate sets the DueDate in the format expected by WHMCS using the given time t

func (*ClientDomainUpdateReq) SetRegDate

func (c *ClientDomainUpdateReq) SetRegDate(t time.Time)

SetRegDate sets the RegDate in the format expected by WHMCS using the given time t

type ClientDomainUpdateResp

type ClientDomainUpdateResp struct {
	Result   string `json:"result"`
	DomainID int64  `json:"domainid,string"`

type ClientProduct

type ClientProduct struct {
	ServiceID            string        `json:"serviceid" xml:"serviceid"`
	PID                  string        `json:"pid,omitempty" xml:"pid,omitempty"`
	ServerID             string        `json:"serverid,omitempty" xml:"serverid,omitempty"`
	RegDate              ProductDate   `json:"regdate,omitempty" xml:"regdate,omitempty"`
	NextDueDate          ProductDate   `json:"nextduedate,omitempty" xml:"nextduedate,omitempty"`
	Domain               string        `json:"domainomitempty,omitempty" xml:"domainomitempty,omitempty"`
	FirstPaymentAmount   float64       `json:"firstpaymentamount,string,omitempty" xml:"firstpaymentamount,omitempty"`
	RecurringAmount      float64       `json:"recurringamount,string,omitempty" xml:"recurringamount,omitempty"`
	BillingCycle         string        `json:"billingcycle,omitempty" xml:"billingcycle,omitempty"`
	PaymentMethod        string        `json:"paymentmethod,omitempty" xml:"paymentmethod,omitempty"`
	Status               string        `json:"status,omitempty" xml:"status,omitempty"`
	ServiceUsername      string        `json:"serviceusername,omitempty" xml:"serviceusername,omitempty"`
	ServicePassword      string        `json:"servicepassword,omitempty" xml:"servicepassword,omitempty"`
	SubscriptionID       string        `json:"subscriptionid,omitempty" xml:"subscriptionid,omitempty"`
	PromoID              string        `json:"promoid,omitempty" xml:"promoid,omitempty"`
	OverrideAutoSuspend  string        `json:"overideautosuspend,omitempty" xml:"overideautosuspend,omitempty"`
	OverrideSuspendUntil ProductDate   `json:"overidesuspenduntil,omitempty" xml:"overideautosuspend,omitempty"`
	NS1                  string        `json:"ns1,omitempty" xml:"ns1,omitempty"`
	NS2                  string        `json:"ns2,omitempty" xml:"ns2,omitempty"`
	DedicatedIP          string        `json:"dedicatedip,omitempty" xml:"dedicatedip,omitempty"`
	AssignedIPs          string        `json:"assignedips,omitempty" xml:"assignedips,omitempty"`
	Notes                string        `json:"notes,omitempty" xml:"notes,omitempty"`
	AutoRecalc           bool          `json:"autorecalc,string,omitempty" xml:"autorecalc,omitempty"`
	CustomFields         CustomFields  `json:"customfields,omitempty" xml:"customfields,omitempty"`
	ConfigOptions        ConfigOptions `json:"configoptions,omitempty" xml:"configoptions,omitempty"`

func (*ClientProduct) Error

func (p *ClientProduct) Error() error

type ConfigOptions

type ConfigOptions map[string]string

ConfigOptions @TODO

func (ConfigOptions) MarshalJSON

func (c ConfigOptions) MarshalJSON() ([]byte, error)

MarshalJSON formats the custom fields in a way that WHMCS recognizes them.

type CreateInvoiceRequest

type CreateInvoiceRequest struct {
	UserID           int64       `json:"userid,string"`
	Date             InvoiceDate `json:"date"`
	DueDate          InvoiceDate `json:"duedate"`
	PaymentMethod    string      `json:"paymentmethod"`
	ItemDescription1 string      `json:"itemdescription1"`
	ItemAmount1      float64     `json:"itemamount1,string"`
	ItemTaxed1       int64       `json:"itemtaxed1,string"`

	// Optional attributes
	TaxRate          int64   `json:"taxrate,string,omitempty"`
	TaxRate2         int64   `json:"taxrate2,string,omitempty"`
	Notes            string  `json:"notes,omitempty"`
	SendInvoice      bool    `json:"sendinvoice,string,omitempty"`
	AutoApplyCredit  bool    `json:"autoapplycredit,string,omitempty"`
	ItemDescription2 string  `json:"itemdescription2,omitempty"`
	ItemAmount2      float64 `json:"itemamount2,string,omitempty"`
	ItemTaxed2       int64   `json:"itemtaxed2,string,omitempty"`

CreateInvoiceRequest contains all available fields for creating a new invoice.

func (*CreateInvoiceRequest) Error

func (r *CreateInvoiceRequest) Error() error

type CreateInvoiceResponse

type CreateInvoiceResponse struct {
	Result    string `json:"result"`
	InvoiceID int64  `json:"invoiceid"`

CreateInvoiceResponse is the WHMCS response when creating an invoice.

type CustomFields

type CustomFields map[string]string

CustomFields @TODO

func (CustomFields) MarshalJSON

func (c CustomFields) MarshalJSON() ([]byte, error)

MarshalJSON formats the custom fields in a way that WHMCS recognizes them.

type Date

type Date time.Time

Date implements WHMCS formatted JSON marshaler for time values that need to match the WHMCS system format.

func (Date) MarshalJSON

func (d Date) MarshalJSON() ([]byte, error)

MarshalJSON changes the date to a format which WHMCS recognizes. @TODO This is for the AddTransaction momentarily, and must match the WHMCS date format of the local install because the WHMCS API is whack. We need to be able to change this as needed. Also, other API endpoints require other fixed date formats, so we have to take that into account down the road too.

func (Date) Time

func (d Date) Time() time.Time

Time returns the original time.Time value of the date.

type ExistingClient

type ExistingClient struct {
	ClientID    string `"json:clientid,omitempty"`
	ClientEmail string `json:"clientemail,omitempty"`

	Firstname           string       `json:"firstname,omitempty"`
	Lastname            string       `json:"lastname,omitempty"`
	CompanyName         string       `json:"companyname,omitempty"`
	Email               string       `json:"email,omitempty"`
	Address1            string       `json:"address1,omitempty"`
	Address2            string       `json:"address2,omitempty"`
	City                string       `json:"city,omitempty"`              //
	State               string       `json:"state,omitempty"`             //
	Postcode            string       `json:"postcode,omitempty"`          //
	Country             string       `json:"country,omitempty"`           // - two letter ISO country code
	PhoneNumber         string       `json:"phonenumber,omitempty"`       //
	Password2           string       `json:"password2,omitempty"`         //
	Credit              float64      `json:"credit,string,omitempty"`     // - credit balance
	TaxExempt           bool         `json:"taxexempt,omitempty"`         // - true to enable
	Notes               string       `json:"notes,omitempty"`             //
	CardType            string       `json:"cardtype,omitempty"`          // - visa, mastercard, etc...
	CardNum             string       `json:"cardnum,omitempty"`           // - cc number
	ExpDate             string       `json:"expdate,omitempty"`           // - cc expiry date
	StartDate           Date         `json:"startdate,omitempty"`         // - cc start date
	IssueNumber         string       `json:"issuenumber,omitempty"`       // - cc issue number
	BankName            string       `json:"bankname,omitempty"`          // - for use with direct debit gateway
	BankType            string       `json:"banktype,omitempty"`          // - for use with direct debit gateway
	BankCode            string       `json:"bankcode,omitempty"`          // - for use with direct debit gateway
	BankAcct            string       `json:"bankacct,omitempty"`          // - for use with direct debit gateway
	Language            string       `json:"language,omitempty"`          // - default language
	ClearCreditCard     bool         `json:"clearcreditcard,omitempty"`   // - set to true to remove stored card data (V5.3.7+ only)
	PaymentMethod       string       `json:"paymentmethod,omitempty"`     // - paypal, authorize, etc...
	CustomFiels         CustomFields `json:"customfields,omitempty"`      // - a base64 encoded serialized array of custom field values
	Status              string       `json:"status,omitempty"`            // - active or inactive
	LateFeeOverride     bool         `json:"latefeeoveride,omitempty"`    // - true/false
	OverrideDueInvoices bool         `json:"overideduenotices,omitempty"` // - true/false
	SeparateInvoices    bool         `json:"separateinvoices,omitempty"`  // - true/false
	DisableAutoCC       bool         `json:"disableautocc,omitempty"`     // - true/false


func (*ExistingClient) ByEmail

func (c *ExistingClient) ByEmail(email string)

ByEmail sets the clients email with the given email string

func (*ExistingClient) ByID

func (c *ExistingClient) ByID(id string)

ByID sets the client's ID with the given id string

func (*ExistingClient) Error

func (c *ExistingClient) Error() error

type InvoiceDate

type InvoiceDate time.Time

InvoiceDate is a time.Time value that gets marshaled into the correct format for the WHMCS invoice date fields.

func (InvoiceDate) MarshalJSON

func (d InvoiceDate) MarshalJSON() ([]byte, error)

MarshalJSON changes the date to a format which WHMCS recognizes for invoices.

func (InvoiceDate) Time

func (d InvoiceDate) Time() time.Time

Time returns the original time.Time value of the date.

type NewClient

type NewClient struct {

	// Required if adding.
	Firstname   string `json:"firstname"`
	Lastname    string `json:"lastname"`
	Email       string `json:"email"`
	Address1    string `json:"address1"`
	City        string `json:"city"`
	State       string `json:"state"`
	Postcode    string `json:"postcode"`
	Country     string `json:"country"`
	PhoneNumber string `json:"phonenumber"`
	Password2   string `json:"password2"`

	// Optional
	CompanyName    string            `json:"companyname,omitempty"`
	Address2       string            `json:"address2,omitempty"`
	Currency       string            `json:"currency,omitempty"`
	ClientIP       string            `json:"clientip,omitempty"`
	Language       string            `json:"language,omitempty"`
	GroupID        int64             `json:"groupid,string,omitempty"`
	SecurityQID    int64             `json:"securityqid,string,omitempty"`
	SecurityQans   string            `json:"securityqans,omitempty"`
	Notes          string            `json:"notes,omitempty"`
	CardNum        string            `json:"cardnum,omitempty"`
	CardType       string            `json:"cardtype,omitempty"`
	ExpDate        string            `json:"expdate,omitempty"`
	StartDate      string            `json:"startdate,omitempty"`
	IssueNumber    string            `json:"isseunumber,omitempty"`
	CustomFields   map[string]string `json:"customfields,omitempty"`
	NoEmail        bool              `json:"noemail,string,omitempty"`
	SkipValidation bool              `json:"skipvalidation,string,omitempty"`

NewClient contains the basic structure of data for creating a new client.

func (*NewClient) Error

func (c *NewClient) Error() error

type Order

type Order struct {

	// Required attributes
	ClientID      int64  `json:"clientid,string"`
	PID           int64  `json:"pid,string,omitempty"`
	Domain        string `json:"domain"`
	BillingCycle  string `json:"billingcycle"`
	DomainType    string `json:"domaintype"`
	Regperiod     int64  `json:"regperiod,string"`
	EppCode       string `json:"eppcode"`
	Nameserver1   string `json:"nameserver1"`
	PaymentMethod string `json:"paymentmethod"`

	// Optional attributes
	CustomFields   CustomFields  `json:"customfields,omitempty"`
	ConfigOptions  ConfigOptions `json:"configoptions,omitempty"`
	PriceOverride  float64       `json:"priceoverride,string,omitempty"`
	PromoCode      string        `json:"promocode,omitempty"`
	PromoOverride  string        `json:"promooverride,omitempty"`
	AffID          string        `json:"affid,omitempty"`
	NoInvoice      bool          `json:"noinvoice,string,omitempty"`
	NoInvoiceEmail bool          `json:"noinvoiceemail,string,omitempty"`
	ClientIP       string        `json:"clientip,omitempty"`
	Addons         string        `json:"addons,omitempty"`

	// For VPS/Dedicated Server Orders only
	Hostname  string `json:"hostname,omitempty"`
	NS1Prefix string `json:"ns1prefix,omitempty"`
	NS2Prefix string `json:"ns2prefix,omitempty"`
	RootPw    string `json:"rootpw,omitempty"`

	// For domain reg only
	ContactID           int64  `json:"contactid,omitempty"`
	DNSManagement       bool   `json:"dnsmanagement,omitempty"`
	EmailForwarding     bool   `json:"emailforwarding,omitempty"`
	IDProtection        bool   `json:"idprotection,omitempty"`
	Nameserver2         string `json:"nameserver2,omitempty"`
	Nameserver3         string `json:"nameserver3,omitempty"`
	Nameserver4         string `json:"nameserver4,omitempty"`
	DomainRenewOverride int64  `json:"domainrenewoverride,omitempty"`
	DomainPriceOverride int64  `json:"domainpriceoverride,omitempty"`

Order is all the data needed to create a WHMCS order.

func (*Order) Error

func (o *Order) Error() error

type OrderResponse

type OrderResponse struct {
	Result     string `json:"result"`
	OrderID    int64  `json:"orderid"`
	InvoiceID  int64  `json:"invoiceid"`
	ProductIDs string `json:"productids"`
	AddonIDs   string `json:"addonids"`
	DomainIDs  string `json:"domainids"`

OrderResponse holds the successful order information sent via from the WHMCS API.

func (*OrderResponse) Addons

func (o *OrderResponse) Addons() []string

Addons returns a slice of addon ids related to the order.

func (*OrderResponse) Domains

func (o *OrderResponse) Domains() []string

Domains returns a slice of domains related to the order.

func (*OrderResponse) Products

func (o *OrderResponse) Products() []string

Products returns a slice of product ids related to the order.

type ProductDate

type ProductDate time.Time

func (ProductDate) MarshalJSON

func (d ProductDate) MarshalJSON() ([]byte, error)

type Transaction

type Transaction struct {
	// Required fields
	AmountIn      float64 `json:"amountin,string" xml:"amountin"`
	AmountOut     float64 `json:"amountout,string" xml:"amountout"`
	PaymentMethod string  `json:"paymentmethod" xml:"paymentmethod"`
	Date          Date    `json:"date" xml:"date"`

	// Optional fields
	UserID      int64   `json:"userid,string,omitempty" xml:"userid,omitempty"`
	InvoiceID   int64   `json:"invoiceid,string,omitempty" xml:"invoiceid,omitempty"`
	Description string  `json:"description,omitempty" xml:"description,omitempty"`
	Fees        float64 `json:"fees,string,omitempty" xml:"fees,omitempty"`
	TransID     string  `json:"transid,omitempty" xml:"transid,omitempty"`
	Credit      bool    `json:"credit,string,omitempty" xml:"credit,omitempty"`

Transaction is a struct containing parameters which can be sent to create a new transaction.

func (*Transaction) Error

func (t *Transaction) Error() error

type UpdateClientProductResult

type UpdateClientProductResult struct {
	Result    string `json:"result" xml:"result"`
	ServiceID string `json:"serviceid" xml:"serviceid"`

type UpdateClientResult

type UpdateClientResult struct {
	Result   string `json:"result"`
	ClientID int64  `json:"clientid,string"`

UpdateClientResult is the WHMCS response when updating a client.

type UpdateInvoiceRequest

type UpdateInvoiceRequest struct {
	InvoiceID int64 `json:"invoiceid,string"`

	// Optional attributes
	Status        string      `json:"status,omitempty"`
	Date          InvoiceDate `json:"date,omitempty"`
	DueDate       InvoiceDate `json:"duedate,omitempty"`
	PaymentMethod string      `json:"paymentmethod,omitempty"`

UpdateInvoiceRequest contains the parameters available to update an existing invoice.

func (*UpdateInvoiceRequest) Error

func (r *UpdateInvoiceRequest) Error() error

type UpdateInvoiceResponse

type UpdateInvoiceResponse struct {
	Result    string `json:"result"`
	InvoiceID int64  `json:"invoiceid,string"`

UpdateInvoiceResponse is the WHMCS response when updating an invoice.

type ValidateLogin

type ValidateLogin struct {
	Email     string `json:"email"`
	Password2 string `json:"password2"`

func (*ValidateLogin) Error

func (v *ValidateLogin) Error() error

type ValidateLoginResult

type ValidateLoginResult struct {
	UserID       int64  `json:"userid"`
	ContactID    int64  `json:"contactid"`
	PasswordHash string `json:"passwordhash"`
	Result       string `json:"result"`
	Message      string `json:"message"`

type WhoisInfo

type WhoisInfo struct {
	Result string `json:"result" xml:"result"`
	Status string `json:"status" xml:"status"`
	Whois  string `json:"whois" xml:"whois"`

type WhoisRequest

type WhoisRequest struct {
	Action string `json:"action" xml:"action"`
	Domain string `json:"domain" xml:"domain"`


Path Synopsis
Package serializer serializes Go data into a PHP serialized format.
Package serializer serializes Go data into a PHP serialized format.

Jump to

Keyboard shortcuts

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