diff --git a/asanaclient/client.go b/asanaclient/client.go index b4f775d..92c83bd 100644 --- a/asanaclient/client.go +++ b/asanaclient/client.go @@ -116,14 +116,26 @@ func (c *Client) get(path string, values *url.Values, out interface{}) error { } func (c *Client) post(path string, body interface{}, out interface{}) error { + return c.doWithBody("POST", path, body, out) +} + +func (c *Client) put(path string, body interface{}, out interface{}) error { + return c.doWithBody("PUT", path, body, out) +} + +func (c *Client) doWithBody(method string, path string, body interface{}, out interface{}) error { url := fmt.Sprintf("%s%s", baseURL, path) - enc, err := json.Marshal(body) + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + + err := enc.Encode(body) if err != nil { return err } - req, err := http.NewRequest("POST", url, bytes.NewReader(enc)) + req, err := http.NewRequest(method, url, buf) if err != nil { return err } diff --git a/asanaclient/workspaceclient.go b/asanaclient/workspaceclient.go index 68d779c..5afc2bd 100644 --- a/asanaclient/workspaceclient.go +++ b/asanaclient/workspaceclient.go @@ -44,11 +44,11 @@ type Tag struct { } type Task struct { - GID string `json:"gid"` - Name string `json:"name"` - DueOn string `json:"due_on"` + GID string `json:"gid,omitempty"` + Name string `json:"name,omitempty"` + DueOn string `json:"due_on,omitempty"` ParsedDueOn *civil.Date `json:"-"` - HTMLNotes string `json:"html_notes"` + HTMLNotes string `json:"html_notes,omitempty"` ParsedHTMLNotes *html.Node `json:"-"` } @@ -94,6 +94,10 @@ type tagsResponse struct { Data []*Tag `json:"data"` } +type taskResponse struct { + Data *Task `json:"data"` +} + type tasksResponse struct { Data []*Task `json:"data"` } @@ -102,6 +106,10 @@ type userResponse struct { Data *User `json:"data"` } +type taskUpdate struct { + Data *Task `json:"data"` +} + func (wc *WorkspaceClient) GetMe() (*User, error) { resp := &userResponse{} err := wc.client.get("users/me", nil, resp) @@ -303,6 +311,24 @@ func (wc *WorkspaceClient) Search(q *SearchQuery) ([]*Task, error) { return resp.Data, nil } +func (wc *WorkspaceClient) UpdateTask(task *Task) error { + path := fmt.Sprintf("tasks/%s", task.GID) + + task.GID = "" + + update := &taskUpdate{ + Data: task, + } + + resp := &taskResponse{} + err := wc.client.put(path, update, resp) + if err != nil { + return err + } + + return nil +} + func (p *Project) String() string { return fmt.Sprintf("%s (%s)", p.GID, p.Name) } diff --git a/asanarules/rules.go b/asanarules/rules.go index 9f63a4e..5c990cd 100644 --- a/asanarules/rules.go +++ b/asanarules/rules.go @@ -1,5 +1,6 @@ package asanarules +import "bytes" import "fmt" import "math/rand" import "strings" @@ -8,6 +9,7 @@ import "time" import "cloud.google.com/go/civil" import "github.com/firestuff/asana-rules/asanaclient" import "golang.org/x/net/html" +import "golang.org/x/net/html/atom" type queryMutator func(*asanaclient.WorkspaceClient, *asanaclient.SearchQuery) error type taskActor func(*asanaclient.WorkspaceClient, *asanaclient.Task) error @@ -231,6 +233,30 @@ func (p *periodic) WithoutDue() *periodic { } // Task actors +func (p *periodic) FixUnlinkedURL() *periodic { + p.taskActors = append(p.taskActors, func(wc *asanaclient.WorkspaceClient, t *asanaclient.Task) error { + fixUnlinkedURL(t.ParsedHTMLNotes) + + buf := &bytes.Buffer{} + + err := html.Render(buf, t.ParsedHTMLNotes) + if err != nil { + return err + } + + notes := buf.String() + + update := &asanaclient.Task{ + GID: t.GID, + HTMLNotes: strings.TrimSuffix(strings.TrimPrefix(notes, ""), ""), + } + + return wc.UpdateTask(update) + }) + + return p +} + func (p *periodic) MoveToMyTasksSection(name string) *periodic { p.taskActors = append(p.taskActors, func(wc *asanaclient.WorkspaceClient, t *asanaclient.Task) error { utl, err := wc.GetMyUserTaskList() @@ -344,6 +370,79 @@ func (p *periodic) exec(c *asanaclient.Client) error { } // Helpers +func fixUnlinkedURL(node *html.Node) { + if node == nil { + return + } + + if node.Type == html.ElementNode && node.Data == "a" { + // Don't go down this tree, since it's a link + return + } + + if node.Type == html.TextNode { + accum := []string{} + nodes := []*html.Node{} + + for _, line := range strings.Split(node.Data, "\n") { + if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") { + if len(accum) > 0 { + accum = append(accum, "") // Trailing newline + nodes = append(nodes, &html.Node{ + Type: html.TextNode, + Data: strings.Join(accum, "\n"), + }) + accum = []string{""} + } + + nodes = append(nodes, &html.Node{ + Type: html.ElementNode, + Data: "a", + DataAtom: atom.A, + Attr: []html.Attribute{ + html.Attribute{ + Key: "href", + Val: line, + }, + }, + FirstChild: &html.Node{ + Type: html.TextNode, + Data: line, + }, + }) + } else { + accum = append(accum, line) + } + } + + if len(nodes) == 0 { + return + } + + nodes = append(nodes, &html.Node{ + Type: html.TextNode, + Data: strings.Join(accum, "\n"), + }) + + for i, iter := range nodes { + if i == len(nodes)-1 { + // Last node + iter.NextSibling = node.NextSibling + } else { + iter.NextSibling = nodes[i+1] + } + } + + node.Data = "" + node.NextSibling = nodes[0] + + return + } + + fixUnlinkedURL(node.FirstChild) + fixUnlinkedURL(node.NextSibling) +} + func hasUnlinkedURL(node *html.Node) bool { if node == nil { return false @@ -354,9 +453,12 @@ func hasUnlinkedURL(node *html.Node) bool { return false } - if node.Type == html.TextNode && (strings.HasPrefix(node.Data, "http://") || - strings.HasPrefix(node.Data, "https://")) { - return true + if node.Type == html.TextNode { + for _, line := range strings.Split(node.Data, "\n") { + if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") { + return true + } + } } if hasUnlinkedURL(node.FirstChild) { diff --git a/main.go b/main.go index 9ff4b8f..7c8ce8a 100644 --- a/main.go +++ b/main.go @@ -54,5 +54,13 @@ func main() { PrintTasks(). MoveToMyTasksSection("Someday") + EverySeconds(30). + InWorkspace("flamingcow.io"). + InMyTasksSections("Recently Assigned", "Today", "Meetings", "Maybe Today", "Tonight", "Upcoming", "Later", "Someday"). + OnlyIncomplete(). + WithUnlinkedURL(). + PrintTasks(). + FixUnlinkedURL() + Loop() }