Add Microsoft Todo migration (#737)

Add more logs

Fix lint

Generate docs

Add swagger docs

Add microsoft todo parsing logic

go mod tidy

Add basic test structure

Add more fields to tasks

rename microsoft todo package

Add getting microsoft todo data

Add structs and helper methods

Add microsoft todo config and routes

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/737
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2020-12-18 11:12:05 +00:00
parent 25deb24849
commit f5b5204776
13 changed files with 951 additions and 29 deletions

View file

@ -197,6 +197,21 @@ migration:
# with the code obtained from the trello api.
# Note that the vikunja frontend expects this to end on /migrate/trello.
redirecturl: <frontend url>/migrate/trello
microsofttodo:
# Wheter to enable the microsoft todo migrator or not
enable: false
# The client id, required for making requests to the microsoft graph api
# See https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application
# for information about how to register your vikuinja instance.
clientid:
# The client secret, also required for making requests to the microsoft graph api
clientsecret:
# The url where clients are redirected after they authorized Vikunja to access their microsoft todo tasks.
# This needs to match the url you entered when registering your Vikunja instance at microsoft.
# This is usually the frontend url where the frontend then makes a request to /migration/microsoft-todo/migrate
# with the code obtained from the microsoft graph api.
# Note that the vikunja frontend expects this to be /migrate/microsoft-todo
redirecturl: <frontend url>/migrate/microsoft-todo
avatar:
# When using gravatar, this is the duration in seconds until a cached gravatar user avatar expires

View file

@ -516,6 +516,10 @@ Default: `<empty>`
Default: `<empty>`
### microsofttodo
Default: `<empty>`
---
## avatar

3
go.sum
View file

@ -692,6 +692,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20191009025716-f1972eb1d1f5 h1:Gojs/hac/DoYEM7WEICT45+hNWczIeuL5D21e5/HPAw=
@ -843,8 +844,6 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU=
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 h1:3wPMTskHO3+O6jqTEXyFcsnuxMQOqYSaHsDxcbUXpqA=

View file

@ -110,17 +110,21 @@ const (
FilesBasePath Key = `files.basepath`
FilesMaxSize Key = `files.maxsize`
MigrationWunderlistEnable Key = `migration.wunderlist.enable`
MigrationWunderlistClientID Key = `migration.wunderlist.clientid`
MigrationWunderlistClientSecret Key = `migration.wunderlist.clientsecret`
MigrationWunderlistRedirectURL Key = `migration.wunderlist.redirecturl`
MigrationTodoistEnable Key = `migration.todoist.enable`
MigrationTodoistClientID Key = `migration.todoist.clientid`
MigrationTodoistClientSecret Key = `migration.todoist.clientsecret`
MigrationTodoistRedirectURL Key = `migration.todoist.redirecturl`
MigrationTrelloEnable Key = `migration.trello.enable`
MigrationTrelloKey Key = `migration.trello.key`
MigrationTrelloRedirectURL Key = `migration.trello.redirecturl`
MigrationWunderlistEnable Key = `migration.wunderlist.enable`
MigrationWunderlistClientID Key = `migration.wunderlist.clientid`
MigrationWunderlistClientSecret Key = `migration.wunderlist.clientsecret`
MigrationWunderlistRedirectURL Key = `migration.wunderlist.redirecturl`
MigrationTodoistEnable Key = `migration.todoist.enable`
MigrationTodoistClientID Key = `migration.todoist.clientid`
MigrationTodoistClientSecret Key = `migration.todoist.clientsecret`
MigrationTodoistRedirectURL Key = `migration.todoist.redirecturl`
MigrationTrelloEnable Key = `migration.trello.enable`
MigrationTrelloKey Key = `migration.trello.key`
MigrationTrelloRedirectURL Key = `migration.trello.redirecturl`
MigrationMicrosoftTodoEnable Key = `migration.microsofttodo.enable`
MigrationMicrosoftTodoClientID Key = `migration.microsofttodo.clientid`
MigrationMicrosoftTodoClientSecret Key = `migration.microsofttodo.clientsecret`
MigrationMicrosoftTodoRedirectURL Key = `migration.microsofttodo.redirecturl`
CorsEnable Key = `cors.enable`
CorsOrigins Key = `cors.origins`
@ -292,6 +296,7 @@ func InitDefaultConfig() {
MigrationWunderlistEnable.setDefault(false)
MigrationTodoistEnable.setDefault(false)
MigrationTrelloEnable.setDefault(false)
MigrationMicrosoftTodoEnable.setDefault(false)
// Avatar
AvatarGravaterExpiration.setDefault(3600)
// List Backgrounds
@ -349,6 +354,10 @@ func InitConfig() {
MigrationTrelloRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/trello")
}
if MigrationMicrosoftTodoRedirectURL.GetString() == "" {
MigrationMicrosoftTodoRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/microsoft-todo")
}
log.Printf("Using config file: %s", viper.ConfigFileUsed())
}

View file

@ -20,6 +20,8 @@ import (
"bytes"
"context"
"net/http"
"net/url"
"strings"
)
// DownloadFile downloads a file and returns its contents
@ -38,3 +40,15 @@ func DownloadFile(url string) (buf *bytes.Buffer, err error) {
_, err = buf.ReadFrom(resp.Body)
return
}
// DoPost makes a form encoded post request
func DoPost(url string, form url.Values) (resp *http.Response, err error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(form.Encode()))
if err != nil {
return
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
hc := http.Client{}
return hc.Do(req)
}

View file

@ -0,0 +1,407 @@
// 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 microsofttodo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"code.vikunja.io/api/pkg/config"
"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"
)
const apiScopes = `tasks.read tasks.read.shared`
type Migration struct {
Code string `json:"code"`
}
type apiTokenResponse struct {
TokenType string `json:"token_type"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
AccessToken string `json:"access_token"`
}
type task struct {
OdataEtag string `json:"@odata.etag"`
Importance string `json:"importance"`
IsReminderOn bool `json:"isReminderOn"`
Status string `json:"status"`
Title string `json:"title"`
CreatedDateTime time.Time `json:"createdDateTime"`
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
ID string `json:"id"`
Body *body `json:"body"`
DueDateTime *dateTimeTimeZone `json:"dueDateTime"`
Recurrence *recurrence `json:"recurrence"`
ReminderDateTime *dateTimeTimeZone `json:"reminderDateTime"`
CompletedDateTime *dateTimeTimeZone `json:"completedDateTime"`
}
type dateTimeTimeZone struct {
DateTime string `json:"dateTime"`
TimeZone string `json:"timeZone"`
}
type body struct {
Content string `json:"content"`
ContentType string `json:"contentType"`
}
type pattern struct {
Type string `json:"type"`
Interval int64 `json:"interval"`
Month int64 `json:"month"`
DayOfMonth int64 `json:"dayOfMonth"`
DaysOfWeek []string `json:"daysOfWeek"`
FirstDayOfWeek string `json:"firstDayOfWeek"`
Index string `json:"index"`
}
type taskRange struct {
Type string `json:"type"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
RecurrenceTimeZone string `json:"recurrenceTimeZone"`
NumberOfOccurrences int `json:"numberOfOccurrences"`
}
type recurrence struct {
Pattern *pattern `json:"pattern"`
Range *taskRange `json:"range"`
}
type tasksResponse struct {
OdataContext string `json:"@odata.context"`
Value []*task `json:"value"`
}
type list struct {
ID string `json:"id"`
OdataEtag string `json:"@odata.etag"`
DisplayName string `json:"displayName"`
IsOwner bool `json:"isOwner"`
IsShared bool `json:"isShared"`
WellknownListName string `json:"wellknownListName"`
Tasks []*task `json:"-"` // This field does not exist in the api, we're just using it to return a structure with everything at once
}
type listsResponse struct {
OdataContext string `json:"@odata.context"`
Value []*list `json:"value"`
}
func (dtt *dateTimeTimeZone) toTime() (t time.Time, err error) {
loc, err := time.LoadLocation(dtt.TimeZone)
if err != nil {
return t, err
}
return time.ParseInLocation(time.RFC3339Nano, dtt.DateTime+"Z", loc)
}
// AuthURL returns the url users need to authenticate against
// @Summary Get the auth url from Microsoft Todo
// @Description Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from Microsoft Todo 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/microsoft-todo/auth [get]
func (m *Migration) AuthURL() string {
return "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" +
"?client_id=" + config.MigrationMicrosoftTodoClientID.GetString() +
"&response_type=code" +
"&redirect_uri=" + config.MigrationMicrosoftTodoRedirectURL.GetString() +
"&response_mode=query" +
"&scope=" + apiScopes
}
// Name is used to get the name of the Microsoft Todo 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/microsoft-todo/status [get]
func (m *Migration) Name() string {
return "microsoft-todo"
}
func getMicrosoftGraphAuthToken(code string) (accessToken string, err error) {
form := url.Values{
"client_id": []string{config.MigrationMicrosoftTodoClientID.GetString()},
"client_secret": []string{config.MigrationMicrosoftTodoClientSecret.GetString()},
"scope": []string{apiScopes},
"code": []string{code},
"redirect_uri": []string{config.MigrationMicrosoftTodoRedirectURL.GetString()},
"grant_type": []string{"authorization_code"},
}
resp, err := migration.DoPost("https://login.microsoftonline.com/common/oauth2/v2.0/token", form)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode > 399 {
buf := &bytes.Buffer{}
_, _ = buf.ReadFrom(resp.Body)
return "", fmt.Errorf("got http status %d while trying to get token, error was %s", resp.StatusCode, buf.String())
}
token := &apiTokenResponse{}
err = json.NewDecoder(resp.Body).Decode(token)
return token.AccessToken, err
}
func makeAuthenticatedGetRequest(token, urlPart string, v interface{}) error {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://graph.microsoft.com/v1.0/me/todo/"+urlPart, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return err
}
if resp.StatusCode > 399 {
return fmt.Errorf("Microsoft Graph API Error: Status Code: %d, Response was: %s", resp.StatusCode, buf.String())
}
// If the response is an empty json array, we need to exit here, otherwise this breaks the json parser since it
// expects a null for an empty slice
str := buf.String()
if str == "[]" {
return nil
}
return json.Unmarshal(buf.Bytes(), v)
}
func getMicrosoftTodoData(token string) (microsoftTodoData []*list, err error) {
microsoftTodoData = []*list{}
lists := &listsResponse{}
err = makeAuthenticatedGetRequest(token, "lists", lists)
if err != nil {
log.Errorf("[Microsoft Todo Migration] Could not get lists: %s", err)
return
}
log.Debugf("[Microsoft Todo Migration] Got %d lists", len(lists.Value))
for _, list := range lists.Value {
tasksResponse := &tasksResponse{}
err = makeAuthenticatedGetRequest(token, "lists/"+list.ID+"/tasks", tasksResponse)
if err != nil {
log.Errorf("[Microsoft Todo Migration] Could not get tasks for list %s: %s", list.ID, err)
return
}
log.Debugf("[Microsoft Todo Migration] Got %d tasks for list %s", len(tasksResponse.Value), list.ID)
list.Tasks = tasksResponse.Value
microsoftTodoData = append(microsoftTodoData, list)
}
log.Debugf("[Microsoft Todo Migration] Got all tasks for %d lists", len(lists.Value))
return
}
func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.NamespaceWithLists, err error) {
// One namespace with all lists
vikunjsStructure = []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Migrated from Microsoft Todo",
},
Lists: []*models.List{},
},
}
log.Debugf("[Microsoft Todo Migration] Converting %d lists", len(todoData))
for _, l := range todoData {
log.Debugf("[Microsoft Todo Migration] Converting list %s", l.ID)
// Lists only with title
list := &models.List{
Title: l.DisplayName,
}
log.Debugf("[Microsoft Todo Migration] Converting %d tasks", len(l.Tasks))
for _, t := range l.Tasks {
log.Debugf("[Microsoft Todo Migration] Converting task %s", t.ID)
task := &models.Task{
Title: t.Title,
Done: t.Status == "completed",
}
// Done Status
if task.Done {
log.Debugf("[Microsoft Todo Migration] Converting done at for task %s", t.ID)
task.DoneAt, err = t.CompletedDateTime.toTime()
if err != nil {
return
}
}
// Description
if t.Body != nil && t.Body.ContentType == "text" {
task.Description = t.Body.Content
}
// Priority
switch t.Importance {
case "low":
task.Priority = 1
case "normal":
task.Priority = 2
case "high":
task.Priority = 3
default:
task.Priority = 0
}
// Reminders
if t.ReminderDateTime != nil {
log.Debugf("[Microsoft Todo Migration] Converting reminder for task %s", t.ID)
reminder, err := t.ReminderDateTime.toTime()
if err != nil {
return nil, err
}
task.Reminders = []time.Time{reminder}
}
// Due Date
if t.DueDateTime != nil {
log.Debugf("[Microsoft Todo Migration] Converting due date for task %s", t.ID)
dueDate, err := t.DueDateTime.toTime()
if err != nil {
return nil, err
}
task.DueDate = dueDate
}
// Repeating
if t.Recurrence != nil && t.Recurrence.Pattern != nil {
log.Debugf("[Microsoft Todo Migration] Converting recurring pattern for task %s", t.ID)
switch t.Recurrence.Pattern.Type {
case "daily":
task.RepeatAfter = t.Recurrence.Pattern.Interval * 60 * 60 * 24
case "weekly":
task.RepeatAfter = t.Recurrence.Pattern.Interval * 60 * 60 * 24 * 7
case "monthly":
task.RepeatAfter = t.Recurrence.Pattern.Interval * 60 * 60 * 24 * 30
case "yearly":
task.RepeatAfter = t.Recurrence.Pattern.Interval * 60 * 60 * 24 * 365
}
}
list.Tasks = append(list.Tasks, task)
log.Debugf("[Microsoft Todo Migration] Done converted %d tasks", len(l.Tasks))
}
vikunjsStructure[0].Lists = append(vikunjsStructure[0].Lists, list)
log.Debugf("[Microsoft Todo Migration] Done converting list %s", l.ID)
}
return
}
// Migrate gets all tasks from Microsoft Todo for a user and puts them into vikunja
// @Summary Migrate all lists, tasks etc. from Microsoft Todo
// @Description Migrates all tasklinsts, tasks, notes and reminders from Microsoft Todo to Vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param migrationCode body microsofttodo.Migration true "The auth token previously obtained from the auth url. See the docs for /migration/microsoft-todo/auth."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/microsoft-todo/migrate [post]
func (m *Migration) Migrate(user *user.User) (err error) {
log.Debugf("[Microsoft Todo Migration] Start Microsoft Todo migration for user %d", user.ID)
log.Debugf("[Microsoft Todo Migration] Getting Microsoft Graph api token")
token, err := getMicrosoftGraphAuthToken(m.Code)
if err != nil {
log.Debugf("[Microsoft Todo Migration] Error getting auth token: %s", err)
return
}
log.Debugf("[Microsoft Todo Migration] Got Microsoft Graph api token")
log.Debugf("[Microsoft Todo Migration] Retrieving Microsoft Todo data")
todoData, err := getMicrosoftTodoData(token)
if err != nil {
log.Debugf("[Microsoft Todo Migration] Error getting Microsoft Todo data: %s", err)
return
}
log.Debugf("[Microsoft Todo Migration] Got Microsoft Todo data")
log.Debugf("[Microsoft Todo Migration] Start converting Microsoft Todo data")
vikunjaStructure, err := convertMicrosoftTodoData(todoData)
if err != nil {
log.Debugf("[Microsoft Todo Migration] Error converting Microsoft Todo data: %s", err)
return
}
log.Debugf("[Microsoft Todo Migration] Done converting Microsoft Todo data")
log.Debugf("[Microsoft Todo Migration] Creating new structure")
err = migration.InsertFromStructure(vikunjaStructure, user)
if err != nil {
log.Debugf("[Microsoft Todo Migration] Error while creating new structure: %s", err)
return
}
log.Debugf("[Microsoft Todo Migration] Created new structure")
log.Debugf("[Microsoft Todo Migration] Microsoft Todo migration done for user %d", user.ID)
return
}

View file

@ -0,0 +1,169 @@
// 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 microsofttodo
import (
"testing"
"time"
"code.vikunja.io/api/pkg/models"
"github.com/d4l3k/messagediff"
"github.com/stretchr/testify/assert"
)
func TestConverting(t *testing.T) {
testtime := &dateTimeTimeZone{
DateTime: "2020-12-18T03:00:00.4770000",
TimeZone: "UTC",
}
testtimeTime, err := time.Parse(time.RFC3339Nano, "2020-12-18T03:00:00.4770000Z")
assert.NoError(t, err)
microsoftTodoData := []*list{
{
DisplayName: "List 1",
Tasks: []*task{
{
Title: "Task 1",
Status: "notStarted",
Body: &body{
Content: "This is a description",
ContentType: "text",
},
},
{
Title: "Task 2",
Status: "completed",
CompletedDateTime: testtime,
},
{
Title: "Task 3",
Status: "notStarted",
Importance: "low",
},
{
Title: "Task 4",
Status: "notStarted",
Importance: "high",
},
{
Title: "Task 5",
Status: "notStarted",
IsReminderOn: true,
ReminderDateTime: testtime,
},
{
Title: "Task 6",
Status: "notStarted",
DueDateTime: testtime,
},
{
Title: "Task 7",
Status: "notStarted",
DueDateTime: testtime,
Recurrence: &recurrence{
Pattern: &pattern{
// Every week
Type: "weekly",
Interval: 1,
},
},
},
},
},
{
DisplayName: "List 2",
Tasks: []*task{
{
Title: "Task 1",
Status: "notStarted",
},
{
Title: "Task 2",
Status: "notStarted",
},
},
},
}
expectedHierachie := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Migrated from Microsoft Todo",
},
Lists: []*models.List{
{
Title: "List 1",
Tasks: []*models.Task{
{
Title: "Task 1",
Description: "This is a description",
},
{
Title: "Task 2",
Done: true,
DoneAt: testtimeTime,
},
{
Title: "Task 3",
Priority: 1,
},
{
Title: "Task 4",
Priority: 3,
},
{
Title: "Task 5",
Reminders: []time.Time{
testtimeTime,
},
},
{
Title: "Task 6",
DueDate: testtimeTime,
},
{
Title: "Task 7",
DueDate: testtimeTime,
RepeatAfter: 60 * 60 * 24 * 7, // The amount of seconds in a week
},
},
},
{
Title: "List 2",
Tasks: []*models.Task{
{
Title: "Task 1",
},
{
Title: "Task 2",
},
},
},
},
},
}
hierachie, err := convertMicrosoftTodoData(microsoftTodoData)
assert.NoError(t, err)
assert.NotNil(t, hierachie)
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {
t.Errorf("converted microsoft todo data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
}
}

View file

@ -18,13 +18,10 @@ package todoist
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
@ -229,17 +226,6 @@ func (m *Migration) AuthURL() string {
"&state=" + utils.MakeRandomString(32)
}
func doPost(url string, form url.Values) (resp *http.Response, err error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, strings.NewReader(form.Encode()))
if err != nil {
return
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
hc := http.Client{}
return hc.Do(req)
}
func convertTodoistToVikunja(sync *sync) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
newNamespace := &models.NamespaceWithLists{
@ -451,7 +437,7 @@ func getAccessTokenFromAuthToken(authToken string) (accessToken string, err erro
"code": []string{authToken},
"redirect_uri": []string{config.MigrationTodoistRedirectURL.GetString()},
}
resp, err := doPost("https://todoist.com/oauth/access_token", form)
resp, err := migration.DoPost("https://todoist.com/oauth/access_token", form)
if err != nil {
return
}
@ -503,7 +489,7 @@ func (m *Migration) Migrate(u *user.User) (err error) {
"sync_token": []string{"*"},
"resource_types": []string{"[\"all\"]"},
}
resp, err := doPost("https://api.todoist.com/sync/v8/sync", form)
resp, err := migration.DoPost("https://api.todoist.com/sync/v8/sync", form)
if err != nil {
return
}

View file

@ -19,6 +19,8 @@ package v1
import (
"net/http"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/trello"
"code.vikunja.io/api/pkg/log"
@ -121,6 +123,10 @@ func Info(c echo.Context) error {
m := &trello.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
}
if config.MigrationMicrosoftTodoEnable.GetBool() {
m := &microsofttodo.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
}
if config.BackgroundsEnabled.GetBool() {
if config.BackgroundsUploadEnabled.GetBool() {

View file

@ -50,6 +50,8 @@ import (
"strings"
"time"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/trello"
"code.vikunja.io/api/pkg/config"
@ -543,6 +545,16 @@ func registerAPIRoutes(a *echo.Group) {
trelloMigrationHandler.RegisterRoutes(m)
}
// Microsoft Todo
if config.MigrationMicrosoftTodoEnable.GetBool() {
microsoftTodoMigrationHandler := &migrationHandler.MigrationWeb{
MigrationStruct: func() migration.Migrator {
return &microsofttodo.Migration{}
},
}
microsoftTodoMigrationHandler.RegisterRoutes(m)
}
// List Backgrounds
if config.BackgroundsEnabled.GetBool() {
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)

View file

@ -2502,6 +2502,113 @@ var doc = `{
}
}
},
"/migration/microsoft-todo/auth": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from Microsoft Todo to Vikunja.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get the auth url from Microsoft Todo",
"responses": {
"200": {
"description": "The auth url.",
"schema": {
"$ref": "#/definitions/handler.AuthURL"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/microsoft-todo/migrate": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Migrates all tasklinsts, tasks, notes and reminders from Microsoft Todo to Vikunja.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Migrate all lists, tasks etc. from Microsoft Todo",
"parameters": [
{
"description": "The auth token previously obtained from the auth url. See the docs for /migration/microsoft-todo/auth.",
"name": "migrationCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/microsofttodo.Migration"
}
}
],
"responses": {
"200": {
"description": "A message telling you everything was migrated successfully.",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/microsoft-todo/status": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"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.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get migration status",
"responses": {
"200": {
"description": "The migration status",
"schema": {
"$ref": "#/definitions/migration.Status"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/todoist/auth": {
"get": {
"security": [
@ -6657,6 +6764,14 @@ var doc = `{
}
}
},
"microsofttodo.Migration": {
"type": "object",
"properties": {
"code": {
"type": "string"
}
}
},
"migration.Status": {
"type": "object",
"properties": {

View file

@ -2485,6 +2485,113 @@
}
}
},
"/migration/microsoft-todo/auth": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from Microsoft Todo to Vikunja.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get the auth url from Microsoft Todo",
"responses": {
"200": {
"description": "The auth url.",
"schema": {
"$ref": "#/definitions/handler.AuthURL"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/microsoft-todo/migrate": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Migrates all tasklinsts, tasks, notes and reminders from Microsoft Todo to Vikunja.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Migrate all lists, tasks etc. from Microsoft Todo",
"parameters": [
{
"description": "The auth token previously obtained from the auth url. See the docs for /migration/microsoft-todo/auth.",
"name": "migrationCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/microsofttodo.Migration"
}
}
],
"responses": {
"200": {
"description": "A message telling you everything was migrated successfully.",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/microsoft-todo/status": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"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.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get migration status",
"responses": {
"200": {
"description": "The migration status",
"schema": {
"$ref": "#/definitions/migration.Status"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/todoist/auth": {
"get": {
"security": [
@ -6640,6 +6747,14 @@
}
}
},
"microsofttodo.Migration": {
"type": "object",
"properties": {
"code": {
"type": "string"
}
}
},
"migration.Status": {
"type": "object",
"properties": {

View file

@ -35,6 +35,11 @@ definitions:
url:
type: string
type: object
microsofttodo.Migration:
properties:
code:
type: string
type: object
migration.Status:
properties:
id:
@ -2671,6 +2676,72 @@ paths:
summary: Login
tags:
- user
/migration/microsoft-todo/auth:
get:
description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from Microsoft Todo to Vikunja.
produces:
- application/json
responses:
"200":
description: The auth url.
schema:
$ref: '#/definitions/handler.AuthURL'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get the auth url from Microsoft Todo
tags:
- migration
/migration/microsoft-todo/migrate:
post:
consumes:
- application/json
description: Migrates all tasklinsts, tasks, notes and reminders from Microsoft Todo to Vikunja.
parameters:
- description: The auth token previously obtained from the auth url. See the docs for /migration/microsoft-todo/auth.
in: body
name: migrationCode
required: true
schema:
$ref: '#/definitions/microsofttodo.Migration'
produces:
- application/json
responses:
"200":
description: A message telling you everything was migrated successfully.
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Migrate all lists, tasks etc. from Microsoft Todo
tags:
- migration
/migration/microsoft-todo/status:
get:
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.
produces:
- application/json
responses:
"200":
description: The migration status
schema:
$ref: '#/definitions/migration.Status'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get migration status
tags:
- migration
/migration/todoist/auth:
get:
description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from todoist to Vikunja.