api/pkg/modules/migration/trello/trello.go
konrad 9f3d898150 Add trello migration (#734)
Fix tests for background images

Generate docs

Fix lint

Do the swag

Add more logging

Remove the default bucket if it was empty

Add launch.json

Make importing backgrounds work

Add comment

Fix getting task attachments

Fix getting trello token

Add trello migration routes and status

Add support for converting checklists

Add test for attachments

Add the actual conversion

Add Trello conversion test

Add migration function stubs

Add basic trello migration structure

Add trello migration config

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#734
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
2020-12-17 13:44:04 +00:00

328 lines
10 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package trello
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
"github.com/adlio/trello"
)
// Migration represents the trello migration struct
type Migration struct {
Token string `json:"code"`
}
var trelloColorMap map[string]string
func init() {
trelloColorMap = make(map[string]string, 10)
trelloColorMap = map[string]string{
"green": "61bd4f",
"yellow": "f2d600",
"orange": "ff9f1a",
"red": "eb5a46",
"sky": "00c2e0",
"lime": "51e898",
"purple": "c377e0",
"blue": "0079bf",
"pink": "ff78cb",
"black": "344563",
"transparent": "", // Empty
}
}
// Name is used to get the name of the trello migration - we're using the docs here to annotate the status route.
// @Summary Get migration status
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} migration.Status "The migration status"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/trello/status [get]
func (m *Migration) Name() string {
return "trello"
}
// AuthURL returns the url users need to authenticate against
// @Summary Get the auth url from trello
// @Description Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from trello to Vikunja.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} handler.AuthURL "The auth url."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/trello/auth [get]
func (m *Migration) AuthURL() string {
return "https://trello.com/1/authorize" +
"?expiration=1hour" +
"&scope=read" +
"&callback_method=fragment" +
"&response_type=token" +
"&name=Vikunja%20Migration" +
"&key=" + config.MigrationTrelloKey.GetString() +
"&return_url=" + config.MigrationTrelloRedirectURL.GetString()
}
func getTrelloData(token string) (trelloData []*trello.Board, err error) {
allArg := trello.Arguments{"fields": "all"}
client := trello.NewClient(config.MigrationTrelloKey.GetString(), token)
client.Logger = log.GetLogger()
log.Debugf("[Trello Migration] Getting boards...")
trelloData, err = client.GetMyBoards(trello.Defaults())
if err != nil {
return
}
log.Debugf("[Trello Migration] Got %d trello boards", len(trelloData))
for _, board := range trelloData {
log.Debugf("[Trello Migration] Getting lists for board %s", board.ID)
board.Lists, err = board.GetLists(trello.Defaults())
if err != nil {
return
}
log.Debugf("[Trello Migration] Got %d lists for board %s", len(board.Lists), board.ID)
listMap := make(map[string]*trello.List, len(board.Lists))
for _, list := range board.Lists {
listMap[list.ID] = list
}
log.Debugf("[Trello Migration] Getting cards for board %s", board.ID)
cards, err := board.GetCards(allArg)
if err != nil {
return nil, err
}
log.Debugf("[Trello Migration] Got %d cards for board %s", len(cards), board.ID)
for _, card := range cards {
list, exists := listMap[card.IDList]
if !exists {
continue
}
card.Attachments, err = card.GetAttachments(allArg)
if err != nil {
return nil, err
}
list.Cards = append(list.Cards, card)
}
log.Debugf("[Trello Migration] Looked for attachements on all cards of board %s", board.ID)
}
return
}
// Converts all previously obtained data from trello into the vikunja format.
// `trelloData` should contain all boards with their lists and cards respectively.
func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
log.Debugf("[Trello Migration] ")
fullVikunjaHierachie = []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Imported from Trello",
},
Lists: []*models.List{},
},
}
var bucketID int64 = 1
log.Debugf("[Trello Migration] Converting %d boards to vikunja lists", len(trelloData))
for _, board := range trelloData {
list := &models.List{
Title: board.Name,
Description: board.Desc,
IsArchived: board.Closed,
}
// Background
// We're pretty much abusing the backgroundinformation field here - not sure if this is really better than adding a new property to the list
if board.Prefs.BackgroundImage != "" {
log.Debugf("[Trello Migration] Downloading background %s for board %s", board.Prefs.BackgroundImage, board.ID)
buf, err := migration.DownloadFile(board.Prefs.BackgroundImage)
if err != nil {
return nil, err
}
log.Debugf("[Trello Migration] Downloaded background %s for board %s", board.Prefs.BackgroundImage, board.ID)
list.BackgroundInformation = buf
} else {
log.Debugf("[Trello Migration] Board %s does not have a background image, not copying...", board.ID)
}
for _, l := range board.Lists {
bucket := &models.Bucket{
ID: bucketID,
Title: l.Name,
}
log.Debugf("[Trello Migration] Converting %d cards to tasks from board %s", len(l.Cards), board.ID)
for _, card := range l.Cards {
log.Debugf("[Trello Migration] Converting card %s", card.ID)
// The usual stuff: Title, description, position, bucket id
task := &models.Task{
Title: card.Name,
Description: card.Desc,
Position: card.Pos,
BucketID: bucketID,
}
if card.Due != nil {
task.DueDate = *card.Due
}
// Checklists (as markdown in description)
for _, checklist := range card.Checklists {
task.Description += "\n\n## " + checklist.Name + "\n"
for _, item := range checklist.CheckItems {
task.Description += "\n* "
if item.State == "completed" {
task.Description += "[x]"
} else {
task.Description += "[ ]"
}
task.Description += " " + item.Name
}
}
if len(card.Checklists) > 0 {
log.Debugf("[Trello Migration] Converted %d checklists from card %s", len(card.Checklists), card.ID)
}
// Labels
for _, label := range card.Labels {
color, exists := trelloColorMap[label.Color]
if !exists {
log.Debugf("[Trello Migration] Color %s not mapped for trello card %s, not adding label", label.Color, card.ID)
continue
}
task.Labels = append(task.Labels, &models.Label{
Title: label.Name,
HexColor: color,
})
log.Debugf("[Trello Migration] Converted label %s from card %s", label.ID, card.ID)
}
// Attachments
if len(card.Attachments) > 0 {
log.Debugf("[Trello Migration] Downloading %d card attachments from card %s", len(card.Attachments), card.ID)
}
for _, attachment := range card.Attachments {
if attachment.MimeType == "" { // Attachments can also be not downloadable - the mime type is empty in that case.
log.Debugf("[Trello Migration] Attachment %s does not have a mime type, not downloading", attachment.ID)
continue
}
log.Debugf("[Trello Migration] Downloading card attachment %s", attachment.ID)
buf, err := migration.DownloadFile(attachment.URL)
if err != nil {
return nil, err
}
task.Attachments = append(task.Attachments, &models.TaskAttachment{
File: &files.File{
Name: attachment.Name,
Mime: attachment.MimeType,
Size: uint64(buf.Len()),
FileContent: buf.Bytes(),
},
})
log.Debugf("[Trello Migration] Downloaded card attachment %s", attachment.ID)
}
list.Tasks = append(list.Tasks, task)
}
list.Buckets = append(list.Buckets, bucket)
bucketID++
}
log.Debugf("[Trello Migration] Converted all cards to tasks for board %s", board.ID)
fullVikunjaHierachie[0].Lists = append(fullVikunjaHierachie[0].Lists, list)
}
return
}
// Migrate gets all tasks from trello for a user and puts them into vikunja
// @Summary Migrate all lists, tasks etc. from trello
// @Description Migrates all projects, tasks, notes, reminders, subtasks and files from trello to vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param migrationCode body trello.Migration true "The auth token previously obtained from the auth url. See the docs for /migration/trello/auth."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/trello/migrate [post]
func (m *Migration) Migrate(u *user.User) (err error) {
log.Debugf("[Trello Migration] Starting migration for user %d", u.ID)
log.Debugf("[Trello Migration] Getting all trello data for user %d", u.ID)
trelloData, err := getTrelloData(m.Token)
if err != nil {
return
}
log.Debugf("[Trello Migration] Got all trello data for user %d", u.ID)
log.Debugf("[Trello Migration] Start converting trello data for user %d", u.ID)
fullVikunjaHierachie, err := convertTrelloDataToVikunja(trelloData)
if err != nil {
return
}
log.Debugf("[Trello Migration] Done migrating trello data for user %d", u.ID)
log.Debugf("[Trello Migration] Start inserting trello data for user %d", u.ID)
err = migration.InsertFromStructure(fullVikunjaHierachie, u)
if err != nil {
return
}
log.Debugf("[Trello Migration] Done inserting trello data for user %d", u.ID)
log.Debugf("[Trello Migration] Migration done for user %d", u.ID)
return nil
}