2020-12-17 14:44:04 +01:00
// Vikunja is a to-do list application to facilitate your life.
2021-02-02 20:19:13 +01:00
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
2020-12-17 14:44:04 +01:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 16:41:52 +01:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-12-17 14:44:04 +01:00
// 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
2020-12-23 16:41:52 +01:00
// GNU Affero General Public Licensee for more details.
2020-12-17 14:44:04 +01:00
//
2020-12-23 16:41:52 +01:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-12-17 14:44:04 +01:00
// 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.
2021-11-14 21:47:51 +01:00
func convertTrelloDataToVikunja ( trelloData [ ] * trello . Board , token string ) ( fullVikunjaHierachie [ ] * models . NamespaceWithListsAndTasks , err error ) {
2020-12-17 14:44:04 +01:00
log . Debugf ( "[Trello Migration] " )
2021-09-04 21:26:31 +02:00
fullVikunjaHierachie = [ ] * models . NamespaceWithListsAndTasks {
2020-12-17 14:44:04 +01:00
{
Namespace : models . Namespace {
Title : "Imported from Trello" ,
} ,
2021-09-04 21:26:31 +02:00
Lists : [ ] * models . ListWithTasksAndBuckets { } ,
2020-12-17 14:44:04 +01:00
} ,
}
var bucketID int64 = 1
log . Debugf ( "[Trello Migration] Converting %d boards to vikunja lists" , len ( trelloData ) )
for _ , board := range trelloData {
2021-09-04 21:26:31 +02:00
list := & models . ListWithTasksAndBuckets {
List : models . List {
Title : board . Name ,
Description : board . Desc ,
IsArchived : board . Closed ,
} ,
2020-12-17 14:44:04 +01:00
}
// 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 {
2021-07-28 21:06:40 +02:00
Title : card . Name ,
Description : card . Desc ,
KanbanPosition : card . Pos ,
BucketID : bucketID ,
2020-12-17 14:44:04 +01:00
}
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 )
}
2022-06-19 16:22:45 +02:00
// Attachments
2020-12-17 14:44:04 +01:00
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 {
2022-06-19 16:22:45 +02:00
if attachment . MimeType == "" { // Attachments can also be not downloadable - the mime type is empty in that case.
2020-12-17 14:44:04 +01:00
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 )
2021-11-14 21:47:51 +01:00
buf , err := migration . DownloadFileWithHeaders ( attachment . URL , map [ string ] [ ] string {
"Authorization" : { ` OAuth oauth_consumer_key=" ` + config . MigrationTrelloKey . GetString ( ) + ` ", oauth_token=" ` + token + ` " ` } ,
} )
2020-12-17 14:44:04 +01:00
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 )
}
2021-09-04 21:26:31 +02:00
list . Tasks = append ( list . Tasks , & models . TaskWithComments { Task : * task } )
2020-12-17 14:44:04 +01:00
}
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 )
2021-11-14 21:47:51 +01:00
fullVikunjaHierachie , err := convertTrelloDataToVikunja ( trelloData , m . Token )
2020-12-17 14:44:04 +01:00
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
}