Skip to content
Permalink
Browse files

basic stuff.

missing:
  checklists and custom fields support;
  a landing page;
  a cool Power-Up interface.
  • Loading branch information...
fiatjaf committed Jul 13, 2018
0 parents commit fd3061840b822d1d3971fd511b918ce64ba92201
@@ -0,0 +1,11 @@
*.swp
*.swo
.env
*.log
browserify-cache.json
node_modules
bundle.js
*.fasthttp.*
*.DS_Store
*package-lock.json

@@ -0,0 +1 @@
web: trello-permissions
@@ -0,0 +1,111 @@
package main

import (
"strings"
"time"

"github.com/jmoiron/sqlx/types"
"github.com/lib/pq"
"github.com/rs/zerolog"
)

func onAllowed(logger zerolog.Logger, trello trelloClient, wh Webhook) {
switch wh.Action.Type {
case "createCard", "copyCard", "convertToCardFromCheckItem", "moveCardToBoard":
err = saveBackupData(wh.Action.Data.Card.Id, wh.Action.Data.Card)
case "deleteCard", "moveCardFromBoard":
err = deleteBackupData(wh.Action.Data.Card.Id)
case "updateCard":
var cardValues types.JSONText
cardValues, err = toJSONText(wh.Action.Data.Card)
if err != nil {
break
}

err = saveBackupData(wh.Action.Data.Card.Id, cardValues)
case "addMemberToCard":
err = updateBackupData(wh.Action.Data.Card.Id, wh.Action.Data.Card,
`'{"idMembers": []}' || $init || data`,
`jsonb_set(new.data, '{idMembers}', (new.data->'idMembers') || $arg)`,
wh.Action.Data.IdMember,
)
case "removeMemberFromCard":
err = updateBackupData(wh.Action.Data.Card.Id, wh.Action.Data.Card,
`'{"idMembers": []}' || $init || data`,
`jsonb_set(new.data, '{idMembers}', (new.data->'idMembers') - ($arg::jsonb#>>'{}'))`,
wh.Action.Data.IdMember,
)
case "addLabelToCard":
saveBackupData(wh.Action.Data.Label.Id, wh.Action.Data.Label)

err = updateBackupData(wh.Action.Data.Card.Id, wh.Action.Data.Card,
`'{"idLabels": []}' || $init || data`,
`jsonb_set(new.data, '{idLabels}', (new.data->'idLabels') || $arg)`,
wh.Action.Data.Label.Id,
)
case "removeLabelFromCard":
saveBackupData(wh.Action.Data.Label.Id, wh.Action.Data.Label)

err = updateBackupData(wh.Action.Data.Card.Id, wh.Action.Data.Card,
`'{"idLabels": []}' || $init || data`,
`jsonb_set(new.data, '{idLabels}', (new.data->'idLabels') - ($arg::jsonb#>>'{}'))`,
wh.Action.Data.Label.Id,
)
case "createLabel", "updateLabel":
saveBackupData(wh.Action.Data.Label.Id, wh.Action.Data.Label)
case "deleteLabel":
err = deleteBackupData(wh.Action.Data.Label.Id)
case "addAttachmentToCard":
primary := Attachment{
Name: wh.Action.Data.Attachment.Name,
Url: wh.Action.Data.Attachment.Url,
}

if strings.Split(primary.Url, "/")[2] == "trello-attachments.s3.amazonaws.com" {
// this file was uploaded on Trello, we must save a
// secondary copy (on the same card)
var secondary Attachment

// ensure we don't enter an infinite loop of backup saving
key := "replicate-attachment:" + wh.Action.Data.Card.Id +
"/" + wh.Action.Data.Attachment.Name
if stored := rds.Get(key).Val(); stored != "t" {
err = trello("post", "/1/cards/"+wh.Action.Data.Card.Id+"/attachments",
primary, &secondary)

if err != nil {
break
}

err = rds.Set(key, "t", time.Minute).Err()

primary.Id = wh.Action.Data.Attachment.Id

// save the primary as a backup to the secondary
saveBackupData(secondary.Id, primary)

// and vice-versa
saveBackupData(primary.Id, secondary)
}
} else {
// just save the primary data as a backup to itself
saveBackupData(wh.Action.Data.Attachment.Id, primary)
}
case "deleteAttachmentFromCard":
err = deleteBackupData(wh.Action.Data.Attachment.Id)
}

if err != nil {
if perr, ok := err.(*pq.Error); ok {
log.Print(perr.Where)
log.Print(perr.Position)
log.Print(perr.Hint)
log.Print(perr.Column)
log.Print(perr.Message)
}

logger.Warn().
Err(err).
Msg("failed to perform action on allowed")
}
}
@@ -0,0 +1,6 @@
package main

const (
TRELLODATEFORMAT = "2006-01-02T15:04:05.000Z"
PRETTYDATEFORMAT = "January 02 2006, 15:04:05 UTC"
)
@@ -0,0 +1,182 @@
package main

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/jmoiron/sqlx/types"

"gopkg.in/jmcvetta/napping.v3"
)

func makeTrelloClient(token string) trelloClient {
authvalues := (napping.Params{
"key": s.TrelloApiKey,
"token": token,
}).AsUrlValues()
h := napping.Session{Params: &authvalues}

return func(method string, path string, data interface{}, res interface{}) error {
request := &napping.Request{
Url: "https://api.trello.com" + path,
Method: method,
Payload: data,
Result: &res,
}
n, err := h.Send(request)
if err != nil || n.Status() > 299 {
if err == nil {
err = fmt.Errorf("Trello returned %d for '%s': '%s'",
n.Status(), n.Url, n.RawText())
}
return err
}
return nil
}
}

type trelloClient func(string, string, interface{}, interface{}) error

func userAllowed(trello trelloClient, userId, boardId, cardId string) bool {
// try admins cache
if s.RedisURL != "" {
v, err := rds.Get("admin:" + boardId + ":" + userId).Result()
if err == nil && v == "t" {
// the user is a board or team admin
return true
}
}

// check board and team admins
var br []struct {
IdMember string `json:"idMember"`
MemberType string `json:"memberType"`
OrgMemberType string `json:"orgMemberType"`
}
err = trello("get", "/1/boards/"+boardId+"/memberships?member=false&orgMemberType=true", nil, &br)
if err != nil {
log.Warn().Str("board", boardId).Err(err).Msg("failed to fetch memberships")
return false
}

for _, ms := range br {
if ms.IdMember == userId {
if ms.MemberType == "admin" || ms.OrgMemberType == "admin" {
return true

go func() {
if s.RedisURL != "" {
rds.Set("admin:"+boardId+":"+userId, "t", time.Hour*2)
}
}()
}
}
}

// check card members
if cardId == "" {
// this action was dispatched by something other than a card action
return false
}
var cr []struct {
Id string `json:"id"`
}

err := trello("get", "/1/cards/"+cardId+"/members?fields=id", nil, &cr)
if err != nil {
log.Warn().Str("card", cardId).Err(err).
Msg("failed to fetch memberships")
} else {
for _, m := range cr {
if m.Id == userId {
return true
}
}
}

return false
}

func toJSONText(data interface{}) (v types.JSONText, err error) {
var x []byte
x, err = json.Marshal(data)
if err != nil {
return
}

err = v.UnmarshalJSON(x)
return
}

func saveBackupData(id string, data interface{}) (err error) {
v, err := toJSONText(data)
if err != nil {
return
}

_, err = pg.Exec(`
INSERT INTO backups VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET data = backups.data || $2
`, id, v)
return
}

func updateBackupData(
id string, initData interface{},
preupdate, updatefun string, value interface{},
) (err error) {
d, err := toJSONText(initData)
if err != nil {
return
}

v, err := toJSONText(value)
if err != nil {
return
}

updatefun = strings.Replace(
strings.Replace(updatefun, "$init", "$2", -1),
"$arg", "$3", -1)

preupdate = strings.Replace(
strings.Replace(preupdate, "$init", "$2", -1),
"$arg", "$3", -1)

_, err = pg.Exec(`
WITH
ins AS (
INSERT INTO backups VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
),
new AS (
SELECT (`+preupdate+`) AS data
FROM backups WHERE id = $1
)
UPDATE backups SET data = `+updatefun+`
FROM new
WHERE id = $1
`, id, d, v)
return
}

func fetchBackupData(id string, data interface{}) (err error) {
var wrapper struct {
Data types.JSONText `db:"data"`
}
err = pg.Get(&wrapper, `SELECT data FROM backups WHERE id = $1`, id)

if err != nil {
return
}

err = wrapper.Data.Unmarshal(data)
return
}

func deleteBackupData(id string) (err error) {
_, err = pg.Exec(`DELETE FROM backups WHERE id = $1`, id)
return
}
BIN +6.13 KB icon.png
Binary file not shown.
Oops, something went wrong.

0 comments on commit fd30618

Please sign in to comment.
You can’t perform that action at this time.