From 97c9ed38864eb442d248eada09226dac7e0807de Mon Sep 17 00:00:00 2001 From: Lineu Felipe Date: Thu, 5 Jul 2018 00:47:35 -0300 Subject: [PATCH 1/2] add todoist widget --- Gopkg.lock | 8 +- Gopkg.toml | 4 + _site/content/posts/modules/todoist.md | 88 ++++++++++++++ _site/static/imgs/modules/todoist.png | Bin 0 -> 3335 bytes .../hyde-hyde/layouts/partials/sidebar.html | 1 + todoist/display.go | 64 ++++++++++ todoist/list.go | 79 ++++++++++++ todoist/widget.go | 113 ++++++++++++++++++ wtf.go | 4 +- 9 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 _site/content/posts/modules/todoist.md create mode 100644 _site/static/imgs/modules/todoist.png create mode 100644 todoist/display.go create mode 100644 todoist/list.go create mode 100644 todoist/widget.go diff --git a/Gopkg.lock b/Gopkg.lock index 9211154d..8a0e37d8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,12 @@ packages = ["."] revision = "6a9abf92e34f4de62ac671caee3143f10b98892d" +[[projects]] + branch = "master" + name = "github.com/darkSasori/todoist" + packages = ["."] + revision = "ec6b38b374ab9c60cc9716d2083ae66eb9383d03" + [[projects]] name = "github.com/davecgh/go-spew" packages = ["spew"] @@ -201,6 +207,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a7a00554f9040d7617458773eafa64b82f9502eace145152cb50eb082800e936" + inputs-digest = "b2141b5945354e95e2f3e8f1f6eb182de11e21f0fe1188862c6dc57983c8cbc4" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 6221a4d7..9261787d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,6 +85,10 @@ branch = "master" name = "github.com/adlio/trello" +[[constraint]] + branch = "master" + name = "github.com/darkSasori/todoist" + [prune] go-tests = true unused-packages = true diff --git a/_site/content/posts/modules/todoist.md b/_site/content/posts/modules/todoist.md new file mode 100644 index 00000000..bb3beb11 --- /dev/null +++ b/_site/content/posts/modules/todoist.md @@ -0,0 +1,88 @@ +--- +title: "Todoist" +date: 2018-07-05T22:55:55-03:00 +draft: false +--- + +Displays all itens on specified project. + +todoist screenshot + +## Source Code + +```bash +wtf/todoist/ +``` + +## Required ENV Variables + +Key: `WTF_TODOIST_TOKEN`
+Value: Your Todoist API Token.
+ +_You can get your API Token at: todoist.com/prefs/integrations._ + +## Keyboard Commands + +Key: `h`
+Action: Show the previous project. + +Key: `←`
+Action: Show the previous project. + +Key: `l`
+Action: Show the next project. + +Key: `→`
+Action: Show the next project. + +Key: `j`
+Action: Select the next item in the list. + +Key: `↓`
+Action: Select the next item in the list. + +Key: `k`
+Action: Select the previous item in the list. + +Key: `↑`
+Action: Select the previous item in the list. + +Key: `c`
+Action: Close current item. + +Key: `d`
+Action: Delete current item. + +Key: `r`
+Action: Reload all projects. + +## Configuration + +```yaml +todoist: + projects: + - project_id + enabled: true + position: + height: 1 + left: 2 + top: 0 + width: 1 + refreshInterval: 3600 +``` + +### Attributes + +`enabled`
+Determines whether or not this module is executed and if its data displayed onscreen.
+Values: `true`, `false`. + +`projects`
+The todoist projects to fetch items from.
+ +`refreshInterval`
+How often, in seconds, this module will update its data.
+Values: A positive integer, `0..n`. + +`position`
+Where in the grid this module's widget will be displayed.
diff --git a/_site/static/imgs/modules/todoist.png b/_site/static/imgs/modules/todoist.png new file mode 100644 index 0000000000000000000000000000000000000000..086d0ec8673a4f407e2b0c382fd9e547711a0909 GIT binary patch literal 3335 zcmeHK=Q|r*8xLBvwOW)uXpEK)qtq&DRqPl|Q8QX2_KrPDNs6ZwwTs8D{lt94hzh0k zSPf#u4$_)Mh!Jo63Gb)(%X?qfIrljq&hOmUz0U8%n;7Y^Uj5@L003as)75$o0MJ>} z*4@nXwArPB97j`Ed~_}S0RXn1i$RzEkc|%jU}@FU(s&X4X_MgT!3m4$+J&i*FYzmH z%k$M}iRjd@m0k}r%eiVa?ibMZN!XXzT#;oiS+G@e)s5lo3v@h>g1YGp zKfZ0SI)7_sStsPQM}m%KA^`Blu9?7_rh7t0GGl_(NS^wDpb*cs`gPm-#us{C7RnArGrsT#}GaP!20 zCFO;I{(ygZ5lTqLm-fdh=`C73ia${jp&MC>dqH6j*RGRo!yv`Vn0a?yb1Ew z_EwlR&vW}Wm%7vBvu{4uJa_Xp^ZXQC=Nm*^a)1T5-Rmw_`dZ3juiCCj_j>N2xtZAk zdB$5uIK>-tMs8ipud1!>^j${aNT;WJ!^NZP@tk6>x8)3<)YsK{e;?E?Hbr7h&COSL zgKKSCQ-@$BN@kwffhTHSe6K`gcqycA;}E%!XeoN(2QQG785O3+vH^Ext2}>Of%Aer z2V?fj443K_ii(7>kHR}Kr@-GsdCI-Ly^)yt?ZZAJ>C?!lJ2$euRswQ2*ViZi@hzU& zOwtn0%gf_5etKguunicxUU)<}K0ZErG8B=W0Pf}}J<)dv;s(dFX~%I;*)X2PHDk7J zT7QlXjV@%{FP&ho$$>;WO$HCCS4eAn6fV$xaq&OTAX4}5?}m2R)#05RvJFLT@tc8d zQy%$Vj^WBFx;>6E;8_p%3;Fdu{bA6?zXgj&C!IdWHstPDAed?U?1{;OlExhg-j-k=p?ni-D36hrMjd6 ze6Fat&N(4-8wf0zvHFUFL+JhmJ9ieo zu(9#r{Yq;ruY=^keEj@ewEI~E^;{oxm-N0vA`qH|BddeC%kF7k@3dw}zqwONs($t3 zbq^McJv%#NWo6ANbImcQ1=r3_GSzNi(ox1W$5|$ON#Qx=(;M-RWe%}(b93WW7A-9; zztD5Dv;54LX1#)FoyXcbx3ZFffnghS-pNr^UtizaI_1_x>%w~Ow~TXCiN)2H4XdS@ zb_s;YrFb#6+bT!BJkXNGDLPByTvH)rHxC53#^L^qA!}j`w7#`*eiehbx7 za2&T#4p1t1KU!r*7}sTTGy*)0BFCT^)|np#u@?P2Jt z^*TXzCp`Cm!Bbv$DT(SEfF>QPe(-w7K16s=pl&QpdY{W<`rZDxvR)Axqb{#U1An$y zR-Au~8IY2a-QG??o+(E`0&skuE||^6im>z6mFdkLiKu{x_n!9m+N(~C@8q67H zxe91COKpcJXzl6btM~hyXSTRHY0NMp-EJ^EOF6{sUJ-gAkg?i#tvcu1<3Hf%bIREL zFkq1niiD49c*(q*>y+pa6|#w2q*{mHrOPV4UyCsM5_FSJT-}D1)!qtc!y0k_k^Fc{ zh|ER$U2LUC3siK&@4SgiZeRr5pk?U?nmYn)^9G1YJ`3vlespY-FU9nsmpr`w@AUJ$;617ZY0$?yW zr)=;riNR8GW&*)>i9T9=p*UI9s>y9!Bw6JL9)vPowbvt5zltxT)2UqzNyf&s9V8OwPPlwt1 zA5^olu87wn7#~SYAQ%Nv^0I*V3y**Rf~J@Lul_eK$aXjmha#|Bn#^gIyyy{l zA@g0_34K!E1PbOR#)*Xi3xeXlF4cB?VG$L|j{gg+f2H-RstT#ru^B q7|y|lR6E(8uUO$TD6+?G5-OUl?^}u literal 0 HcmV?d00001 diff --git a/_site/themes/hyde-hyde/layouts/partials/sidebar.html b/_site/themes/hyde-hyde/layouts/partials/sidebar.html index 3bb31b75..2da8d975 100644 --- a/_site/themes/hyde-hyde/layouts/partials/sidebar.html +++ b/_site/themes/hyde-hyde/layouts/partials/sidebar.html @@ -47,6 +47,7 @@ + diff --git a/todoist/display.go b/todoist/display.go new file mode 100644 index 00000000..8cbfa858 --- /dev/null +++ b/todoist/display.go @@ -0,0 +1,64 @@ +package todoist + +import ( + "fmt" + + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "github.com/senorprogrammer/wtf/wtf" +) + +func (w *Widget) display() { + if len(w.list) == 0 { + return + } + list := w.list[w.idx] + + w.View.SetTitle(fmt.Sprintf("%s- [green]%s[white] ", w.Name, list.Project.Name)) + str := wtf.SigilStr(len(w.list), w.idx, w.View) + "\n" + + for index, item := range list.items { + if index == list.index { + str = str + fmt.Sprintf("[%s]", wtf.Config.UString("wtf.colors.border.focused", "grey")) + } + str = str + fmt.Sprintf("| | %s[white]\n", tview.Escape(item.Content)) + } + + w.View.Clear() + w.View.SetText(str) +} + +func (w *Widget) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { + if len(w.list) == 0 { + return event + } + + switch string(event.Rune()) { + case "r": + w.Refresh() + return nil + case "d": + w.Delete() + return nil + case "c": + w.Close() + return nil + } + + switch fromVim(event) { + case tcell.KeyLeft: + w.Prev() + return nil + case tcell.KeyRight: + w.Next() + return nil + case tcell.KeyUp: + w.UP() + return nil + case tcell.KeyDown: + w.Down() + return nil + } + + return event +} diff --git a/todoist/list.go b/todoist/list.go new file mode 100644 index 00000000..81b9d55e --- /dev/null +++ b/todoist/list.go @@ -0,0 +1,79 @@ +package todoist + +import ( + "fmt" + + "github.com/darkSasori/todoist" +) + +type List struct { + todoist.Project + items []todoist.Task + index int +} + +func NewList(id int) *List { + project, err := todoist.GetProject(id) + if err != nil { + panic(err) + } + + list := &List{ + Project: project, + index: -1, + } + list.loadItems() + return list +} + +func (l List) isFirst() bool { + return l.index == 0 +} + +func (l List) isLast() bool { + return l.index >= len(l.items)-1 +} + +func (l *List) up() { + l.index = l.index - 1 + if l.index < 0 { + l.index = len(l.items) - 1 + } +} + +func (l *List) down() { + if l.index == -1 { + l.index = 0 + return + } + + l.index = l.index + 1 + if l.index >= len(l.items) { + l.index = 0 + } +} + +func (l *List) loadItems() { + tasks, err := todoist.ListTask(todoist.QueryParam{"project_id": fmt.Sprintf("%d", l.ID)}) + if err != nil { + panic(err) + } + + l.items = tasks +} + +func (l *List) close() { + if err := l.items[l.index].Close(); err != nil { + panic(err) + } + + l.loadItems() +} + +func (l *List) delete() { + if err := l.items[l.index].Delete(); err != nil { + panic(err) + } + + l.loadItems() +} diff --git a/todoist/widget.go b/todoist/widget.go new file mode 100644 index 00000000..e40089fa --- /dev/null +++ b/todoist/widget.go @@ -0,0 +1,113 @@ +package todoist + +import ( + "os" + + "github.com/darkSasori/todoist" + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "github.com/senorprogrammer/wtf/wtf" +) + +type Widget struct { + wtf.TextWidget + + app *tview.Application + pages *tview.Pages + list []*List + idx int +} + +func NewWidget(app *tview.Application, pages *tview.Pages) *Widget { + widget := Widget{ + TextWidget: wtf.NewTextWidget(" Todoist ", "todoist", true), + + app: app, + pages: pages, + } + + todoist.Token = os.Getenv("WTF_TODOIST_TOKEN") + widget.list = loadProjects() + widget.View.SetInputCapture(widget.keyboardIntercept) + + return &widget +} + +func (w *Widget) Refresh() { + if w.Disabled() || len(w.list) == 0 { + return + } + + w.UpdateRefreshedAt() + w.display() +} + +func (w *Widget) Next() { + w.idx = w.idx + 1 + if w.idx == len(w.list) { + w.idx = 0 + } + + w.display() +} + +func (w *Widget) Prev() { + w.idx = w.idx - 1 + if w.idx < 0 { + w.idx = len(w.list) - 1 + } + + w.display() +} + +func (w *Widget) Down() { + w.list[w.idx].down() + w.display() +} + +func (w *Widget) UP() { + w.list[w.idx].up() + w.display() +} + +func (w *Widget) Close() { + w.list[w.idx].close() + if w.list[w.idx].isLast() { + w.UP() + return + } + w.Down() +} + +func (w *Widget) Delete() { + w.list[w.idx].close() + if w.list[w.idx].isLast() { + w.UP() + return + } + w.Down() +} + +func loadProjects() []*List { + lists := []*List{} + for _, id := range wtf.Config.UList("wtf.mods.todoist.projects") { + list := NewList(id.(int)) + lists = append(lists, list) + } + + return lists +} + +func fromVim(event *tcell.EventKey) tcell.Key { + switch string(event.Rune()) { + case "h": + return tcell.KeyLeft + case "l": + return tcell.KeyRight + case "k": + return tcell.KeyUp + case "j": + return tcell.KeyDown + } + return event.Key() +} diff --git a/wtf.go b/wtf.go index 79b2a6d8..c7709776 100644 --- a/wtf.go +++ b/wtf.go @@ -39,6 +39,7 @@ import ( "github.com/senorprogrammer/wtf/system" "github.com/senorprogrammer/wtf/textfile" "github.com/senorprogrammer/wtf/todo" + "github.com/senorprogrammer/wtf/todoist" "github.com/senorprogrammer/wtf/trello" "github.com/senorprogrammer/wtf/weatherservices/prettyweather" "github.com/senorprogrammer/wtf/weatherservices/weather" @@ -218,6 +219,8 @@ func addWidget(app *tview.Application, pages *tview.Pages, widgetName string) { Widgets = append(Widgets, textfile.NewWidget(app, pages)) case "todo": Widgets = append(Widgets, todo.NewWidget(app, pages)) + case "todoist": + Widgets = append(Widgets, todoist.NewWidget(app, pages)) case "trello": Widgets = append(Widgets, trello.NewWidget()) case "weather": @@ -233,7 +236,6 @@ func makeWidgets(app *tview.Application, pages *tview.Pages) { if enabled := Config.UBool("wtf.mods."+mod+".enabled", false); enabled { addWidget(app, pages, mod) } - } } From fad99a22623889d94065e0c6f63aa5a9a4a58cb3 Mon Sep 17 00:00:00 2001 From: Chris Cummer Date: Wed, 11 Jul 2018 10:00:00 -0700 Subject: [PATCH 2/2] Add todoist dependency --- Gopkg.lock | 2 +- .../github.com/darkSasori/todoist/.gitignore | 2 + .../github.com/darkSasori/todoist/.travis.yml | 19 +++ .../github.com/darkSasori/todoist/Gopkg.lock | 21 +++ .../github.com/darkSasori/todoist/Gopkg.toml | 34 ++++ vendor/github.com/darkSasori/todoist/LICENSE | 21 +++ .../github.com/darkSasori/todoist/README.md | 6 + .../github.com/darkSasori/todoist/projects.go | 149 +++++++++++++++++ vendor/github.com/darkSasori/todoist/task.go | 158 ++++++++++++++++++ .../github.com/darkSasori/todoist/todoist.go | 145 ++++++++++++++++ 10 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/darkSasori/todoist/.gitignore create mode 100644 vendor/github.com/darkSasori/todoist/.travis.yml create mode 100644 vendor/github.com/darkSasori/todoist/Gopkg.lock create mode 100644 vendor/github.com/darkSasori/todoist/Gopkg.toml create mode 100644 vendor/github.com/darkSasori/todoist/LICENSE create mode 100644 vendor/github.com/darkSasori/todoist/README.md create mode 100644 vendor/github.com/darkSasori/todoist/projects.go create mode 100644 vendor/github.com/darkSasori/todoist/task.go create mode 100644 vendor/github.com/darkSasori/todoist/todoist.go diff --git a/Gopkg.lock b/Gopkg.lock index 9c34c32e..bfa5b6c7 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -207,6 +207,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "04892edb6b5f0be61b391ccead307ed15899532db05a17b9e28c00ee32a34861" + inputs-digest = "9eaa70ed639c832e3cde26a4270f4c7b9124960952aa76506f702c2c296d5019" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/darkSasori/todoist/.gitignore b/vendor/github.com/darkSasori/todoist/.gitignore new file mode 100644 index 00000000..9a990fe4 --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/.gitignore @@ -0,0 +1,2 @@ +*.out +vendor/* diff --git a/vendor/github.com/darkSasori/todoist/.travis.yml b/vendor/github.com/darkSasori/todoist/.travis.yml new file mode 100644 index 00000000..0052d33c --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/.travis.yml @@ -0,0 +1,19 @@ +language: go + +notifications: + email: false + +go: + - "1.10.3" + - master + +before_install: + - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + - go get -u github.com/golang/lint/golint + +install: + - dep ensure + +script: + - golint -set_exit_status + - go test -v diff --git a/vendor/github.com/darkSasori/todoist/Gopkg.lock b/vendor/github.com/darkSasori/todoist/Gopkg.lock new file mode 100644 index 00000000..99656b15 --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/Gopkg.lock @@ -0,0 +1,21 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/darkSasori/todoist" + packages = ["."] + revision = "ab3a9f0b9d551ab4e57f1caae9bb20d5aa51ec10" + +[[projects]] + branch = "v1" + name = "gopkg.in/jarcoal/httpmock.v1" + packages = ["."] + revision = "16f9a43967d613f0adc2000f0094a17b9f6c4c20" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "4d0d4d6dd7b4141be119cb48281ea7c10f05d3037c0e4ac49560cf4af21917a7" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/darkSasori/todoist/Gopkg.toml b/vendor/github.com/darkSasori/todoist/Gopkg.toml new file mode 100644 index 00000000..ec866dff --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/Gopkg.toml @@ -0,0 +1,34 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "v1" + name = "gopkg.in/jarcoal/httpmock.v1" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/darkSasori/todoist/LICENSE b/vendor/github.com/darkSasori/todoist/LICENSE new file mode 100644 index 00000000..cc68e07a --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Lineu Felipe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/darkSasori/todoist/README.md b/vendor/github.com/darkSasori/todoist/README.md new file mode 100644 index 00000000..8ffc3976 --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/README.md @@ -0,0 +1,6 @@ +# todoist +[![godoc](https://godoc.org/github.com/darkSasori/todoist?status.svg)](https://godoc.org/github.com/darkSasori/todoist) +[![Build Status](https://travis-ci.org/darkSasori/todoist.svg?branch=master)](https://travis-ci.org/darkSasori/todoist) +[![Go Report Card](https://goreportcard.com/badge/github.com/darkSasori/todoist)](https://goreportcard.com/report/github.com/darkSasori/todoist) + +Unofficial todoist api implementation diff --git a/vendor/github.com/darkSasori/todoist/projects.go b/vendor/github.com/darkSasori/todoist/projects.go new file mode 100644 index 00000000..9493b18a --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/projects.go @@ -0,0 +1,149 @@ +package todoist + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// Project is a model of todoist project entity +type Project struct { + ID int `json:"id"` + Name string `json:"name"` + CommentCount int `json:"comment_count"` + Order int `json:"order"` + Indent int `json:"indent"` +} + +func decodeProject(body io.ReadCloser) (Project, error) { + defer body.Close() + decoder := json.NewDecoder(body) + var project Project + + if err := decoder.Decode(&project); err != nil { + return Project{}, err + } + return project, nil +} + +// ListProject return all projects +// +// Example: +// todoist.Token = "your token" +// projects, err := todoist.ListProject() +// if err != nil { +// panic(err) +// } +// fmt.Println(projects) +func ListProject() ([]Project, error) { + res, err := makeRequest(http.MethodGet, "projects", nil) + if err != nil { + return []Project{}, err + } + + defer res.Body.Close() + decoder := json.NewDecoder(res.Body) + var projects []Project + + if err := decoder.Decode(&projects); err != nil { + return []Project{}, err + } + + return projects, nil +} + +// GetProject return a project by id +// +// Example: +// todoist.Token = "your token" +// project, err := todoist.GetProject(1) +// if err != nil { +// panic(err) +// } +// fmt.Println(project) +func GetProject(id int) (Project, error) { + path := fmt.Sprintf("projects/%d", id) + res, err := makeRequest(http.MethodGet, path, nil) + if err != nil { + return Project{}, err + } + + return decodeProject(res.Body) +} + +// CreateProject create a new project with a name +// +// Example: +// todoist.Token = "your token" +// project, err := todoist.CreateProject("New Project") +// if err != nil { +// panic(err) +// } +// fmt.Println(project) +func CreateProject(name string) (Project, error) { + project := struct { + Name string `json:"name"` + }{ + name, + } + + res, err := makeRequest(http.MethodPost, "projects", project) + if err != nil { + return Project{}, err + } + + return decodeProject(res.Body) +} + +// Delete project +// +// Example: +// todoist.Token = "your token" +// project, err := todoist.GetProject(1) +// if err != nil { +// panic(err) +// } +// err = project.Delete() +// if err != nil { +// panic(err) +// } +func (p Project) Delete() error { + path := fmt.Sprintf("projects/%d", p.ID) + _, err := makeRequest(http.MethodDelete, path, nil) + if err != nil { + return err + } + + return nil +} + +// Update project +// +// Example: +// todoist.Token = "your token" +// project, err := todoist.GetProject(1) +// if err != nil { +// panic(err) +// } +// project.Name = "updated" +// err = project.Update() +// if err != nil { +// panic(err) +// } +// fmt.Println(project) +func (p Project) Update() error { + path := fmt.Sprintf("projects/%d", p.ID) + project := struct { + Name string `json:"name"` + }{ + p.Name, + } + + _, err := makeRequest(http.MethodPost, path, project) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/darkSasori/todoist/task.go b/vendor/github.com/darkSasori/todoist/task.go new file mode 100644 index 00000000..f2c0ce06 --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/task.go @@ -0,0 +1,158 @@ +package todoist + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// Task is a model of todoist project entity +type Task struct { + ID int `json:"id"` + CommentCount int `json:"comment_count"` + Completed bool `json:"completed"` + Content string `json:"content"` + Indent int `json:"indent"` + LabelIDs []int `json:"label_ids"` + Order int `json:"order"` + Priority int `json:"priority"` + ProjectID int `json:"project_id"` + Due Due `json:"due"` +} + +// Due is a model of todoist project entity +type Due struct { + String string `json:"string"` + Date string `json:"date"` + Datetime CustomTime `json:"datetime"` + Timezone string `json:"timezone"` +} + +func (t Task) taskSave() taskSave { + return taskSave{ + t.Content, + t.ProjectID, + t.Order, + t.LabelIDs, + t.Priority, + t.Due.String, + t.Due.Datetime, + "en", + } +} + +func decodeTask(body io.ReadCloser) (Task, error) { + defer body.Close() + decoder := json.NewDecoder(body) + var task Task + + if err := decoder.Decode(&task); err != nil { + return Task{}, err + } + return task, nil +} + +// QueryParam is a map[string]string to build http query +type QueryParam map[string]string + +func (qp QueryParam) String() string { + if len(qp) == 0 { + return "" + } + + ret := "?" + for key, value := range qp { + if ret != "?" { + ret = ret + "&" + } + ret = ret + key + "=" + value + } + + return ret +} + +// ListTask return all task, you can filter using QueryParam +// See documentation: https://developer.todoist.com/rest/v8/#get-tasks +func ListTask(qp QueryParam) ([]Task, error) { + path := fmt.Sprintf("tasks%s", qp) + res, err := makeRequest(http.MethodGet, path, nil) + if err != nil { + return []Task{}, err + } + + defer res.Body.Close() + decoder := json.NewDecoder(res.Body) + var tasks []Task + + if err := decoder.Decode(&tasks); err != nil { + return []Task{}, err + } + + return tasks, nil +} + +// GetTask return a task by id +func GetTask(id int) (Task, error) { + path := fmt.Sprintf("tasks/%d", id) + res, err := makeRequest(http.MethodGet, path, nil) + if err != nil { + return Task{}, err + } + + return decodeTask(res.Body) +} + +// CreateTask create a new task +func CreateTask(task Task) (Task, error) { + res, err := makeRequest(http.MethodPost, "tasks", task.taskSave()) + if err != nil { + return Task{}, err + } + + return decodeTask(res.Body) +} + +// Delete remove a task +func (t Task) Delete() error { + path := fmt.Sprintf("tasks/%d", t.ID) + _, err := makeRequest(http.MethodDelete, path, nil) + if err != nil { + return err + } + + return nil +} + +// Update a task +func (t Task) Update() error { + path := fmt.Sprintf("tasks/%d", t.ID) + _, err := makeRequest(http.MethodPost, path, t.taskSave()) + if err != nil { + return err + } + + return nil +} + +// Close mask task as done +func (t Task) Close() error { + path := fmt.Sprintf("tasks/%d/close", t.ID) + _, err := makeRequest(http.MethodPost, path, nil) + if err != nil { + return err + } + + return nil +} + +// Reopen a task +func (t Task) Reopen() error { + path := fmt.Sprintf("tasks/%d/reopen", t.ID) + _, err := makeRequest(http.MethodPost, path, nil) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/darkSasori/todoist/todoist.go b/vendor/github.com/darkSasori/todoist/todoist.go new file mode 100644 index 00000000..d487426c --- /dev/null +++ b/vendor/github.com/darkSasori/todoist/todoist.go @@ -0,0 +1,145 @@ +package todoist + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +// Token save the personal token from todoist +var Token string +var todoistURL = "https://beta.todoist.com/API/v8/" + +func makeRequest(method, endpoint string, data interface{}) (*http.Response, error) { + url := todoistURL + endpoint + body := bytes.NewBuffer([]byte{}) + + if data != nil { + json, err := json.Marshal(data) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(json) + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + bearer := fmt.Sprintf("Bearer %s", Token) + req.Header.Add("Authorization", bearer) + + if data != nil { + req.Header.Add("Content-Type", "application/json") + } + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return nil, err + } + + if res.StatusCode >= 400 { + defer res.Body.Close() + str, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + return nil, fmt.Errorf(string(str)) + } + + return res, nil +} + +const ctLayout = "2006-01-02T15:04:05+00:00" + +// CustomTime had a custom json date format +type CustomTime struct { + time.Time +} + +// UnmarshalJSON convert from []byte to CustomTime +func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), "\"") + if s == "null" { + ct.Time = time.Time{} + return nil + } + + ct.Time, err = time.Parse(ctLayout, s) + return err +} + +// MarshalJSON convert CustomTime to []byte +func (ct CustomTime) MarshalJSON() ([]byte, error) { + if ct.Time.IsZero() { + return []byte("null"), nil + } + return []byte(`"` + ct.Time.Format(ctLayout) + `"`), nil +} + +type taskSave struct { + Content string `json:"content"` + ProjectID int `json:"project_id,omitempty"` + Order int `json:"order,omitempty"` + LabelIDs []int `json:"label_ids,omitempty"` + Priority int `json:"priority,omitempty"` + DueString string `json:"due_string,omitempty"` + DueDateTime CustomTime `json:"due_datetime,omitempty"` + DueLang string `json:"due_lang,omitempty"` +} + +func (ts taskSave) MarshalJSON() ([]byte, error) { + buffer := bytes.NewBufferString("{") + + if ts.Content == "" { + return nil, fmt.Errorf("Content is empty") + } + buffer.WriteString(fmt.Sprintf("\"content\":\"%s\"", ts.Content)) + + if ts.ProjectID != 0 { + buffer.WriteString(fmt.Sprintf(",\"project_id\":%d", ts.ProjectID)) + } + + if ts.Order != 0 { + buffer.WriteString(fmt.Sprintf(",\"order\":%d", ts.Order)) + } + + if !ts.DueDateTime.IsZero() { + buffer.WriteString(",\"due_datetime\":") + json, err := json.Marshal(ts.DueDateTime) + if err != nil { + return nil, err + } + buffer.Write(json) + } + + if len(ts.LabelIDs) != 0 { + buffer.WriteString(",\"label_ids\":") + json, err := json.Marshal(ts.LabelIDs) + if err != nil { + return nil, err + } + buffer.Write(json) + } + + if ts.Priority != 0 { + buffer.WriteString(fmt.Sprintf(",\"priority\":%d", ts.Priority)) + } + + if ts.DueString != "" { + buffer.WriteString(fmt.Sprintf(",\"due_string\":\"%s\"", ts.DueString)) + } + + if ts.DueLang != "" { + buffer.WriteString(fmt.Sprintf(",\"due_lang\":\"%s\"", ts.DueLang)) + } + + buffer.WriteString("}") + return buffer.Bytes(), nil +}