diff --git a/cmd/develop/main.go b/cmd/develop/main.go index c1835b4..37ee46d 100644 --- a/cmd/develop/main.go +++ b/cmd/develop/main.go @@ -29,10 +29,10 @@ func main() { // } client = api.NewSyncClient(os.Getenv("TODOIST_API_KEY")) - client.sync(true) + client.Synchronize(true) for { - _, changes, err := client.sync(false) + changes, err := client.Synchronize(false) if err != nil { fmt.Println("Error syncing:", err) diff --git a/internal/api/activity.go b/internal/api/activity.go index 2ea1953..f594a18 100644 --- a/internal/api/activity.go +++ b/internal/api/activity.go @@ -3,7 +3,6 @@ package api import ( "encoding/json" "io" - "net/http" "net/url" "time" ) @@ -27,17 +26,12 @@ type Event struct { func (sc *SyncClient) RecentlyCompleted() (*ActivityLog, error) { params := url.Values{"event_type": {"completed"}} - req, err := http.NewRequest("GET", API_BASE_URL+"/activity/get?"+params.Encode(), nil) + + resp, err := sc.get("activity/get", params) if err != nil { return nil, err } - req.Header.Set("Authorization", "Bearer "+sc.ApiToken) - - resp, err := sc.Http.Do(req) - if err != nil { - return nil, err - } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) diff --git a/internal/api/sync.go b/internal/api/sync.go index 8edddaf..6a36f1f 100644 --- a/internal/api/sync.go +++ b/internal/api/sync.go @@ -1,7 +1,38 @@ package api -type ClientState struct { - // Items map[string]Item +import "encoding/json" + +type SyncResponse struct { + // The new token to use for the next incremental sync. + SyncToken string `json:"sync_token"` + // If true, this response is a full sync and the client should not merge but instead replace it's state. + FullSync bool `json:"full_sync"` + // Used for commands where local IDs are temporarily chosen by the client and need to be mapped to the server's IDs. + TempIDMapping map[string]interface{} `json:"temp_id_mapping"` + Items []interface{} `json:"items"` + // CompletedInfo []interface{} `json:"completed_info"` + // Collaborators []interface{} `json:"collaborators"` + // CollaboratorStates []interface{} `json:"collaborator_states"` + // DayOrders map[string]interface{} `json:"day_orders"` + // Filters []interface{} `json:"filters"` + // Labels []interface{} `json:"labels"` + // LiveNotifications []interface{} `json:"live_notifications"` + // LiveNotificationsLastReadID string `json:"live_notifications_last_read_id"` + // Locations []interface{} `json:"locations"` + // Notes []interface{} `json:"notes"` + // ProjectNotes []interface{} `json:"project_notes"` + // Projects []interface{} `json:"projects"` + // Reminders []interface{} `json:"reminders"` + // Sections []interface{} `json:"sections"` + // Stats map[string]interface{} `json:"stats"` + // SettingsNotifications map[string]interface{} `json:"settings_notifications"` + // User map[string]interface{} `json:"user"` + // UserPlanLimits map[string]interface{} `json:"user_plan_limits"` + // UserSettings map[string]interface{} `json:"user_settings"` +} + +type State struct { + Items map[string]Item } type Changes struct { @@ -23,6 +54,54 @@ type Changes struct { // int - the number of changes synchronized. // *Changes - a pointer to a Changes struct containing the details of the changes. // error - an error object if an error occurred during synchronization, otherwise nil. -func (sc *SyncClient) sync(full bool) (int, *Changes, error) { - return 0, nil, nil +func (sc *SyncClient) Synchronize(full bool) (*Changes, error) { + if sc.RequireFullSync { + sc.syncToken = "*" + } + + body := map[string]interface{}{ + "sync_token": sc.syncToken, + } + + jsonBody, err := json.Marshal(body) + if err != nil { + return 0, nil, err + } + + res, err := sc.post("/sync", nil, jsonBody) + if err != nil { + return nil, err + } + + defer res.Body.Close() + var syncResponse SyncResponse + if err := json.NewDecoder(res.Body).Decode(&syncResponse); err != nil { + return nil, err + } + + sc.syncToken = syncResponse.SyncToken + + // changes := &Changes{ + // Added: []string{}, + // Updated: []string{}, + // Deleted: []string{}, + // } + + // // Process the items in syncResponse.Items to populate changes + // for _, item := range syncResponse.Items { + // // Assuming item is a map[string]interface{} and has a "status" field + // itemMap := item.(map[string]interface{}) + // if status, ok := itemMap["status"].(string); ok { + // switch status { + // case "added": + // changes.Added = append(changes.Added, itemMap["id"].(string)) + // case "updated": + // changes.Updated = append(changes.Updated, itemMap["id"].(string)) + // case "deleted": + // changes.Deleted = append(changes.Deleted, itemMap["id"].(string)) + // } + // } + // } + + return changes, nil } diff --git a/internal/api/types.go b/internal/api/types.go index 8b9c909..11d35dc 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -6,6 +6,7 @@ import ( "net/url" "runtime" "runtime/debug" + "strings" "time" ) @@ -33,7 +34,7 @@ func init() { // and the types of resources to be synchronized. type SyncClient struct { Http *http.Client - SyncToken string + syncToken string ApiToken string // LastSync is the timestamp of the last synchronization, full or incremental. LastSync time.Time @@ -48,7 +49,7 @@ func NewSyncClient(apiToken string) *SyncClient { return &SyncClient{ Http: &http.Client{}, ApiToken: apiToken, - SyncToken: "*", + syncToken: "*", } } @@ -70,18 +71,44 @@ func (sc *SyncClient) UseResources(resourceTypes ...ResourceType) { } } +// headers applies common headers to the given request. +func (sc *SyncClient) headers(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+sc.ApiToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", userAgent) +} + // get performs a GET request to the Todoist API, building a request with the given path and parameters. // It will also apply Authorization, Content-Type, Accept, and User-Agent headers. func (sc *SyncClient) get(path string, params url.Values) (*http.Response, error) { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + req, err := http.NewRequest("GET", API_BASE_URL+path+"?"+params.Encode(), nil) if err != nil { return nil, err } - req.Header.Set("Authorization", "Bearer "+sc.ApiToken) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", userAgent) + sc.headers(req) + + return sc.Http.Do(req) +} + +// post performs a POST request to the Todoist API, building a request with the given path and parameters. +// It will also apply Authorization, Content-Type, Accept, and User-Agent headers. +func (sc *SyncClient) post(path string, params url.Values, body []byte) (*http.Response, error) { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + req, err := http.NewRequest("POST", API_BASE_URL+path+"?"+params.Encode(), strings.NewReader(string(body))) + if err != nil { + return nil, err + } + + sc.headers(req) return sc.Http.Do(req) }