1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +02:00
documize/core/section/trello/trello.go
2016-11-09 15:16:44 -08:00

686 lines
16 KiB
Go

// 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"
"sort"
"strings"
"time"
"unicode"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/log"
"github.com/documize/community/core/section/provider"
)
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"
meta.PageType = "tab"
}
// 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
}
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
}
if len(config.Token) == 0 {
config.Token = ctx.GetSecrets("token") // get a token, if we have one
}
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 {
log.IfErr(err)
provider.WriteError(w, "trello", err)
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
return
}
provider.WriteJSON(w, render)
case "boards":
render, err := getBoards(&config)
if err != nil {
log.IfErr(err)
provider.WriteError(w, "trello", err)
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
return
}
provider.WriteJSON(w, render)
case "lists":
render, err := getLists(config)
if err != nil {
log.IfErr(err)
provider.WriteError(w, "trello", err)
log.IfErr(ctx.SaveSecrets("")) // failure means our secrets are invalid
return
}
provider.WriteJSON(w, render)
case "config":
var ret struct {
AppKey string `json:"appKey"`
Token string `json:"token"`
}
ret.AppKey = config.AppKey
if config.Token != "" {
ret.Token = provider.SecretReplacement
}
provider.WriteJSON(w, ret)
return
default:
log.ErrorString("trello unknown method name: " + method)
provider.WriteMessage(w, "trello", "missing method name")
return
}
// 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)))
}
// Render the payload using the template.
func (*Provider) Render(ctx *provider.Context, config, data string) string {
var payload = trelloRender{}
var c = trelloConfig{}
json.Unmarshal([]byte(data), &payload)
json.Unmarshal([]byte(config), &c)
buildPayloadAnalysis(&c, &payload)
t := template.New("trello")
var err error
t, err = t.Parse(renderTemplate)
if err != nil {
log.IfErr(err)
return ""
}
buffer := new(bytes.Buffer)
err = t.Execute(buffer, payload)
if err != nil {
log.IfErr(err)
return ""
}
return buffer.String()
}
// Refresh just sends back data as-is.
func (*Provider) Refresh(ctx *provider.Context, config, data string) string {
var c = trelloConfig{}
log.IfErr(json.Unmarshal([]byte(config), &c))
save := trelloRender{}
save.Boards = make([]trelloRenderBoard, 0, len(c.Boards))
if len(c.Since) >= len("yyyy/mm/dd hh:ss") {
var since time.Time
tt := []byte("yyyy-mm-ddThh:mm:00Z")
for _, i := range []int{0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15} {
tt[i] = c.Since[i]
}
err := since.UnmarshalText(tt)
if err != nil {
log.ErrorString("Date unmarshall '" + c.Since + "'->'" + string(tt) + "' error: " + err.Error())
} else {
c.SincePtr = &since
}
}
dateMessage := ""
if c.SincePtr == nil {
dateMessage = " (the last 7 days)"
since := time.Now().AddDate(0, 0, -7)
c.SincePtr = &since
c.Since = (*c.SincePtr).Format("2006/01/02 ")
}
save.Since = (*c.SincePtr).Format("January 2, 2006") + dateMessage
c.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
if c.Board.ID != "" { // set up detail board
var err error
save.Detail.Board = c.Board
save.Detail.Data, err = getCards(c)
log.IfErr(err)
save.Detail.ListCount = len(save.Detail.Data)
for _, list := range save.Detail.Data {
save.Detail.CardCount += len(list.Cards)
}
}
for _, board := range c.Boards {
if board.Included && board.ID != "" {
var payload = trelloRenderBoard{}
c.Board = board
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)
}
payload.Actions, payload.Archived = fetchBoardActions(&c, &save, board.ID, c.Since)
save.Boards = append(save.Boards, payload)
}
}
j, err := json.Marshal(save)
if err != nil {
log.Error("unable to marshall trello cards", err)
return data
}
return string(j)
}
// Helpers
func getOrg(config *trelloConfig, orgID string) (*trelloOrganization, error) {
if config.OrgByID == nil {
config.OrgByID = make(map[string]trelloOrganization)
}
if org, found := config.OrgByID[orgID]; found {
return &org, nil
}
req, err := http.NewRequest("GET", fmt.Sprintf(
"https://api.trello.com/1/organizations/%s?fields=name,desc&key=%s&token=%s",
orgID, config.AppKey, config.Token), nil)
log.IfErr(err)
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 := trelloOrganization{}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
err = dec.Decode(&b)
if err != nil {
fmt.Println(err)
return nil, err
}
config.OrgByID[orgID] = b
return &b, nil
}
func getBoards(config *trelloConfig) (boards []trelloBoard, err error) {
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)
log.IfErr(err)
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)
if err != nil {
fmt.Println(err)
return nil, err
}
// we only show open, team boards (not personal)
for _, b := range b {
if !b.Closed && len(b.OrganizationID) > 0 {
if o, e := getOrg(config, b.OrganizationID); e == nil {
b.OrgName = o.Name
b.NamePath = o.Name + " / " + b.Name
} else {
log.Error("failed to get organisation infomation", e)
}
boards = append(boards, b)
}
}
for bx, bd := range boards {
for _, cd := range config.Boards {
if bd.ID == cd.ID {
boards[bx].Included = cd.Included // to pick up the previous selection or not
goto foundID
}
}
boards[bx].Included = false // don't include boards by default
foundID:
}
return boards, nil
}
func getLists(config trelloConfig) (lists []trelloList, err error) {
if config.Board.ID == "" {
return
}
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)
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)
log.IfErr(err)
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
}
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
}
func fetchBoardActions(config *trelloConfig, render *trelloRender, boardID string, since string) (actions []trelloAction, archived []trelloCard) {
sinceString := since[:10]
if len(config.AppKey) == 0 {
config.AppKey = request.ConfigString(meta.ConfigHandle(), "appKey")
}
{
uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/actions?limit=1000&since=%s&key=%s&token=%s", boardID, sinceString, 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
}
}
{
uri := fmt.Sprintf("https://api.trello.com/1/boards/%s/cards?filter=closed&since=%s&key=%s&token=%s",
boardID, sinceString, 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 {
msg := ""
txt, err := ioutil.ReadAll(res.Body)
if err == nil {
msg = string(txt)
} else {
msg = err.Error()
}
log.ErrorString("Trello fetch board archived HTTP status not OK - " + msg)
return
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
err = dec.Decode(&archived)
if err != nil {
log.IfErr(err)
return
}
}
return
}
func buildPayloadAnalysis(config *trelloConfig, render *trelloRender) {
//totals
render.CardTotal = 0
render.CardAssignTotal = 0
render.ListTotal = 0
// pre-process labels
type labT struct {
color string
boards map[string]trelloBoard
}
labels := make(map[string]labT)
// pre-process member stats
memberBoardCount := make(map[string]map[string]int)
// main loop
for brdIdx, brd := range render.Boards {
for _, lst := range brd.Data {
render.ListTotal++
for _, crd := range lst.Cards {
render.CardTotal++
if len(crd.MembersID) > 0 {
render.CardAssignTotal++
}
// process labels
for _, lab := range crd.Labels {
if _, exists := labels[lab.Name]; !exists {
labels[lab.Name] = labT{color: lab.Color, boards: make(map[string]trelloBoard)}
}
labels[lab.Name].boards[brd.Board.URL+" / "+brd.Board.Name] = brd.Board
}
// process member stats
for _, mem := range crd.MembersID {
if _, exists := memberBoardCount[mem]; !exists {
memberBoardCount[mem] = make(map[string]int)
}
memberBoardCount[mem][brd.Board.ID]++
}
}
}
// ActionSummary
actionSummaryMap := make(map[string]int)
for _, act := range brd.Actions {
englishType := ""
for _, c := range act.Type {
if unicode.IsUpper(c) {
englishType += " "
englishType += string(unicode.ToLower(c))
} else {
englishType += string(c)
}
}
englishType = strings.Replace(englishType, "organization", "team", -1)
if newTxt, found := activityTranslation[englishType]; found {
englishType = newTxt
}
if len(englishType) > 0 {
actionSummaryMap[englishType]++
}
}
acts := make([]string, 0, len(actionSummaryMap))
for a := range actionSummaryMap {
acts = append(acts, a)
}
sort.Strings(acts)
render.Boards[brdIdx].ActionSummary = make([]trelloActionSummaryEntry, len(acts))
for k, v := range acts {
render.Boards[brdIdx].ActionSummary[k] = trelloActionSummaryEntry{Name: v, Count: actionSummaryMap[v]}
}
}
//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)
lbrds := []trelloBoard{}
for _, h := range brds {
lbrds = append(lbrds, labels[lname].boards[h])
}
render.SharedLabels = append(render.SharedLabels, trelloSharedLabel{
Name: lname, Color: labels[lname].color, Boards: lbrds,
})
}
}
//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})
for _, b := range render.Boards { // these are already in order
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:
}
}