package main import ( "bytes" "context" "database/sql" "encoding/json" "fmt" "net/http" "time" _ "modernc.org/sqlite" synchronizator "git.alecodes.page/alecodes/synchronizator/pkg" ) const API_TOKEN = "" type ReadwiseDocument struct { Id string `json:"id"` Url string `json:"url"` Title string `json:"title"` Location string `json:"location"` SourceUrl string `json:"source_url"` Tags map[string]ReadwiseTag `json:"tags"` } type ReadwiseTag struct { Name string `json:"name"` Type string `json:"type"` Created int `json:"created"` } type ReadwiseHighlight struct { UserBookID int `json:"user_book_id"` Title string `json:"title"` Author string `json:"author"` Source string `json:"source"` UniqueURL string `json:"unique_url"` BookTags []HighlightTag `json:"book_tags"` Category string `json:"category"` DocumentNote *string `json:"document_note"` ReadwiseURL string `json:"readwise_url"` SourceURL string `json:"source_url"` Highlights []HighlightItem `json:"highlights"` } type HighlightTag struct { Id int `json:"id"` Name string `json:"name"` } type HighlightItem struct { ID int `json:"id"` Text string `json:"text"` Location int `json:"location"` LocationType string `json:"location_type"` Note string `json:"note"` Color string `json:"color"` HighlightedAt string `json:"highlighted_at"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` ExternalID string `json:"external_id"` EndLocation *int `json:"end_location"` URL string `json:"url"` BookID int `json:"book_id"` Tags []HighlightTag `json:"tags"` IsFavorite bool `json:"is_favorite"` IsDiscard bool `json:"is_discard"` ReadwiseURL string `json:"readwise_url"` } type ReadeckBookmark struct { Id string `json:"id"` Url string `json:"url"` Title string `json:"title"` Labels []string `json:"labels"` IsArchived bool `json:"is_archived"` } func readwiseToReadeck(document *ReadwiseDocument) (*synchronizator.Node, error) { bookmark := &ReadeckBookmark{ Url: document.SourceUrl, Title: document.Title, IsArchived: document.Location == "archive", } for _, tag := range document.Tags { bookmark.Labels = append(bookmark.Labels, tag.Name) } metadata, err := json.Marshal(bookmark) if err != nil { return nil, err } node := synchronizator.NewNode(bookmark.Title, "BOOKMARK", metadata, nil) return node, nil } func createReadeckBookmark(ctx context.Context, bookmark ReadeckBookmark) (string, error) { fmt.Printf("Bookmark: %v\n", bookmark) url := "https://web-archive.alecodes.page/api/bookmarks" method := "POST" payload, err := json.Marshal(bookmark) if err != nil { return "", err } client := &http.Client{} req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) if err != nil { return "", err } req.Header.Add("accept", "application/json") req.Header.Add("content-type", "application/json") req.Header.Add("authorization", "Bearer "+API_TOKEN) res, err := client.Do(req) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode > 202 { return "", fmt.Errorf( "Request for bookmark %v failed with status code %v", bookmark.Id, res.Status, ) } if !bookmark.IsArchived { return "", nil } updatePayload := []byte(fmt.Sprintf(`{"is_archived": %t}`, bookmark.IsArchived)) url = "https://web-archive.alecodes.page/api/bookmarks/" + res.Header.Get("bookmark-id") updateReq, err := http.NewRequest("PATCH", url, bytes.NewBuffer(updatePayload)) if err != nil { return "", err } updateReq.Header.Add("accept", "application/json") updateReq.Header.Add("authorization", "Bearer "+API_TOKEN) updateReq.Header.Add("content-type", "application/json") updateRes, err := client.Do(updateReq) if err != nil { return "", nil } defer updateRes.Body.Close() if res.StatusCode > 202 { return "", fmt.Errorf( "Request for bookmark %v failed with status code %v", bookmark.Id, res.Status, ) } return "", nil } func drop_data(conn *sql.DB) error { sql := ` DELETE FROM nodes WHERE name = 'READECK'; ` _, err := conn.Exec(sql) return err } func main() { start := time.Now() defer func() { elapsed := time.Now().Sub(start) fmt.Printf("\n\nExecution time took: %s", elapsed) }() connection, err := sql.Open("sqlite", "readwise.sql") if err != nil { fmt.Println(err) return } defer connection.Close() opts := synchronizator.DefaultOptions // opts.Log_level = synchronizator.DEBUG // err = drop_data(connection) // if err != nil { // fmt.Println(err) // // return // } sync, err := synchronizator.New(connection, opts) if err != nil { fmt.Println(err) return } // err = reconciliateCollections(sync) // if err != nil { // fmt.Println(err) // return // } err = pushChanges(sync) if err != nil { fmt.Printf("ERROR: %v\n", err) return } } func pushChanges(sync *synchronizator.DB) error { readeckBookmarks, err := sync.GetCollection(3167) if err != nil { return nil } ctx, cancel := context.WithCancel(context.Background()) defer cancel() poolConfig := &synchronizator.WorkConfig{ AmountOfWorkers: 5, MaxRetries: 2, BaseRetryTime: time.Second * 30, RateLimit: synchronizator.NewRateLimiter(10, time.Minute), Timeout: time.Second * 2, } err = synchronizator.PushNodes(ctx, readeckBookmarks, createReadeckBookmark, poolConfig) if err != nil { return err } return nil } func reconciliateCollections(sync *synchronizator.DB) error { readeck, err := sync.NewPlatform("READECK", nil, nil) if err != nil { return nil } collection, err := readeck.GetDefaultCollection() if err != nil { return nil } readwiseDocuments, err := sync.GetCollection(3) if err != nil { return nil } err = synchronizator.ReconciliateCollections(readwiseDocuments, collection, readwiseToReadeck) if err != nil { return nil } return nil }