Categories
PHP Programming Todoist Zenkit

Zenkit API to Todoist CSV

I’m of two minds about posting this. The code is ugly. I’m posting it anyway because there aren’t a lot of resource for this this and was kind of a bear. I hope to save somebody else some trouble.

As is well-documented, I’m a fan of productivity tools. Todoist added boards to support a kanban format and let me tell you, I am here for it.

My productivity software journey of the past couple years goes like this: Trello and Todoist -> Asana -> Trello alone -> Zenkit -> Todoist alone. I have data living in all four places. I’m trying to move as much as I can to Todoist (it has global tags which Trello lacks, but its missing some other features I’m pretty fond of). Definitely Todoist will last forever and I won’t come running back to Trello.

Don’t forget to put in your API key.

<?php
/*
 * Create a Todoist CSV file from the Zenkit API. Resulting file requires some
 * manual work.
 *
 * @license: GPL
 * @author KJ Coop
 *
 * Zenkit is more complicated then Todoist, so we kind of have to wedge things
 * in. For example, Zenkit only has groups of labels, and you decide which ones
 * you want to use a section heads at any given moment. Consequently, when
 * Zenkit hands off its data, it's not easy to see which labels Todoist should
 * use as section heads and which it should use as labels. It's probably
 * possible to detect which labels are grouped together in the Zenkit output,
 * but I haven't put any effort into it (yet?).
 *
 * In Todoist, to add a label, you append the title with @Desired_Label. Because
 * we don't know whether we want Todoist to show any given label as a label or
 * a section head, it appends all the label-like strings as labels.
 * Consequently, if you want to keep your kanban format and of course you do or
 * what is even the point of being alive, you have to open up the finished
 * CSV file an manually manipulate it.
 *
 * @todo Things I haven't put any work into:
 *    - Descriptions - Zenkit has them whereas Todoist does not
 *    - Attachments - in Zenkit, they can be on a file. In Todoist, they're
 *      necessarily in a comment
 *    - Comments
 *    - Users
 *
 *
 *
 * Links:
 *    - https://todoist.com/help/articles/how-to-format-your-csv-file-so-you-can-import-it-into-todoist
 *    - https://base.zenkit.com/docs/api/overview/introduction
 *    - https://packagist.org/packages/idoit/zenkit-api-client
 */

require_once(__DIR__ . "/vendor/autoload.php");

use idoit\zenkit\API;

function appendToCsv($filename, $type, $content) {

    // https://todoist.com/help/articles/how-to-format-your-csv-file-so-you-can-import-it-into-todoist
    $array = [
        $type,
        $content,
        '4', // Priority
        '1', // Indent
        '',  // Author
        '',  // Responsible
        '',  // Date
        '',  // Date lang
        '',  // Timezone

    ];


    $csv_file = __DIR__.'/'.$filename.'.csv';

    // https://www.geeksforgeeks.org/how-to-convert-an-array-to-csv-file-in-php/
    $fp = fopen($csv_file, 'a');

    // Loop through file pointer and a line
    fputcsv($fp, $array);

    fclose($fp);
}



function listWorkspacesAndBoards() {
    $apiKey = '';

    $api = new API($apiKey);

    $count = 0;

    // Some of the arrays Zenkit returns are like this and we can ignore them.
    $emptyArray = [
        'text' => '',
        'searchText' => '',
        'textType' => 'plain'
    ];

    // Output data as array - workspaces and their collections/lists.
    $workspaces = $api->getWorkspaceService()->getAllWorkspacesAndLists();
    foreach ($workspaces as $key => $workspace) {
        $filename = $workspace->id;

        foreach ($workspace->lists as $list_key => $list_item) {
            $elements = $api->getElementService()
                ->getElementsInList($list_item->shortId);

            $entries = $api->getEntryService()
                ->setElementConfiguration($elements)
                ->getEntriesForListView($list_item->shortId);
            //    ->getEntry($list_item->shortId, 1);

            $listEntries = $entries->listEntries;

            $distinctTitles = [];
            $distinctWholeStrings = [];

            if (!is_array($listEntries)) {
                continue;
            }

            foreach ($listEntries as $entry) {
                if (isset($entry->displayString)) {
                    $title = '';

                    foreach ($entry->elements as $element) {
                        /*
                            Don't reset the title at each iteration of this
                            loop. We append labels in subsequent iterations.
                            Fesetting the title resets the whole task. Consider:
                            In Zenkit, you have something like the following:
                                Task 1
                                    * Label A
                                    * Label B
                                Task 2

                            The best sense of the API I can get is that it gives
                            you:
                                Task 1
                                Label A
                                Label B
                                Task 2

                            The code can see that Task 1 is a task and that the
                            labels are a different kind of string that relates
                            to the last thing it encountered. If doesn't know
                            until Task 2 comes along that it's finished with
                            Task 1. Until it sees a task, it has to keep
                            appending tasks to the last title it encountered.
                        */

                        if (isset($element['text']) && $element['text'] != '' && !strpos($element['text'], 'http')) {
                            // Remember that this is a title
                            $title = $element['text'];
                            $distinctTitles[md5($element['text'])] = $element['text'];

                        } else if (isset($element['categoriesSort']) && isset($element['categoriesSort'])) {
                            foreach ($element['categoriesSort'] as $cat) {
                                if (isset($distinctTitles[md5($title)])) {
                                    /*
                                        Add categories as labels. This is an not
                                        ideal map - Zenkit sees no distinction
                                        between labels and column heads,
                                        wbereas Todoist does. We add them all as
                                        labels if you want to change it to a
                                        section head, you must alter the
                                        resulting CSV manually :(
                                    */
                                    $distinctTitles[md5($title)] .= ' @'.str_replace(' ', '_', $cat->name);
                                }
                            }
                        } else if ($element == $emptyArray) {
                            // Silence it
                        } else if (isset($element['persons'])) {
                            // Silence it - I'm the only person who matters, obviously
                        } else if (isset($element['files']) && isset($element['filesData'])) {
                            // Not worried about files right now.
                        } else {
                            // IDK LOL OMG
                        }

                        // Remember the whole string.
                        $distinctWholeStrings[md5($title)] = $title;

                    }
                }
            }
        }
    }

    echo "Distinct Titles: \n";
    print_r($distinctTitles);

    foreach ($distinctTitles as $title) {
        appendToCsv($filename, 'task', $title);
    }
}

try {
    listWorkspacesAndBoards();
    echo "\n";
} catch (idoit\zenkit\BadResponseException $e) {
    echo 'Exception! The status was ' . $e->getCode() . ', response: ' . $e->getResponse()->getBody()->getContents();
} catch (Exception $e) {
    echo 'Exception! Something else went wrong: ' . $e->getMessage();
}