2016-07-07 18:54:16 -07:00
|
|
|
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
|
|
|
//
|
|
|
|
// This software (Documize Community Edition) is licensed under
|
|
|
|
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
//
|
|
|
|
// You can operate outside the AGPL restrictions by purchasing
|
|
|
|
// Documize Enterprise Edition and obtaining a commercial license
|
|
|
|
// by contacting <sales@documize.com>.
|
|
|
|
//
|
|
|
|
// https://documize.com
|
|
|
|
|
|
|
|
package trello
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2016-09-07 18:07:37 +01:00
|
|
|
"sort"
|
2016-09-08 15:08:20 +01:00
|
|
|
"time"
|
2016-07-07 18:54:16 -07:00
|
|
|
|
2016-07-20 15:58:37 +01:00
|
|
|
"github.com/documize/community/core/api/request"
|
|
|
|
"github.com/documize/community/core/log"
|
2016-09-07 18:07:37 +01:00
|
|
|
"github.com/documize/community/core/section/provider"
|
2016-07-07 18:54:16 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
var meta provider.TypeMeta
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
meta = provider.TypeMeta{}
|
|
|
|
meta.ID = "c455a552-202e-441c-ad79-397a8152920b"
|
|
|
|
meta.Title = "Trello"
|
|
|
|
meta.Description = "Embed cards from boards and lists"
|
|
|
|
meta.ContentType = "trello"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Provider represents Trello
|
|
|
|
type Provider struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
// Meta describes us
|
|
|
|
func (*Provider) Meta() provider.TypeMeta {
|
|
|
|
return meta
|
|
|
|
}
|
|
|
|
|
|
|
|
// Command stub.
|
|
|
|
func (*Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http.Request) {
|
|
|
|
query := r.URL.Query()
|
|
|
|
method := query.Get("method")
|
|
|
|
|
|
|
|
defer r.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
provider.WriteMessage(w, "trello", "Bad body")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var config = trelloConfig{}
|
|
|
|
err = json.Unmarshal(body, &config)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
provider.WriteError(w, "trello", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-08 12:29:53 +01:00
|
|
|
config.Clean()
|
|
|
|
config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
|
|
|
|
|
|
|
|
if len(config.AppKey) == 0 {
|
|
|
|
log.ErrorString("missing trello App Key")
|
|
|
|
provider.WriteMessage(w, "trello", "Missing appKey")
|
|
|
|
return
|
2016-07-07 18:54:16 -07:00
|
|
|
}
|
|
|
|
|
2016-07-08 12:29:53 +01:00
|
|
|
if len(config.Token) == 0 {
|
|
|
|
config.Token = ctx.GetSecrets("token") // get a token, if we have one
|
|
|
|
}
|
2016-07-07 18:54:16 -07:00
|
|
|
|
|
|
|
if method != "config" {
|
|
|
|
if len(config.Token) == 0 {
|
|
|
|
provider.WriteMessage(w, "trello", "Missing token")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch method {
|
|
|
|
case "cards":
|
|
|
|
render, err := getCards(config)
|
|
|
|
|
|
|
|
if err != nil {
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
provider.WriteError(w, "trello", err)
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
|
2016-07-07 18:54:16 -07:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
provider.WriteJSON(w, render)
|
|
|
|
|
|
|
|
case "boards":
|
|
|
|
render, err := getBoards(config)
|
|
|
|
|
|
|
|
if err != nil {
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
provider.WriteError(w, "trello", err)
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
|
2016-07-07 18:54:16 -07:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
provider.WriteJSON(w, render)
|
|
|
|
|
|
|
|
case "lists":
|
|
|
|
render, err := getLists(config)
|
|
|
|
|
|
|
|
if err != nil {
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
provider.WriteError(w, "trello", err)
|
2016-07-08 12:29:53 +01:00
|
|
|
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
|
2016-07-07 18:54:16 -07:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
provider.WriteJSON(w, render)
|
|
|
|
|
|
|
|
case "config":
|
2016-07-08 12:29:53 +01:00
|
|
|
var ret struct {
|
|
|
|
AppKey string `json:"appKey"`
|
|
|
|
Token string `json:"token"`
|
2016-07-07 18:54:16 -07:00
|
|
|
}
|
2016-07-08 12:29:53 +01:00
|
|
|
ret.AppKey = config.AppKey
|
2016-07-08 15:29:01 +01:00
|
|
|
if config.Token != "" {
|
|
|
|
ret.Token = provider.SecretReplacement
|
|
|
|
}
|
2016-07-08 12:29:53 +01:00
|
|
|
provider.WriteJSON(w, ret)
|
|
|
|
return
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.ErrorString("trello unknown method name: " + method)
|
|
|
|
provider.WriteMessage(w, "trello", "missing method name")
|
|
|
|
return
|
2016-07-07 18:54:16 -07:00
|
|
|
}
|
2016-07-08 12:29:53 +01:00
|
|
|
|
|
|
|
// the token has just worked, so save it as our secret
|
|
|
|
var s secrets
|
|
|
|
s.Token = config.Token
|
|
|
|
b, e := json.Marshal(s)
|
|
|
|
log.IfErr(e)
|
|
|
|
log.IfErr(ctx.SaveSecrets(string(b)))
|
2016-07-07 18:54:16 -07:00
|
|
|
}
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
// Render the payload using the template.
|
2016-07-07 18:54:16 -07:00
|
|
|
func (*Provider) Render(ctx *provider.Context, config, data string) string {
|
2016-09-07 18:07:37 +01:00
|
|
|
var payload = trelloRender{}
|
2016-07-07 18:54:16 -07:00
|
|
|
var c = trelloConfig{}
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
json.Unmarshal([]byte(data), &payload)
|
2016-07-07 18:54:16 -07:00
|
|
|
json.Unmarshal([]byte(config), &c)
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
buildPayloadAnalysis(&c, &payload)
|
2016-07-07 18:54:16 -07:00
|
|
|
|
|
|
|
t := template.New("trello")
|
2016-09-07 18:07:37 +01:00
|
|
|
var err error
|
|
|
|
t, err = t.Parse(renderTemplate)
|
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
|
|
|
|
buffer := new(bytes.Buffer)
|
|
|
|
t.Execute(buffer, payload)
|
|
|
|
|
|
|
|
return buffer.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh just sends back data as-is.
|
|
|
|
func (*Provider) Refresh(ctx *provider.Context, config, data string) string {
|
|
|
|
var c = trelloConfig{}
|
|
|
|
json.Unmarshal([]byte(config), &c)
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
save := trelloRender{}
|
|
|
|
save.Boards = make([]trelloRenderBoard, 0, len(c.Boards))
|
2016-07-07 18:54:16 -07:00
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
for _, board := range c.Boards {
|
|
|
|
|
|
|
|
var payload = trelloRenderBoard{}
|
|
|
|
|
|
|
|
c.Board = board
|
|
|
|
c.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
|
|
|
|
|
|
|
|
lsts, err := getLists(c)
|
|
|
|
log.IfErr(err)
|
|
|
|
if err == nil {
|
|
|
|
c.Lists = lsts
|
|
|
|
}
|
|
|
|
|
|
|
|
for l := range c.Lists {
|
|
|
|
c.Lists[l].Included = true
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshed, err := getCards(c)
|
|
|
|
log.IfErr(err)
|
|
|
|
|
|
|
|
payload.Board = c.Board
|
|
|
|
payload.Data = refreshed
|
|
|
|
payload.ListCount = len(refreshed)
|
|
|
|
|
|
|
|
for _, list := range refreshed {
|
|
|
|
payload.CardCount += len(list.Cards)
|
|
|
|
}
|
|
|
|
|
2016-09-08 15:08:20 +01:00
|
|
|
payload.Actions = fetchBoardActions(&c, &save, board.ID, nil) // TODO pass in date
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
save.Boards = append(save.Boards, payload)
|
2016-07-07 18:54:16 -07:00
|
|
|
}
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
j, err := json.Marshal(save)
|
2016-07-07 18:54:16 -07:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error("unable to marshall trello cards", err)
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(j)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helpers
|
|
|
|
func getBoards(config trelloConfig) (boards []trelloBoard, err error) {
|
2016-09-08 15:08:20 +01:00
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf(
|
|
|
|
"https://api.trello.com/1/members/me/boards?fields=id,name,url,closed,prefs,idOrganization&key=%s&token=%s",
|
|
|
|
config.AppKey, config.Token), nil)
|
2016-09-07 18:07:37 +01:00
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
client := &http.Client{}
|
|
|
|
res, err := client.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("error: HTTP status code %d", res.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
b := []trelloBoard{}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
dec := json.NewDecoder(res.Body)
|
|
|
|
err = dec.Decode(&b)
|
|
|
|
|
|
|
|
// we only show open, team boards (not personal)
|
|
|
|
for _, b := range b {
|
|
|
|
if !b.Closed && len(b.OrganizationID) > 0 {
|
|
|
|
boards = append(boards, b)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return boards, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getLists(config trelloConfig) (lists []trelloList, err error) {
|
2016-09-07 18:07:37 +01:00
|
|
|
uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/lists/open?key=%s&token=%s", config.Board.ID, config.AppKey, config.Token)
|
|
|
|
req, err := http.NewRequest("GET", uri, nil)
|
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
client := &http.Client{}
|
|
|
|
res, err := client.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("error: HTTP status code %d", res.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
dec := json.NewDecoder(res.Body)
|
|
|
|
err = dec.Decode(&lists)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return lists, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCards(config trelloConfig) (listCards []trelloListCards, err error) {
|
|
|
|
for _, list := range config.Lists {
|
|
|
|
|
|
|
|
// don't process lists that user excluded from rendering
|
|
|
|
if !list.Included {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.trello.com/1/lists/%s/cards?key=%s&token=%s", list.ID, config.AppKey, config.Token), nil)
|
2016-09-07 18:07:37 +01:00
|
|
|
log.IfErr(err)
|
2016-07-07 18:54:16 -07:00
|
|
|
client := &http.Client{}
|
|
|
|
res, err := client.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("error: HTTP status code %d", res.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
var cards []trelloCard
|
|
|
|
|
|
|
|
dec := json.NewDecoder(res.Body)
|
|
|
|
err = dec.Decode(&cards)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
data := trelloListCards{}
|
|
|
|
data.Cards = cards
|
|
|
|
data.List = list
|
|
|
|
|
|
|
|
listCards = append(listCards, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
return listCards, nil
|
|
|
|
}
|
2016-09-07 18:07:37 +01:00
|
|
|
|
|
|
|
func fetchMember(config *trelloConfig, render *trelloRender, memberID string) (memberInfo trelloMember) {
|
|
|
|
memberInfo.FullName = "(unknown)"
|
|
|
|
|
|
|
|
if render.MembersByID == nil {
|
|
|
|
render.MembersByID = make(map[string]trelloMember)
|
|
|
|
}
|
|
|
|
found := false
|
|
|
|
if memberInfo, found = render.MembersByID[memberID]; found {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
render.MembersByID[memberID] = memberInfo // write unknown, so that we do not retry on errors
|
|
|
|
|
|
|
|
if len(config.AppKey) == 0 {
|
|
|
|
config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
|
|
|
|
}
|
|
|
|
uri := fmt.Sprintf("https://api.trello.com/1/members/%s?key=%s&token=%s", memberID, config.AppKey, config.Token)
|
|
|
|
req, err := http.NewRequest("GET", uri, nil)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
log.ErrorString("Trello fetch member HTTP status not OK")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
dec := json.NewDecoder(res.Body)
|
|
|
|
err = dec.Decode(&memberInfo)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
render.MembersByID[memberID] = memberInfo
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-09-08 15:08:20 +01:00
|
|
|
func fetchBoardActions(config *trelloConfig, render *trelloRender, boardID string, since *time.Time) (actions []trelloAction) {
|
|
|
|
|
|
|
|
if len(config.AppKey) == 0 {
|
|
|
|
config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
|
|
|
|
}
|
|
|
|
uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/actions?since=2016-08-01&key=%s&token=%s", boardID, config.AppKey, config.Token)
|
|
|
|
req, err := http.NewRequest("GET", uri, nil)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
log.ErrorString("Trello fetch board actions HTTP status not OK")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
dec := json.NewDecoder(res.Body)
|
|
|
|
err = dec.Decode(&actions)
|
|
|
|
if err != nil {
|
|
|
|
log.IfErr(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
func buildPayloadAnalysis(config *trelloConfig, render *trelloRender) {
|
|
|
|
|
2016-09-08 15:28:25 +01:00
|
|
|
//totals
|
|
|
|
render.CardTotal = 0
|
|
|
|
render.CardAssignTotal = 0
|
|
|
|
|
2016-09-07 18:07:37 +01:00
|
|
|
// pre-process labels
|
|
|
|
type labT struct {
|
|
|
|
color string
|
|
|
|
boards map[string]bool
|
|
|
|
}
|
|
|
|
labels := make(map[string]labT)
|
|
|
|
|
|
|
|
// pre-process member stats
|
|
|
|
memberBoardCount := make(map[string]map[string]int)
|
|
|
|
|
|
|
|
// main loop
|
|
|
|
for _, brd := range render.Boards {
|
|
|
|
for _, lst := range brd.Data {
|
|
|
|
for _, crd := range lst.Cards {
|
2016-09-08 15:28:25 +01:00
|
|
|
render.CardTotal++
|
|
|
|
if len(crd.MembersID) > 0 {
|
|
|
|
render.CardAssignTotal++
|
|
|
|
}
|
2016-09-07 18:07:37 +01:00
|
|
|
|
|
|
|
// process labels
|
|
|
|
for _, lab := range crd.Labels {
|
|
|
|
if _, exists := labels[lab.Name]; !exists {
|
|
|
|
labels[lab.Name] = labT{color: lab.Color, boards: make(map[string]bool)}
|
|
|
|
}
|
|
|
|
labels[lab.Name].boards[brd.Board.Name] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// process member stats
|
|
|
|
for _, mem := range crd.MembersID {
|
|
|
|
if _, exists := memberBoardCount[mem]; !exists {
|
|
|
|
memberBoardCount[mem] = make(map[string]int)
|
|
|
|
}
|
|
|
|
memberBoardCount[mem][brd.Board.ID]++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//post-process labels
|
|
|
|
labs := make([]string, 0, len(labels))
|
|
|
|
for lname := range labels {
|
|
|
|
labs = append(labs, lname)
|
|
|
|
}
|
|
|
|
sort.Strings(labs)
|
|
|
|
for _, lname := range labs {
|
|
|
|
thisLabel := labels[lname].boards
|
|
|
|
if l := len(thisLabel); l > 1 {
|
|
|
|
brds := make([]string, 0, l)
|
|
|
|
for bname := range thisLabel {
|
|
|
|
brds = append(brds, bname)
|
|
|
|
}
|
|
|
|
sort.Strings(brds)
|
|
|
|
render.SharedLabels = append(render.SharedLabels, trelloSharedLabel{
|
|
|
|
Name: lname, Color: labels[lname].color, Boards: brds,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//post-process member stats
|
|
|
|
mNames := make([]string, 0, len(memberBoardCount))
|
|
|
|
for mID := range memberBoardCount {
|
|
|
|
memInfo := fetchMember(config, render, mID)
|
|
|
|
mNames = append(mNames, memInfo.FullName)
|
|
|
|
}
|
|
|
|
sort.Strings(mNames)
|
|
|
|
for _, mNam := range mNames {
|
|
|
|
for mem, brdCounts := range memberBoardCount {
|
|
|
|
memInfo := fetchMember(config, render, mem)
|
|
|
|
if mNam == memInfo.FullName {
|
|
|
|
render.MemberBoardAssign = append(render.MemberBoardAssign, trelloBoardAssign{MemberName: mNam, AvatarHash: memInfo.AvatarHash})
|
2016-09-08 15:08:20 +01:00
|
|
|
for _, b := range render.Boards { // these are already in order
|
2016-09-07 18:07:37 +01:00
|
|
|
if count, ok := brdCounts[b.Board.ID]; ok {
|
|
|
|
render.MemberBoardAssign[len(render.MemberBoardAssign)-1].AssignCounts =
|
|
|
|
append(render.MemberBoardAssign[len(render.MemberBoardAssign)-1].AssignCounts,
|
|
|
|
trelloBoardAssignCount{BoardName: b.Board.Name, Count: count})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
goto found
|
|
|
|
}
|
|
|
|
}
|
|
|
|
found:
|
|
|
|
}
|
|
|
|
}
|