asana

package
v0.0.0-...-1161574 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2018 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Example (Client_AddUserToTeam)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

confirmation, err := client.AddUserToTeam(&asana.TeamRequest{
	UserID: "emm.odeke@gmail.com",
	TeamID: "331783765164429",
})

if err != nil {
	log.Fatalf("err adding myself to the team: %v", err)
}

log.Printf("confirmation: %#v\n", confirmation)
Output:

Example (Client_CreateProject)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

proj, err := client.CreateProject(&asana.ProjectRequest{
	Name:      "Project-Go",
	Notes:     "This is a port of api clients to Go",
	Layout:    asana.BoardLayout,
	Workspace: "331783765164429",

	PublicToOrganization: true,
})

if err != nil {
	log.Fatal(err)
}

log.Printf("Created project: %#v", proj)
Output:

Example (Client_CreateTask)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

setupServers, err := client.CreateTask(&asana.TaskRequest{
	Assignee:  "emm.odeke@gmail.com",
	Notes:     "Announce Asana Go API client release",
	Name:      "api-client-release",
	Workspace: "331783765164429",
	Followers: []asana.UserID{
		"emmanuel@orijtech.com",
	},
})

if err != nil {
	log.Fatalf("the error: %#v", err)
}

log.Printf("Here is the task: %#v", setupServers)
Output:

Example (Client_DeleteProject)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

projectID := "332697649493087"
if err := client.DeleteProjectByID(projectID); err != nil {
	log.Printf("Successfully deleted project %q!", projectID)
} else {
	log.Fatalf("Failed to delete project %q!", projectID)
}
Output:

Example (Client_DeleteTask)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

if err := client.DeleteTask("332508471165497"); err != nil {
	log.Fatalf("Task deletion err: %v", err)
} else {
	log.Printf("Successfully deleted the task!")
}
Output:

Example (Client_FindAttachmentByID)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

foundAttachment, err := client.FindAttachmentByID("338179717217493")
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Found attachment: %#v\n", foundAttachment)
Output:

Example (Client_FindProjectByID)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

proj, err := client.FindProjectByID("332697649493087")
if err != nil {
	log.Fatal(err)
}

log.Printf("The project: %#v", proj)
Output:

Example (Client_FindTaskByID)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

setupServers, err := client.FindTaskByID("332508471165497")
if err != nil {
	log.Fatal(err)
}

fmt.Printf("TaskName: %s\n", setupServers.Name)
if setupServers.HeartedByMe {
	fmt.Printf("I heart'd this task!\n")
}

fmt.Printf("Assignee: %#v\n", setupServers.AssigneeStatus)
fmt.Printf("Assignee: %#v\n", setupServers.Assignee)
fmt.Printf("Notes: %#v\n", setupServers.Notes)
fmt.Printf("Followers\n")
fmt.Printf("CreatedAt: %v\n", setupServers.CreatedAt)
fmt.Printf("ModifiedAt: %v\n", setupServers.ModifiedAt)

for _, follower := range setupServers.Followers {
	fmt.Printf("ID: %v Name: %s\n", follower.ID, follower.Name)
}

for i, heart := range setupServers.Hearts {
	fmt.Printf("#%d HeartID: %v Name: %s\n", i+1, heart.ID, heart.Name)
}

for _, tag := range setupServers.Tags {
	fmt.Printf("Tag: %v ID: %v\n", tag.Name, tag.ID)
}
Output:

Example (Client_FindTeamByID)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

engTeam, err := client.FindTeamByID("")
if err != nil {
	log.Fatal(err)
}
log.Printf("This is the information for the 331783765164429 team: %#v", engTeam)
Output:

Example (Client_ListAllAttachmentsForTask)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

attachmentsPage, err := client.ListAllAttachmentsForTask("331727965981099")
if err != nil {
	log.Fatal(err)
}

for i, task := range attachmentsPage.Attachments {
	fmt.Printf("Task #%d: %#v\n\n", i, task)
}
Output:

Example (Client_ListAllTeamsForUser)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

teamsPagesChan, _, err := client.ListAllTeamsForUser(&asana.TeamRequest{
	UserID:         asana.MeAsUser,
	OrganizationID: "332697157202049",
})
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range teamsPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, team := range page.Teams {
		log.Printf("Page: #%d i: %d team: %#v", pageCount, i, team)
	}
	pageCount += 1
}
Output:

Example (Client_ListAllTeamsInOrganization)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

teamsPagesChan, _, err := client.ListAllTeamsInOrganization("332697157202049")
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range teamsPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, team := range page.Teams {
		log.Printf("Page: #%d i: %d team: %#v", pageCount, i, team)
	}
	pageCount += 1
}
Output:

Example (Client_ListAllUsersInTeam)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

usersPagesChan, _, err := client.ListAllUsersInTeam("331783765164429")
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range usersPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, user := range page.Users {
		log.Printf("Page: #%d i: %d team: %#v", pageCount, i, user)
	}
	pageCount += 1
}
Output:

Example (Client_ListMyTasks)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}
taskPagesChan, err := client.ListMyTasks(&asana.TaskRequest{
	Workspace: "331727068525363",
})
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range taskPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, task := range page.Tasks {
		log.Printf("Page: #%d i: %d task: %#v", pageCount, i, task)
	}
	pageCount += 1
}
Output:

Example (Client_ListMyWorkspaces)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

workspacesChan, err := client.ListMyWorkspaces()
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range workspacesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, workspace := range page.Workspaces {
		log.Printf("Page: #%d i: %d workspace: %#v", pageCount, i, workspace)
	}
	pageCount += 1
}
Output:

Example (Client_ListTasksForProject)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

taskPagesChan, _, err := client.ListTasksForProject(&asana.TaskRequest{
	ProjectID: "331783765164429",
})
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range taskPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, task := range page.Tasks {
		log.Printf("Page: #%d i: %d task: %#v", pageCount, i, task)
	}
	pageCount += 1
}
Output:

Example (Client_QueryForProjects)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

pagesChan, _, err := client.QueryForProjects(&asana.ProjectQuery{
	Archived:    false,
	WorkspaceID: "331783765164429",
})

if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range pagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, project := range page.Projects {
		log.Printf("Page: #%d i: %d project: %#v", pageCount, i, project)
	}
	pageCount += 1
}
Output:

Example (Client_RemoveUserFromTeam)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

err = client.RemoveUserFromTeam(&asana.TeamRequest{
	UserID: "emm.odeke@gmail.com",
	TeamID: "331783765164429",
})

if err != nil {
	log.Fatalf("failed to remove user from team, err: %v", err)
} else {
	log.Printf("successfully removed the user from the team")
}
Output:

Example (Client_TasksForProject)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

tasksPagesChan, _, err := client.TasksForProject("332697157202049")
if err != nil {
	log.Fatal(err)
}

pageCount := 0
for page := range tasksPagesChan {
	if err := page.Err; err != nil {
		log.Printf("Page: #%d err: %v", pageCount, err)
		continue
	}

	for i, task := range page.Tasks {
		log.Printf("Page: #%d i: %d task: %#v", pageCount, i, task)
	}
	pageCount += 1
}
Output:

Example (Client_UpdateProject)
client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

proj, err := client.UpdateProject(&asana.ProjectRequest{
	ProjectID: "332697649493087",
	Name:      "Project-Go updated",
	Notes:     "We need to prioritize which features will be included\nAm also changing it to a list layout",
	Layout:    asana.ListLayout,

	PublicToOrganization: false,
})

if err != nil {
	log.Fatal(err)
}

log.Printf("Updated project: %#v", proj)
Output:

Example (Client_UploadAttachment)
imageR, err := os.Open("./testdata/messengerQR.png")
if err != nil {
	log.Fatal(err)
}
defer imageR.Close()

client, err := asana.NewClient()
if err != nil {
	log.Fatal(err)
}

respAttachment, err := client.UploadAttachment(&asana.AttachmentUpload{
	TaskID: "331727965981099",
	Name:   "messenger QR code",
	Body:   imageR,
})
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Response attachment: %#v\n", respAttachment)
Output:

Index

Examples

Constants

View Source
const MeAsUser = "me"

Variables

This section is empty.

Functions

This section is empty.

Types

type AssigneeStatus

type AssigneeStatus string
const (
	StatusInbox    AssigneeStatus = "inbox"
	StatusLater    AssigneeStatus = "later"
	StatusToday    AssigneeStatus = "today"
	StatusUpcoming AssigneeStatus = "upcoming"
)

func (AssigneeStatus) MarshalJSON

func (as AssigneeStatus) MarshalJSON() ([]byte, error)

func (AssigneeStatus) String

func (as AssigneeStatus) String() string

type Attachment

type Attachment struct {
	ID int64 `json:"id,omitempty"`

	CreatedAt   *otils.NullableTime  `json:"created_at,omitempty"`
	DownloadURL otils.NullableString `json:"download_url"`

	// Host is a read-only value.
	// Valid values are asana, dropbox, gdrive and box.
	Host otils.NullableString `json:"host"`

	Name otils.NullableString `json:"name"`

	// Parent contains the information of the
	// task that this attachment is attached to.
	Parent *NamedAndIDdEntity `json:"parent,omitempty"`

	ViewURL otils.NullableString `json:"view_url,omitempty"`
}

type AttachmentUpload

type AttachmentUpload struct {
	Body   io.Reader `json:"-"`
	TaskID string    `json:"task_id"`
	Name   string    `json:"name"`
}

func (*AttachmentUpload) Validate

func (au *AttachmentUpload) Validate() error

type AttachmentWrap

type AttachmentWrap struct {
	Attachment *Attachment `json:"data"`
}

type AttachmentsPage

type AttachmentsPage struct {
	Attachments []*Attachment `json:"data"`
}

type Client

type Client struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

func NewClient

func NewClient(personalAccessTokens ...string) (*Client, error)

NewClient tries to use the first non-empty token passed otherwise if no tokens are passed in, it will look for the variable

`ASANA_PERSONAL_ACCESS_TOKEN`

in your environment. It returns an error if it fails to find any API key to use.

func (*Client) AddUserToTeam

func (c *Client) AddUserToTeam(treq *TeamRequest) (*Team, error)

func (*Client) AddUsersToProject

func (c *Client) AddUsersToProject(preq *ProjectRequest) error

func (*Client) CreateProject

func (c *Client) CreateProject(preq *ProjectRequest) (*Project, error)

func (*Client) CreateTask

func (c *Client) CreateTask(t *TaskRequest) (*Task, error)

func (*Client) DeleteProjectByID

func (c *Client) DeleteProjectByID(projectID string) error

func (*Client) DeleteTask

func (c *Client) DeleteTask(taskID string) error

func (*Client) FindAttachmentByID

func (c *Client) FindAttachmentByID(attachmentID string) (*Attachment, error)

func (*Client) FindProjectByID

func (c *Client) FindProjectByID(projectID string) (*Project, error)

func (*Client) FindTaskByID

func (c *Client) FindTaskByID(taskID string) (*Task, error)

func (*Client) FindTeamByID

func (c *Client) FindTeamByID(teamID string) (*Team, error)

func (*Client) GetUser

func (c *Client) GetUser(id UserID) (*User, error)

func (*Client) ListAllAttachmentsForTask

func (c *Client) ListAllAttachmentsForTask(taskID string) (*AttachmentsPage, error)

ListAllAttachmentsForTask retrieves all the attachments for the taskID provided.

func (*Client) ListAllMyTasks

func (c *Client) ListAllMyTasks() (resultsChan chan *TaskResultPage, cancelChan chan<- bool, err error)

func (*Client) ListAllTeamsForUser

func (c *Client) ListAllTeamsForUser(treq *TeamRequest) (pagesChan chan *TeamPage, cancelChan chan<- bool, err error)

func (*Client) ListAllTeamsInOrganization

func (c *Client) ListAllTeamsInOrganization(organizationID string) (pagesChan chan *TeamPage, cancelChan chan<- bool, err error)

func (*Client) ListAllUsersInOrganization

func (c *Client) ListAllUsersInOrganization(teamID string) (pagesChan chan *UsersPage, cancelChan chan<- bool, err error)

func (*Client) ListAllUsersInTeam

func (c *Client) ListAllUsersInTeam(teamID string) (pagesChan chan *UsersPage, cancelChan chan<- bool, err error)

func (*Client) ListMyTasks

func (c *Client) ListMyTasks(treq *TaskRequest) (chan *TaskResultPage, error)

func (*Client) ListMyWorkspaces

func (c *Client) ListMyWorkspaces() (chan *WorkspacePage, error)

func (*Client) ListTasksForProject

func (c *Client) ListTasksForProject(treq *TaskRequest) (resultsChan chan *TaskResultPage, cancelChan chan<- bool, err error)

func (*Client) QueryForProjects

func (c *Client) QueryForProjects(pq *ProjectQuery) (pagesChan chan *ProjectsPage, cancelChan chan<- bool, err error)

FindProjects queries for projects with atleast one of the fields of the ProjectQuery set as a filter.

func (*Client) RemoveUserFromTeam

func (c *Client) RemoveUserFromTeam(treq *TeamRequest) error

func (*Client) RemoveUsersFromProject

func (c *Client) RemoveUsersFromProject(preq *ProjectRequest) error

func (*Client) SetHTTPRoundTripper

func (c *Client) SetHTTPRoundTripper(rt http.RoundTripper)

func (*Client) TasksForProject

func (c *Client) TasksForProject(projectID string) (resultsChan chan *TaskResultPage, cancelChan chan<- bool, err error)

func (*Client) UpdateProject

func (c *Client) UpdateProject(preq *ProjectRequest) (*Project, error)

UpdateProject changes the attributes of a project. Note that some fields like Workspace cannot be changed once the project has been created. Trying to modify this field will return an error.

func (*Client) UploadAttachment

func (c *Client) UploadAttachment(au *AttachmentUpload) (*Attachment, error)

UploadAtatchment uploads an attachment to a specific task. Its fields: TaskID and Body must be set otherwise it will return an error.

type CustomField

type CustomField map[string]interface{}

type HTTPError

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

func (HTTPError) Code

func (he HTTPError) Code() int

func (HTTPError) Error

func (he HTTPError) Error() string

type Layout

type Layout string
const (
	ListLayout  Layout = "list"
	BoardLayout Layout = "board"
)

func (*Layout) MarshalJSON

func (l *Layout) MarshalJSON() ([]byte, error)

func (*Layout) UnmarshalJSON

func (l *Layout) UnmarshalJSON(b []byte) error

type Membership

type Membership struct {
	Project *NamedAndIDdEntity `json:"project,omitempty"`
	Section *NamedAndIDdEntity `json:"section,omitempty"`
}

type Metadata

type Metadata map[string]interface{}

type NamedAndIDdEntity

type NamedAndIDdEntity struct {
	Name string `json:"name"`
	ID   int64  `json:"id"`
}

type Project

type Project struct {
	ID       int64              `json:"id,omitempty"`
	GID      string             `json:"gid,omitempty"`
	Team     *NamedAndIDdEntity `json:"team,omitempty"`
	Name     string             `json:"name,omitempty"`
	Notes    string             `json:"notes,omitempty"`
	Color    string             `json:"color,omitempty"`
	Archived bool               `json:"archived,omitempty"`

	Owner      *NamedAndIDdEntity `json:"owner,omitempty"`
	CreatedAt  *time.Time         `json:"created_at,omitempty"`
	ModifiedAt *time.Time         `json:"created_at,omitempty"`

	Workspace *NamedAndIDdEntity `json:"workspace,omitempty"`

	Members   []*NamedAndIDdEntity `json:"members,omitempty"`
	Followers []*NamedAndIDdEntity `json:"followers,omitempty"`
}

type ProjectQuery

type ProjectQuery struct {
	WorkspaceID string `json:"workspace,omitempty"`
	TeamID      string `json:"team,omitempty"`
	Archived    bool   `json:"archived,omitempty"`
}

type ProjectRequest

type ProjectRequest struct {
	ProjectID  string `json:"id"`
	ProjectGID string `json:"project_gid"`

	Name  string `json:"name,omitempty"`
	Notes string `json:"notes,omitempty"`

	Color  string `json:"color,omitempty"`
	Layout Layout `json:"layout,omitempty"`

	Team *NamedAndIDdEntity `json:"team,omitempty"`

	Workspace string `json:"workspace,omitempty"`

	PublicToOrganization bool     `json:"public,omitempty"`
	Members              []string `json:"members,omitempty"`
}

func (*ProjectRequest) Validate

func (preq *ProjectRequest) Validate() error

type ProjectsPage

type ProjectsPage struct {
	Projects []*Project `json:"data"`
	Err      error
}

type Task

type Task struct {
	ID          int64              `json:"id,omitempty"`
	Assignee    *NamedAndIDdEntity `json:"assignee,omitempty"`
	CreatedAt   *time.Time         `json:"created_at,omitempty"`
	Completed   bool               `json:"completed,omitempty"`
	CompletedAt *time.Time         `json:"completed_at,omitempty"`

	AssigneeStatus AssigneeStatus `json:"assignee_status,omitempty"`

	CustomFields []CustomField `json:"custom_fields,omitempty"`

	DueOn *YYYYMMDD  `json:"due_on,omitempty"`
	DueAt *time.Time `json:"due_at,omitempty"`

	Metadata Metadata `json:"external,omitempty"`

	Followers []*NamedAndIDdEntity `json:"followers,omitempty"`

	HeartedByMe bool                 `json:"hearted,omitempty"`
	Hearts      []*NamedAndIDdEntity `json:"hearts,omitempty"`
	HeartCount  int64                `json:"num_hearts,omitempty"`
	ModifiedAt  *time.Time           `json:"modified_at"`

	Name string `json:"name,omitempty"`

	Notes string `json:"notes,omitempty"`

	Projects   []*Project `json:"projects,omitempty"`
	ParentTask *Task      `json:"parent,omitempty"`

	Workspace *NamedAndIDdEntity `json:"workspace,omitempty"`

	Memberships []*Membership `json:"memberships,omitempty"`

	Tags []*NamedAndIDdEntity `json:"tags,omitempty"`
}

type TaskRequest

type TaskRequest struct {
	Page        int        `json:"page,omitempty"`
	Limit       int        `json:"limit,omitempty"`
	MaxRetries  int        `json:"max_retries,omitempty"`
	Assignee    string     `json:"assignee"`
	ProjectID   string     `json:"project,omitempty"`
	Workspace   string     `json:"workspace,omitempty"`
	ID          int64      `json:"id,omitempty"`
	CreatedAt   *time.Time `json:"created_at,omitempty"`
	Completed   bool       `json:"completed,omitempty"`
	CompletedAt *time.Time `json:"completed_at,omitempty"`

	AssigneeStatus AssigneeStatus `json:"assignee_status,omitempty"`

	CustomFields []CustomField `json:"custom_fields,omitempty"`

	DueOn *YYYYMMDD  `json:"due_on,omitempty"`
	DueAt *time.Time `json:"due_at,omitempty"`

	Metadata Metadata `json:"external,omitempty"`

	Followers []UserID `json:"followers,omitempty"`

	HeartedByMe bool       `json:"hearted,omitempty"`
	Hearts      []*User    `json:"hearts,omitempty"`
	HeartCount  int64      `json:"num_hearts,omitempty"`
	ModifiedAt  *time.Time `json:"modified_at"`

	Name string `json:"name,omitempty"`

	Notes string `json:"notes,omitempty"`

	Projects   []*NamedAndIDdEntity `json:"projects,omitempty"`
	ParentTask *Task                `json:"parent,omitempty"`

	Memberships []*Membership `json:"memberships,omitempty"`

	Tags []*NamedAndIDdEntity `json:"tags,omitempty"`
}

type TaskResultPage

type TaskResultPage struct {
	Tasks []*Task `json:"data"`
	Err   error
}

type Team

type Team struct {
	Name            string `json:"name"`
	ID              int64  `json:"id"`
	HtmlDescription string `json:"html_description"`
}

type TeamPage

type TeamPage struct {
	Teams []*Team `json:"data"`
	Err   error
}

type TeamRequest

type TeamRequest struct {
	// TeamID is a globally unique identifier for the team.
	TeamID string `json:"team_id"`

	UserID string `json:"user_id"`

	OrganizationID string `json:"organization"`
}

func (*TeamRequest) Validate

func (treq *TeamRequest) Validate() error

type User

type User struct {
	UID          UserID `json:"id"`
	GID          string `json:"gid"`
	Name         string `json:"name"`
	Email        string `json:"email"`
	ResourceType string `json:"resource_type"`
}

type UserID

type UserID int64

func (UserID) MarshalJSON

func (uid UserID) MarshalJSON() ([]byte, error)

func (UserID) String

func (uid UserID) String() string

type UsersPage

type UsersPage struct {
	Users []*User `json:"data"`
	Err   error
}

type Workspace

type Workspace NamedAndIDdEntity

type WorkspacePage

type WorkspacePage struct {
	Err        error
	Workspaces []*Workspace `json:"data,omitempty"`

	NextPage *pageToken `json:"next_page,omitempty"`
}

type YYYYMMDD

type YYYYMMDD struct {
	sync.RWMutex

	YYYY int64
	MM   int64
	DD   int64
	// contains filtered or unexported fields
}

func (*YYYYMMDD) MarshalJSON

func (ymd *YYYYMMDD) MarshalJSON() ([]byte, error)

func (*YYYYMMDD) String

func (ymd *YYYYMMDD) String() string

func (*YYYYMMDD) UnmarshalJSON

func (ymd *YYYYMMDD) UnmarshalJSON(b []byte) error

Jump to

Keyboard shortcuts

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