Skip to content

Commit bcae8f1

Browse files
committed
🚧 WIP Asana plugin
1 parent 5f1b09b commit bcae8f1

File tree

9 files changed

+957
-0
lines changed

9 files changed

+957
-0
lines changed

plugins/asana/.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Go template downloaded with gut
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
*.test
8+
*.out
9+
go.work
10+
.gut
11+
12+
# Dev files
13+
*.log
14+
.init
15+
16+
dist/
17+
18+
devManifest.json

plugins/asana/.goreleaser.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
version: 2
3+
4+
before:
5+
hooks:
6+
# You may remove this if you don't use go modules.
7+
- go mod tidy
8+
9+
builds:
10+
- env:
11+
- CGO_ENABLED=0
12+
goos:
13+
- linux
14+
- windows
15+
- darwin
16+
binary: asana
17+
id: anyquery
18+
ldflags: "-s -w"
19+
flags: # To ensure reproducible builds
20+
- -trimpath
21+
22+
goarch:
23+
- amd64
24+
- arm64
25+
26+
archives:
27+
- format: binary
28+
29+
changelog:
30+
sort: asc
31+
filters:
32+
exclude:
33+
- "^docs:"
34+
- "^test:"

plugins/asana/Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
files := $(wildcard *.go)
3+
4+
all: $(files)
5+
go build -o asana.out $(files)
6+
7+
prod: $(files)
8+
go build -o asana.out -ldflags "-s -w" $(files)
9+
10+
release: prod
11+
goreleaser build -f .goreleaser.yaml --clean --snapshot
12+
13+
clean:
14+
rm -f asana.out
15+
16+
.PHONY: all clean

plugins/asana/asana_client.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"github.com/go-resty/resty/v2"
8+
)
9+
10+
type TasksResponse struct {
11+
Data []Task `json:"data"`
12+
}
13+
14+
type TaskResponse struct {
15+
Data Task `json:"data"`
16+
}
17+
18+
type TasksQueryNextPage struct {
19+
Offset string `json:"offset"`
20+
}
21+
22+
type TasksQueryResponse struct {
23+
Data []Task `json:"data"`
24+
NextPage TasksQueryNextPage `json:"next_page"`
25+
}
26+
27+
// AsanaClient is a client for interacting with the Asana API.
28+
type AsanaClient struct {
29+
client *resty.Client
30+
token string
31+
baseURL string
32+
}
33+
34+
// NewAsanaClient creates and returns a new AsanaClient using the provided personal access token.
35+
func NewAsanaClient(token string) *AsanaClient {
36+
c := resty.New()
37+
c = c.SetBaseURL("https://app.asana.com/api/1.0")
38+
c.SetHeader("Authorization", "Bearer "+token)
39+
c.SetHeader("Accept", "application/json")
40+
return &AsanaClient{
41+
client: c,
42+
token: token,
43+
baseURL: "https://app.asana.com/api/1.0",
44+
}
45+
}
46+
47+
// Task represents an Asana task with the most common fields.
48+
type Task struct {
49+
Gid string `json:"gid,omitempty"` // Globally unique task identifier.
50+
Name string `json:"name,omitempty"` // The name of the task.
51+
Completed bool `json:"completed,omitempty"` // Task completion status.
52+
Liked bool `json:"liked,omitempty"` // Task like status.
53+
Assignee *User `json:"assignee,omitempty"` // The user assigned to the task.
54+
DueOn string `json:"due_on,omitempty"` // Task due date (YYYY-MM-DD format).
55+
DueAt string `json:"due_at,omitempty"` // Task due date (YYYY-MM-DDTHH:MM:SS format).
56+
StartAt string `json:"start_at,omitempty"` // Task start date (YYYY-MM-DD format).
57+
StartOn string `json:"start_on,omitempty"` // Task start date (YYYY-MM-DDTHH:MM:SS format).
58+
Parent string `json:"parent,omitempty"` // Parent task identifier.
59+
Notes string `json:"notes,omitempty"` // Task description or notes.
60+
Memberships []Memberships `json:"memberships,omitempty"` // Project memberships.s
61+
CreatedAt string `json:"created_at,omitempty"` // Timestamp when the task was created.
62+
UpdatedAt string `json:"modified_at,omitempty"` // Timestamp when the task was last modified.
63+
ResourceSubtype string `json:"resource_subtype,omitempty"`
64+
CustomFields []CustomField `json:"custom_fields,omitempty"`
65+
66+
// Internal fields for CreateTask and UpdateTask
67+
Project string `json:"project,omitempty"`
68+
}
69+
70+
type Memberships struct {
71+
Project ProjectSection `json:"project,omitempty"`
72+
Section ProjectSection `json:"section,omitempty"`
73+
}
74+
75+
type ProjectSection struct {
76+
Gid string `json:"gid,omitempty"` // Globally unique project identifier.
77+
Name string `json:"name,omitempty"` // The name of the project.
78+
}
79+
80+
// User represents an Asana user.
81+
type User struct {
82+
Gid string `json:"gid"` // Globally unique user identifier.
83+
Name string `json:"name"` // The name of the user.
84+
}
85+
86+
type CustomField struct {
87+
Gid string `json:"gid,omitempty"`
88+
Name string `json:"name,omitempty"`
89+
DisplayValue *string `json:"display_value,omitempty"`
90+
}
91+
92+
// GetTasks retrieves tasks from Asana.
93+
func (ac *AsanaClient) GetTasks() ([]Task, error) {
94+
resp, err := ac.client.R().
95+
SetResult(&TasksResponse{}).
96+
Get("/tasks")
97+
if err != nil {
98+
return nil, err
99+
}
100+
result := resp.Result().(*TasksResponse)
101+
return result.Data, nil
102+
}
103+
104+
// CreateTask creates a new task in Asana.
105+
func (ac *AsanaClient) CreateTask(task Task) (*Task, error) {
106+
payload := map[string]interface{}{
107+
"data": map[string]interface{}{
108+
"projects": task.Project,
109+
"name": task.Name,
110+
"notes": task.Notes,
111+
"due_on": task.DueAt,
112+
// Additional fields can be added here.
113+
"resource_type": "task",
114+
},
115+
}
116+
resp, err := ac.client.R().
117+
SetBody(payload).
118+
SetResult(&TaskResponse{}).
119+
Post("/tasks")
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
if resp.IsError() {
125+
return nil, fmt.Errorf("failed to create task (%d): %s", resp.StatusCode(), resp.String())
126+
}
127+
128+
result := resp.Result().(*TaskResponse)
129+
return &result.Data, nil
130+
}
131+
132+
// UpdateTask updates an existing task in Asana.
133+
func (ac *AsanaClient) UpdateTask(id string, task Task) (*Task, error) {
134+
payload := map[string]interface{}{
135+
"data": map[string]interface{}{
136+
/* "name": task.Name,
137+
"notes": task.Notes,
138+
"due_on": task.DueOn,
139+
"completed": task.Completed, */
140+
},
141+
}
142+
if task.Name != "" {
143+
payload["data"].(map[string]interface{})["name"] = task.Name
144+
}
145+
if task.Notes != "" {
146+
payload["data"].(map[string]interface{})["notes"] = task.Notes
147+
}
148+
if task.DueAt != "" {
149+
payload["data"].(map[string]interface{})["due_on"] = task.DueAt
150+
}
151+
if task.Completed {
152+
payload["data"].(map[string]interface{})["completed"] = task.Completed
153+
}
154+
if task.Liked {
155+
payload["data"].(map[string]interface{})["liked"] = task.Liked
156+
}
157+
if task.StartAt != "" {
158+
payload["data"].(map[string]interface{})["start_on"] = task.StartAt
159+
}
160+
161+
log.Printf("Payload: %v (%s)", payload, id)
162+
resp, err := ac.client.R().
163+
SetBody(payload).
164+
SetResult(&TaskResponse{}).
165+
Put(fmt.Sprintf("/tasks/%s", id))
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
if resp.IsError() {
171+
return nil, fmt.Errorf("failed to update task (%d): %s", resp.StatusCode(), resp.String())
172+
}
173+
result := resp.Result().(*TaskResponse)
174+
return &result.Data, nil
175+
}
176+
177+
// DeleteTask deletes a task in Asana.
178+
func (ac *AsanaClient) DeleteTask(id string) error {
179+
resp, err := ac.client.R().
180+
Delete(fmt.Sprintf("/tasks/%s", id))
181+
182+
if err != nil {
183+
return err
184+
}
185+
186+
if resp.IsError() {
187+
return fmt.Errorf("failed to delete task (%d): %s", resp.StatusCode(), resp.String())
188+
}
189+
190+
return nil
191+
}

plugins/asana/go.mod

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module github.com/anyquery/plugins/asana
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/go-resty/resty/v2 v2.16.5
7+
github.com/julien040/anyquery v0.1.6
8+
)
9+
10+
require (
11+
github.com/adrg/xdg v0.5.3 // indirect
12+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
13+
github.com/dgraph-io/badger/v4 v4.4.0 // indirect
14+
github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect
15+
github.com/dustin/go-humanize v1.0.1 // indirect
16+
github.com/fatih/color v1.18.0 // indirect
17+
github.com/gogo/protobuf v1.3.2 // indirect
18+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
19+
github.com/golang/protobuf v1.5.4 // indirect
20+
github.com/google/flatbuffers v24.3.25+incompatible // indirect
21+
github.com/hashicorp/go-hclog v1.6.3 // indirect
22+
github.com/hashicorp/go-plugin v1.6.3 // indirect
23+
github.com/hashicorp/yamux v0.1.2 // indirect
24+
github.com/klauspost/compress v1.17.11 // indirect
25+
github.com/mattn/go-colorable v0.1.14 // indirect
26+
github.com/mattn/go-isatty v0.0.20 // indirect
27+
github.com/oklog/run v1.1.0 // indirect
28+
github.com/pkg/errors v0.9.1 // indirect
29+
github.com/stretchr/testify v1.10.0 // indirect
30+
go.opencensus.io v0.24.0 // indirect
31+
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
32+
golang.org/x/net v0.35.0 // indirect
33+
golang.org/x/sys v0.30.0 // indirect
34+
golang.org/x/text v0.22.0 // indirect
35+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
36+
google.golang.org/grpc v1.70.0 // indirect
37+
google.golang.org/protobuf v1.36.5 // indirect
38+
)

0 commit comments

Comments
 (0)