From 305b1a0f98ed543889aab4bb564367c772c63082 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 12 Sep 2021 20:08:13 +0000 Subject: [PATCH] Real rules language --- asanaclient/client.go | 247 ++++----------------------------- asanaclient/workspaceclient.go | 232 +++++++++++++++++++++++++++++++ asanarules/rules.go | 152 ++++++++++++++++++-- main.go | 64 +-------- 4 files changed, 406 insertions(+), 289 deletions(-) create mode 100644 asanaclient/workspaceclient.go diff --git a/asanaclient/client.go b/asanaclient/client.go index 77e4d96..fe475a7 100644 --- a/asanaclient/client.go +++ b/asanaclient/client.go @@ -7,90 +7,20 @@ import "io/ioutil" import "net/http" import "net/url" import "os" -import "strings" -import "cloud.google.com/go/civil" import "github.com/firestuff/asana-rules/headers" -import "golang.org/x/net/html" - -var _TRUE = true -var TRUE = &_TRUE -var _FALSE = false -var FALSE = &_FALSE type Client struct { client *http.Client } -type SearchQuery struct { - SectionsAny []*Section - Completed *bool -} - -type Project struct { +type workspace struct { GID string `json:"gid"` Name string `json:"name"` } -type Section struct { - GID string `json:"gid"` - Name string `json:"name"` -} - -type Task struct { - GID string `json:"gid"` - Name string `json:"name"` - DueOn string `json:"due_on"` - ParsedDueOn *civil.Date `json:"-"` - HTMLNotes string `json:"html_notes"` - ParsedHTMLNotes *html.Node `json:"-"` -} - -type User struct { - GID string `json:"gid"` - Name string `json:"name"` - Email string `json:"email"` -} - -type Workspace struct { - GID string `json:"gid"` - Name string `json:"name"` -} - -type addTaskDetails struct { - Task string `json:"task"` -} - -type addTaskRequest struct { - Data *addTaskDetails `json:"data"` -} - -type emptyResponse struct { - Data interface{} `json:"data"` -} - -type projectResponse struct { - Data *Project `json:"data"` -} - -type projectsResponse struct { - Data []*Project `json:"data"` -} - -type sectionsResponse struct { - Data []*Section `json:"data"` -} - -type tasksResponse struct { - Data []*Task `json:"data"` -} - -type userResponse struct { - Data *User `json:"data"` -} - type workspacesResponse struct { - Data []*Workspace `json:"data"` + Data []*workspace `json:"data"` } func NewClient(token string) *Client { @@ -109,76 +39,19 @@ func NewClientFromEnv() *Client { return NewClient(os.Getenv("ASANA_TOKEN")) } -func (c *Client) AddTaskToSection(task *Task, section *Section) error { - req := &addTaskRequest{ - Data: &addTaskDetails{ - Task: task.GID, - }, - } +func (c *Client) InWorkspace(name string) (*WorkspaceClient, error) { + wrk, err := c.getWorkspaceByName(name) + if err != nil { + return nil, err + } - resp := &emptyResponse{} - - path := fmt.Sprintf("sections/%s/addTask", section.GID) - err := c.post(path, req, resp) - if err != nil { - return err - } - - return nil + return &WorkspaceClient{ + client: c, + workspace: wrk, + }, nil } -func (c *Client) GetMe() (*User, error) { - resp := &userResponse{} - err := c.get("users/me", nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) GetProjects(workspace *Workspace) ([]*Project, error) { - path := fmt.Sprintf("workspaces/%s/projects", workspace.GID) - resp := &projectsResponse{} - err := c.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) GetSections(project *Project) ([]*Section, error) { - path := fmt.Sprintf("projects/%s/sections", project.GID) - resp := §ionsResponse{} - err := c.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) GetTasksFromSection(section *Section) ([]*Task, error) { - path := fmt.Sprintf("sections/%s/tasks", section.GID) - resp := &tasksResponse{} - err := c.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) GetUserTaskList(user *User, workspace *Workspace) (*Project, error) { - path := fmt.Sprintf("users/%s/user_task_list", user.GID) - values := &url.Values{} - values.Add("workspace", workspace.GID) - resp := &projectResponse{} - err := c.get(path, values, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) GetWorkspaces() ([]*Workspace, error) { +func (c *Client) getWorkspaces() ([]*workspace, error) { resp := &workspacesResponse{} err := c.get("workspaces", nil, resp) if err != nil { @@ -187,54 +60,19 @@ func (c *Client) GetWorkspaces() ([]*Workspace, error) { return resp.Data, nil } -// Returns one workspace if there is only one -func (c *Client) GetWorkspace() (*Workspace, error) { - workspaces, err := c.GetWorkspaces() +func (c *Client) getWorkspaceByName(name string) (*workspace, error) { + wrks, err := c.getWorkspaces() if err != nil { return nil, err } - if len(workspaces) != 1 { - return nil, fmt.Errorf("%d workspaces found", len(workspaces)) - } - - return workspaces[0], nil -} - -func (c *Client) Search(workspace *Workspace, q *SearchQuery) ([]*Task, error) { - path := fmt.Sprintf("workspaces/%s/tasks/search", workspace.GID) - - values := &url.Values{} - - values.Add("opt_fields", "due_on,html_notes,name") - - if len(q.SectionsAny) > 0 { - gids := []string{} - for _, sec := range q.SectionsAny { - gids = append(gids, sec.GID) - } - values.Add("sections.any", strings.Join(gids, ",")) - } - - if q.Completed != nil { - values.Add("completed", fmt.Sprintf("%t", *q.Completed)) - } - - resp := &tasksResponse{} - err := c.get(path, values, resp) - if err != nil { - return nil, err - } - - for _, task := range resp.Data { - err := task.parse() - if err != nil { - return nil, err - } - } - - return resp.Data, nil + for _, wrk := range wrks { + if wrk.Name == name { + return wrk, nil + } + } + return nil, fmt.Errorf("Workspace `%s` not found", name) } const baseURL = "https://app.asana.com/api/1.0/" @@ -257,15 +95,17 @@ func (c *Client) get(path string, values *url.Values, out interface{}) error { return err } + dec := json.NewDecoder(resp.Body) + if resp.StatusCode != 200 { - body, err := ioutil.ReadAll(resp.Body) + errorResp := &errorResponse{} + err = dec.Decode(errorResp) if err != nil { return err } - return fmt.Errorf("%s: %s", resp.Status, string(body)) + return fmt.Errorf("%s: %s", resp.Status, errorResp.Errors[0].Message) } - dec := json.NewDecoder(resp.Body) err = dec.Decode(out) if err != nil { return err @@ -309,41 +149,6 @@ func (c *Client) post(path string, body interface{}, out interface{}) error { return nil } -func (p *Project) String() string { - return fmt.Sprintf("%s (%s)", p.GID, p.Name) -} - -func (s *Section) String() string { - return fmt.Sprintf("%s (%s)", s.GID, s.Name) -} - -func (t *Task) String() string { - return fmt.Sprintf("%s (%s)", t.GID, t.Name) -} - -func (u *User) String() string { - return fmt.Sprintf("%s (%s <%s>)", u.GID, u.Name, u.Email) -} - -func (wrk *Workspace) String() string { +func (wrk *workspace) String() string { return fmt.Sprintf("%s (%s)", wrk.GID, wrk.Name) } - -func (t *Task) parse() error { - r := strings.NewReader(t.HTMLNotes) - root, err := html.Parse(r) - if err != nil { - return err - } - t.ParsedHTMLNotes = root - - if t.DueOn != "" { - d, err := civil.ParseDate(t.DueOn) - if err != nil { - return err - } - t.ParsedDueOn = &d - } - - return nil -} diff --git a/asanaclient/workspaceclient.go b/asanaclient/workspaceclient.go new file mode 100644 index 0000000..9d4546d --- /dev/null +++ b/asanaclient/workspaceclient.go @@ -0,0 +1,232 @@ +package asanaclient + +import "fmt" +import "net/url" +import "strings" + +import "cloud.google.com/go/civil" +import "golang.org/x/net/html" + +var _TRUE = true +var TRUE = &_TRUE +var _FALSE = false +var FALSE = &_FALSE + +type WorkspaceClient struct { + client *Client + workspace *workspace +} + +type SearchQuery struct { + SectionsAny []*Section + Completed *bool + DueOn *string +} + +type Project struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type Section struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type Task struct { + GID string `json:"gid"` + Name string `json:"name"` + DueOn string `json:"due_on"` + ParsedDueOn *civil.Date `json:"-"` + HTMLNotes string `json:"html_notes"` + ParsedHTMLNotes *html.Node `json:"-"` +} + +type User struct { + GID string `json:"gid"` + Name string `json:"name"` + Email string `json:"email"` +} + +type addTaskDetails struct { + Task string `json:"task"` +} + +type addTaskRequest struct { + Data *addTaskDetails `json:"data"` +} + +type emptyResponse struct { + Data interface{} `json:"data"` +} + +type errorDetails struct { + Message string `json:"message"` +} + +type errorResponse struct { + Errors []*errorDetails `json:"errors"` +} + +type projectResponse struct { + Data *Project `json:"data"` +} + +type projectsResponse struct { + Data []*Project `json:"data"` +} + +type sectionsResponse struct { + Data []*Section `json:"data"` +} + +type tasksResponse struct { + Data []*Task `json:"data"` +} + +type userResponse struct { + Data *User `json:"data"` +} + +func (wc *WorkspaceClient) GetMe() (*User, error) { + resp := &userResponse{} + err := wc.client.get("users/me", nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) AddTaskToSection(task *Task, section *Section) error { + req := &addTaskRequest{ + Data: &addTaskDetails{ + Task: task.GID, + }, + } + + resp := &emptyResponse{} + + path := fmt.Sprintf("sections/%s/addTask", section.GID) + err := wc.client.post(path, req, resp) + if err != nil { + return err + } + + return nil +} + +func (wc *WorkspaceClient) GetProjects() ([]*Project, error) { + path := fmt.Sprintf("workspaces/%s/projects", wc.workspace.GID) + resp := &projectsResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetSections(project *Project) ([]*Section, error) { + path := fmt.Sprintf("projects/%s/sections", project.GID) + resp := §ionsResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetTasksFromSection(section *Section) ([]*Task, error) { + path := fmt.Sprintf("sections/%s/tasks", section.GID) + resp := &tasksResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetUserTaskList(user *User) (*Project, error) { + path := fmt.Sprintf("users/%s/user_task_list", user.GID) + values := &url.Values{} + values.Add("workspace", wc.workspace.GID) + resp := &projectResponse{} + err := wc.client.get(path, values, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) Search(q *SearchQuery) ([]*Task, error) { + path := fmt.Sprintf("workspaces/%s/tasks/search", wc.workspace.GID) + + values := &url.Values{} + + values.Add("opt_fields", "due_on,html_notes,name") + + if len(q.SectionsAny) > 0 { + gids := []string{} + for _, sec := range q.SectionsAny { + gids = append(gids, sec.GID) + } + values.Add("sections.any", strings.Join(gids, ",")) + } + + if q.Completed != nil { + values.Add("completed", fmt.Sprintf("%t", *q.Completed)) + } + + if q.DueOn != nil { + values.Add("due_on", *q.DueOn) + } + + resp := &tasksResponse{} + err := wc.client.get(path, values, resp) + if err != nil { + return nil, err + } + + for _, task := range resp.Data { + err := task.parse() + if err != nil { + return nil, err + } + } + + return resp.Data, nil +} + +func (p *Project) String() string { + return fmt.Sprintf("%s (%s)", p.GID, p.Name) +} + +func (s *Section) String() string { + return fmt.Sprintf("%s (%s)", s.GID, s.Name) +} + +func (t *Task) String() string { + return fmt.Sprintf("%s (%s)", t.GID, t.Name) +} + +func (u *User) String() string { + return fmt.Sprintf("%s (%s <%s>)", u.GID, u.Name, u.Email) +} + +func (t *Task) parse() error { + r := strings.NewReader(t.HTMLNotes) + root, err := html.Parse(r) + if err != nil { + return err + } + t.ParsedHTMLNotes = root + + if t.DueOn != "" { + d, err := civil.ParseDate(t.DueOn) + if err != nil { + return err + } + t.ParsedDueOn = &d + } + + return nil +} diff --git a/asanarules/rules.go b/asanarules/rules.go index c151e9a..ab69e16 100644 --- a/asanarules/rules.go +++ b/asanarules/rules.go @@ -3,9 +3,20 @@ package asanarules import "fmt" import "time" +import "cloud.google.com/go/civil" +import "github.com/firestuff/asana-rules/asanaclient" + +type queryMutator func(*asanaclient.WorkspaceClient, *asanaclient.SearchQuery) error +type taskActor func(*asanaclient.WorkspaceClient, *asanaclient.Task) error +type workspaceClientGetter func(*asanaclient.Client) (*asanaclient.WorkspaceClient, error) + type periodic struct { duration time.Duration done chan bool + + workspaceClientGetter workspaceClientGetter + queryMutators []queryMutator + taskActors []taskActor } var periodics = []*periodic{} @@ -22,8 +33,10 @@ func Every(d time.Duration) *periodic { } func Loop() { + client := asanaclient.NewClientFromEnv() + for _, periodic := range periodics { - periodic.start() + periodic.start(client) } for _, periodic := range periodics { @@ -31,29 +44,150 @@ func Loop() { } } -func (p *periodic) MyTasks() *periodic { - return p +func (p *periodic) InMyTasksSections(names ...string) *periodic { + p.queryMutators = append(p.queryMutators, func(wc *asanaclient.WorkspaceClient, q *asanaclient.SearchQuery) error { + me, err := wc.GetMe() + if err != nil { + return err + } + + utl, err := wc.GetUserTaskList(me) + if err != nil { + return err + } + + secs, err := wc.GetSections(utl) + if err != nil { + return err + } + + secsByName := map[string]*asanaclient.Section{} + for _, sec := range secs { + secsByName[sec.Name] = sec + } + + for _, name := range names { + sec, found := secsByName[name] + if !found { + return fmt.Errorf("Section '%s' not found", name) + } + + q.SectionsAny = append(q.SectionsAny, sec) + } + + return nil + }) + + return p } -func (p *periodic) start() { - go p.loop() +func (p *periodic) DueInDays(days int) *periodic { + p.queryMutators = append(p.queryMutators, func(wc *asanaclient.WorkspaceClient, q *asanaclient.SearchQuery) error { + d := civil.DateOf(time.Now()) + d = d.AddDays(days) + dueOn := d.String() + q.DueOn = &dueOn + return nil + }) + + return p +} + +func (p *periodic) InWorkspace(name string) *periodic { + p.workspaceClientGetter = func(c *asanaclient.Client) (*asanaclient.WorkspaceClient, error) { + return c.InWorkspace(name) + } + + return p +} + +func (p *periodic) OnlyIncomplete() *periodic { + p.queryMutators = append(p.queryMutators, func(wc *asanaclient.WorkspaceClient, q *asanaclient.SearchQuery) error { + q.Completed = asanaclient.FALSE + return nil + }) + + return p +} + +func (p *periodic) OnlyComplete() *periodic { + p.queryMutators = append(p.queryMutators, func(wc *asanaclient.WorkspaceClient, q *asanaclient.SearchQuery) error { + q.Completed = asanaclient.TRUE + return nil + }) + + return p +} + +func (p *periodic) PrintTasks() *periodic { + p.taskActors = append(p.taskActors, func(wc *asanaclient.WorkspaceClient, t *asanaclient.Task) error { + fmt.Printf("%s\n", t) + return nil + }) + + return p +} + +func (p *periodic) start(client *asanaclient.Client) { + err := p.validate() + if err != nil { + panic(err) + } + + go p.loop(client) +} + +func (p *periodic) validate() error { + return nil } func (p *periodic) wait() { <-p.done } -func (p *periodic) loop() { +func (p *periodic) loop(client *asanaclient.Client) { ticker := time.NewTicker(p.duration) for { <-ticker.C - p.exec() + err := p.exec(client) + if err != nil { + fmt.Printf("%s\n", err) + // continue + } } close(p.done) } -func (p *periodic) exec() { - fmt.Printf("exec\n") +func (p *periodic) exec(c *asanaclient.Client) error { + wc, err := p.workspaceClientGetter(c) + if err != nil { + return err + } + + q := &asanaclient.SearchQuery{} + + for _, mut := range p.queryMutators { + err = mut(wc, q) + if err != nil { + return err + } + } + + tasks, err := wc.Search(q) + if err != nil { + return err + } + + for _, task := range tasks { + for _, act := range p.taskActors { + err = act(wc, task) + if err != nil { + return err + } + } + } + + return nil } diff --git a/main.go b/main.go index 649a5bf..6e23970 100644 --- a/main.go +++ b/main.go @@ -1,70 +1,16 @@ package main -// import "fmt" import "time" import . "github.com/firestuff/asana-rules/asanarules" -// import "github.com/firestuff/asana-rules/asanaclient" - func main() { Every(5 * time.Second). - MyTasks() + InWorkspace("flamingcow.io"). + InMyTasksSections("Recently Assigned"). + OnlyIncomplete(). + DueInDays(0). + PrintTasks() Loop() - /* - a := asana.NewClientFromEnv() - - me, err := a.GetMe() - if err != nil { - panic(err) - } - - fmt.Printf("User: %s\n", me) - - wrk, err := a.GetWorkspace() - if err != nil { - panic(err) - } - - fmt.Printf("Workspace: %s\n", wrk) - - utl, err := a.GetUserTaskList(me, wrk) - if err != nil { - panic(err) - } - - fmt.Printf("User Task List: %s\n", utl) - - secs, err := a.GetSections(utl) - if err != nil { - panic(err) - } - - fmt.Printf("Sections:\n") - for _, sec := range secs { - fmt.Printf("\t%s\n", sec) - - if sec.Name != "Recently Assigned" { - continue - } - - q := &asana.SearchQuery{ - SectionsAny: []*asana.Section{sec}, - Completed: asana.FALSE, - } - - tasks, err := a.Search(wrk, q) - if err != nil { - panic(err) - } - - for _, task := range tasks { - fmt.Printf("\t\t%s\n", task) - a.AddTaskToSection(task, &asana.Section{ - GID: "1200372179004456", - }) - } - } - */ }