2020-12-17 13:44:04 +00:00
// Vikunja is a to-do list application to facilitate your life.
2023-09-01 06:32:28 +00:00
// Copyright 2018-present Vikunja and contributors. All rights reserved.
2020-12-17 13:44:04 +00:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 15:41:52 +00:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-12-17 13:44:04 +00: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 15:41:52 +00:00
// GNU Affero General Public Licensee for more details.
2020-12-17 13:44:04 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-12-17 13:44:04 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package trello
import (
2024-03-09 08:31:50 +00:00
"bytes"
2020-12-17 13:44:04 +00:00
"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"
2024-03-12 19:25:58 +00:00
2020-12-17 13:44:04 +00:00
"github.com/adlio/trello"
2024-03-09 08:31:50 +00:00
"github.com/yuin/goldmark"
2020-12-17 13:44:04 +00:00
)
// Migration represents the trello migration struct
type Migration struct {
Token string ` json:"code" `
}
var trelloColorMap map [ string ] string
func init ( ) {
2024-03-10 11:23:38 +00:00
trelloColorMap = make ( map [ string ] string , 30 )
2020-12-17 13:44:04 +00:00
trelloColorMap = map [ string ] string {
2024-03-10 11:23:38 +00:00
"green" : "4bce97" ,
"yellow" : "f5cd47" ,
"orange" : "fea362" ,
"red" : "f87168" ,
"purple" : "9f8fef" ,
"blue" : "579dff" ,
"sky" : "6cc3e0" ,
"lime" : "94c748" ,
"pink" : "e774bb" ,
"black" : "8590a2" ,
"green_dark" : "1f845a" ,
"yellow_dark" : "946f00" ,
"orange_dark" : "c25100" ,
"red_dark" : "c9372c" ,
"purple_dark" : "6e5dc6" ,
"blue_dark" : "0c66e4" ,
"sky_dark" : "227d9b" ,
"lime_dark" : "5b7f24" ,
"pink_dark" : "ae4787" ,
"black_dark" : "626f86" ,
"green_light" : "baf3db" ,
"yellow_light" : "f8e6a0" ,
"orange_light" : "fedec8" ,
"red_light" : "ffd5d2" ,
"purple_light" : "dfd8fd" ,
"blue_light" : "cce0ff" ,
"sky_light" : "c6edfb" ,
"lime_light" : "d3f1a7" ,
"ping_light" : "fdd0ec" ,
"black_light" : "dcdfe4" ,
"transparent" : "" , // Empty
2020-12-17 13:44:04 +00:00
}
}
// 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" +
2024-03-06 14:13:54 +00:00
"?expiration=never" +
2020-12-17 13:44:04 +00:00
"&scope=read" +
"&callback_method=fragment" +
"&response_type=token" +
"&name=Vikunja%20Migration" +
"&key=" + config . MigrationTrelloKey . GetString ( ) +
"&return_url=" + config . MigrationTrelloRedirectURL . GetString ( )
}
2024-04-09 10:54:38 +00:00
func getTrelloBoards ( client * trello . Client ) ( trelloData [ ] * trello . Board , err error ) {
2020-12-17 13:44:04 +00:00
log . Debugf ( "[Trello Migration] Getting boards..." )
trelloData , err = client . GetMyBoards ( trello . Defaults ( ) )
if err != nil {
2024-04-09 10:54:38 +00:00
return nil , err
2020-12-17 13:44:04 +00:00
}
log . Debugf ( "[Trello Migration] Got %d trello boards" , len ( trelloData ) )
2024-04-09 10:54:38 +00:00
return
}
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
func getTrelloOrganizationsWithBoards ( boards [ ] * trello . Board ) ( boardsByOrg map [ string ] [ ] * trello . Board ) {
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
boardsByOrg = make ( map [ string ] [ ] * trello . Board )
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
for _ , board := range boards {
// Trello boards without an organization are considered personal boards
if board . IDOrganization == "" {
board . IDOrganization = "Personal"
2020-12-17 13:44:04 +00:00
}
2024-04-09 10:54:38 +00:00
_ , has := boardsByOrg [ board . IDOrganization ]
if ! has {
boardsByOrg [ board . IDOrganization ] = [ ] * trello . Board { }
2020-12-17 13:44:04 +00:00
}
2024-04-09 10:54:38 +00:00
boardsByOrg [ board . IDOrganization ] = append ( boardsByOrg [ board . IDOrganization ] , board )
}
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
return
}
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
func fillCardData ( client * trello . Client , board * trello . Board ) ( err error ) {
allArg := trello . Arguments { "fields" : "all" }
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
log . Debugf ( "[Trello Migration] Getting projects for board %s" , board . ID )
2023-02-24 11:13:18 +00:00
2024-04-09 10:54:38 +00:00
board . Lists , err = board . GetLists ( trello . Defaults ( ) )
if err != nil {
return err
}
log . Debugf ( "[Trello Migration] Got %d projects 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
}
log . Debugf ( "[Trello Migration] Got %d cards for board %s" , len ( cards ) , board . ID )
2023-02-24 11:13:18 +00:00
2024-04-09 10:54:38 +00:00
for _ , card := range cards {
list , exists := listMap [ card . IDList ]
if ! exists {
continue
}
card . Attachments , err = card . GetAttachments ( allArg )
if err != nil {
return
}
if len ( card . IDCheckLists ) > 0 {
for _ , checkListID := range card . IDCheckLists {
checklist , err := client . GetChecklist ( checkListID , allArg )
if err != nil {
return err
2023-02-24 11:13:18 +00:00
}
2024-04-09 10:54:38 +00:00
checklist . CheckItems = [ ] trello . CheckItem { }
err = client . Get ( "checklists/" + checkListID + "/checkItems" , allArg , & checklist . CheckItems )
if err != nil {
return err
}
card . Checklists = append ( card . Checklists , checklist )
log . Debugf ( "Retrieved checklist %s for card %s" , checkListID , card . ID )
}
2020-12-17 13:44:04 +00:00
}
2024-04-09 10:54:38 +00:00
list . Cards = append ( list . Cards , card )
2020-12-17 13:44:04 +00:00
}
2024-04-09 10:54:38 +00:00
log . Debugf ( "[Trello Migration] Looked for attachements on all cards of board %s" , board . ID )
2020-12-17 13:44:04 +00:00
return
}
2024-03-09 08:31:50 +00:00
func convertMarkdownToHTML ( input string ) ( output string , err error ) {
var buf bytes . Buffer
err = goldmark . Convert ( [ ] byte ( input ) , & buf )
if err != nil {
return
}
//#nosec - we are not responsible to escape this as we don't know the context where it is used
return buf . String ( ) , nil
}
2020-12-17 13:44:04 +00:00
// Converts all previously obtained data from trello into the vikunja format.
2022-11-13 16:07:01 +00:00
// `trelloData` should contain all boards with their projects and cards respectively.
2024-04-09 10:54:38 +00:00
func convertTrelloDataToVikunja ( organizationName string , trelloData [ ] * trello . Board , token string ) ( fullVikunjaHierachie [ ] * models . ProjectWithTasksAndBuckets , err error ) {
2020-12-17 13:44:04 +00:00
log . Debugf ( "[Trello Migration] " )
2023-11-08 21:56:10 +00:00
var pseudoParentID int64 = 1
2022-12-29 17:11:15 +00:00
fullVikunjaHierachie = [ ] * models . ProjectWithTasksAndBuckets {
2020-12-17 13:44:04 +00:00
{
2022-12-29 17:11:15 +00:00
Project : models . Project {
2023-11-08 21:56:10 +00:00
ID : pseudoParentID ,
2024-04-09 10:54:38 +00:00
Title : organizationName ,
2020-12-17 13:44:04 +00:00
} ,
} ,
}
var bucketID int64 = 1
2022-11-13 16:07:01 +00:00
log . Debugf ( "[Trello Migration] Converting %d boards to vikunja projects" , len ( trelloData ) )
2020-12-17 13:44:04 +00:00
2023-11-08 21:56:10 +00:00
for index , board := range trelloData {
2022-11-13 16:07:01 +00:00
project := & models . ProjectWithTasksAndBuckets {
Project : models . Project {
2023-11-08 21:56:10 +00:00
ID : int64 ( index + 1 ) + pseudoParentID ,
ParentProjectID : pseudoParentID ,
Title : board . Name ,
Description : board . Desc ,
IsArchived : board . Closed ,
2021-09-04 19:26:31 +00:00
} ,
2020-12-17 13:44:04 +00:00
}
// Background
2022-11-13 16:07:01 +00:00
// We're pretty much abusing the backgroundinformation field here - not sure if this is really better than adding a new property to the project
2020-12-17 13:44:04 +00:00
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 )
2022-11-13 16:07:01 +00:00
project . BackgroundInformation = buf
2020-12-17 13:44:04 +00:00
} 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 {
2024-03-14 21:28:07 +00:00
Title : card . Name ,
BucketID : bucketID ,
2020-12-17 13:44:04 +00:00
}
2024-03-09 08:31:50 +00:00
task . Description , err = convertMarkdownToHTML ( card . Desc )
if err != nil {
return nil , err
}
2020-12-17 13:44:04 +00:00
if card . Due != nil {
task . DueDate = * card . Due
}
// Checklists (as markdown in description)
for _ , checklist := range card . Checklists {
2024-03-09 09:01:02 +00:00
task . Description += "\n\n<h2> " + checklist . Name + "</h2>\n\n" + ` <ul data-type="taskList"> `
2020-12-17 13:44:04 +00:00
for _ , item := range checklist . CheckItems {
2024-03-09 09:01:02 +00:00
task . Description += "\n"
2023-02-24 11:13:18 +00:00
if item . State == "complete" {
2024-03-09 09:01:02 +00:00
task . Description += ` <li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p> ` + item . Name + ` </p></div></li> `
2020-12-17 13:44:04 +00:00
} else {
2024-03-09 09:01:02 +00:00
task . Description += ` <li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p> ` + item . Name + ` </p></div></li> `
2020-12-17 13:44:04 +00:00
}
}
2024-03-09 09:01:02 +00:00
task . Description += "</ul>"
2020-12-17 13:44:04 +00:00
}
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 {
2024-03-10 11:23:38 +00:00
log . Debugf ( "[Trello Migration] Color %s not mapped for trello card %s, falling back to transparent" , label . Color , card . ID )
color = trelloColorMap [ "transparent" ]
2020-12-17 13:44:04 +00:00
}
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 14:22:45 +00:00
// Attachments
2020-12-17 13:44:04 +00: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 {
2024-03-10 17:41:37 +00:00
if ! attachment . IsUpload { // There are other types of attachments which are not files. We can only handle files.
2020-12-17 13:44:04 +00: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 20:47:51 +00:00
buf , err := migration . DownloadFileWithHeaders ( attachment . URL , map [ string ] [ ] string {
"Authorization" : { ` OAuth oauth_consumer_key=" ` + config . MigrationTrelloKey . GetString ( ) + ` ", oauth_token=" ` + token + ` " ` } ,
} )
2020-12-17 13:44:04 +00:00
if err != nil {
return nil , err
}
2024-03-10 15:30:01 +00:00
vikunjaAttachment := & models . TaskAttachment {
2020-12-17 13:44:04 +00:00
File : & files . File {
Name : attachment . Name ,
Mime : attachment . MimeType ,
Size : uint64 ( buf . Len ( ) ) ,
FileContent : buf . Bytes ( ) ,
} ,
2024-03-10 15:30:01 +00:00
}
if card . IDAttachmentCover != "" && card . IDAttachmentCover == attachment . ID {
vikunjaAttachment . ID = 42
task . CoverImageAttachmentID = 42
}
task . Attachments = append ( task . Attachments , vikunjaAttachment )
2020-12-17 13:44:04 +00:00
log . Debugf ( "[Trello Migration] Downloaded card attachment %s" , attachment . ID )
}
2024-03-10 15:30:01 +00:00
// When the cover image was set manually, we need to add it as an attachment
if card . ManualCoverAttachment && len ( card . Cover . Scaled ) > 0 {
cover := card . Cover . Scaled [ len ( card . Cover . Scaled ) - 1 ]
buf , err := migration . DownloadFile ( cover . URL )
if err != nil {
return nil , err
}
coverAttachment := & models . TaskAttachment {
ID : 43 ,
File : & files . File {
Name : cover . ID + ".jpg" ,
Mime : "image/jpg" , // Seems to always return jpg
Size : uint64 ( buf . Len ( ) ) ,
FileContent : buf . Bytes ( ) ,
} ,
}
task . Attachments = append ( task . Attachments , coverAttachment )
task . CoverImageAttachmentID = coverAttachment . ID
}
2022-11-13 16:07:01 +00:00
project . Tasks = append ( project . Tasks , & models . TaskWithComments { Task : * task } )
2020-12-17 13:44:04 +00:00
}
2022-11-13 16:07:01 +00:00
project . Buckets = append ( project . Buckets , bucket )
2020-12-17 13:44:04 +00:00
bucketID ++
}
log . Debugf ( "[Trello Migration] Converted all cards to tasks for board %s" , board . ID )
2023-11-08 21:56:10 +00:00
fullVikunjaHierachie = append ( fullVikunjaHierachie , project )
2020-12-17 13:44:04 +00:00
}
return
}
// Migrate gets all tasks from trello for a user and puts them into vikunja
2022-11-13 16:07:01 +00:00
// @Summary Migrate all projects, tasks etc. from trello
2020-12-17 13:44:04 +00:00
// @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 )
2024-04-09 10:54:38 +00:00
client := trello . NewClient ( config . MigrationTrelloKey . GetString ( ) , m . Token )
client . Logger = log . GetLogger ( )
boards , err := getTrelloBoards ( client )
2020-12-17 13:44:04 +00:00
if err != nil {
return
}
log . Debugf ( "[Trello Migration] Got all trello data for user %d" , u . ID )
2024-04-09 10:54:38 +00:00
organizationMap := getTrelloOrganizationsWithBoards ( boards )
for organizationID , boards := range organizationMap {
log . Debugf ( "[Trello Migration] Getting organization with id %s for user %d" , organizationID , u . ID )
orgName := organizationID
if organizationID != "Personal" {
organization , err := client . GetOrganization ( organizationID , trello . Defaults ( ) )
if err != nil {
return err
}
orgName = organization . DisplayName
}
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
for _ , board := range boards {
log . Debugf ( "[Trello Migration] Getting card data for board %s for user %d for organization %s" , board . ID , u . ID , organizationID )
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
err = fillCardData ( client , board )
if err != nil {
return err
}
log . Debugf ( "[Trello Migration] Got card data for board %s for user %d for organization %s" , board . ID , u . ID , organizationID )
}
log . Debugf ( "[Trello Migration] Start converting trello data for user %d for organization %s" , u . ID , organizationID )
hierarchy , err := convertTrelloDataToVikunja ( orgName , boards , m . Token )
if err != nil {
return err
}
log . Debugf ( "[Trello Migration] Done migrating trello data for user %d for organization %s" , u . ID , organizationID )
log . Debugf ( "[Trello Migration] Start inserting trello data for user %d for organization %s" , u . ID , organizationID )
err = migration . InsertFromStructure ( hierarchy , u )
if err != nil {
return err
}
log . Debugf ( "[Trello Migration] Done inserting trello data for user %d for organization %s" , u . ID , organizationID )
2020-12-17 13:44:04 +00:00
}
2024-04-09 10:54:38 +00:00
log . Debugf ( "[Trello Migration] Done migrating all trello data for user %d" , u . ID )
2020-12-17 13:44:04 +00:00
2024-04-09 10:54:38 +00:00
return
2020-12-17 13:44:04 +00:00
}