Hilfe bei Todoist Export

Hallo,

ich nutze für meine Taskverwaltung Todoist. Todoist bietet auch eine API an. Nur leider habe ich noch keinen Plan, wie ich alle Tasks über die API in eine CSV Datei bekomme.

Hier die Beschreibung zur API: http://todoist.com/API/

Ich will keinerlei Änderungen in Todoist über die API vornehmen - lediglich einen Export in eine CSV Datei.

Über diesen Link kann ich alle Tasks abfragen. Das Ergebnis ist aber etwas schwierig zu verarbeiten: ["]Link](https://api.todoist.com/API/query?token=d2518a6541e3c0e0702bab9d9d161450f72aba05&queries=[%22viewall%22)

Die restliche Aufbereitung kann ich dann selbst übernehmen. Wäre klasse, wenn sich jemand mal dies kurz anschauen könnte. Vielen Dank.

Grüße

Guten abend,

ich hatte heute ein ähnliches problem und habe es mit so einem Code mir eine Liste im Symcon erstellt. Damit kann ich meinen Mitarbeitern direkt Aufgaben Anzeigen lassen.

Du möchtest diese aber runterladen?
Könnte dieser code gehen, vielleicht hilft es dir. Einfach Token Eingeben wie beschrieben.

<?php
/*******************************************************
 * Todoist → CSV + HTML (IP-Symcon)
 * - Liest alle AKTIVEN Tasks per Todoist API v1 (nur GET)
 * - Schreibt todoist_tasks.csv + todoist_tasks.html in denselben Ordner wie dieses Skript
 * - Optional: auch erledigte Tasks (zeitlich begrenzt) anhängen
 *
 * Doku: https://developer.todoist.com/api/v1/ (Tasks, Pagination, Completed)
 *******************************************************/

/*** === Einstellungen (nur hier anpassen) =============================== ***/
$TODOIST_TOKEN              = 'HIER_DEIN_TODOIST_TOKEN_EINTRAGEN';    // ⚠️ Pflicht
$INCLUDE_COMPLETED          = false;                                   // true = erledigte Tasks zusätzlich exportieren
$COMPLETED_SINCE_ISO        = date('Y-m-d', strtotime('-90 days'));    // Zeitfenster für erledigte (max. ~3 Monate sinnvoll)
$CSV_DELIMITER              = ';';                                      // Excel-DE: meist Semikolon
$CSV_FILENAME               = __DIR__ . '/todoist_tasks.csv';
$HTML_FILENAME              = __DIR__ . '/todoist_tasks.html';
$REQUEST_LIMIT              = 200;                                      // Page-Größe für paginierte Endpunkte
$TIMEOUT_SEC                = 20;                                       // HTTP Timeout

/*** === Hilfsfunktionen ================================================== ***/
function http_get_json_paginated(string $baseUrl, string $token, array $query = [], int $timeout = 15): array {
    $all = [];
    $cursor = null;

    do {
        $params = $query;
        if ($cursor !== null) $params['cursor'] = $cursor;

        list($code, $body, $err) = http_get($baseUrl, $token, $params, $timeout);
        if ($code < 200 || $code >= 300) {
            throw new Exception("HTTP $code beim GET $baseUrl: $body");
        }
        $data = json_decode($body, true);
        if ($data === null) throw new Exception("Ungueltige JSON-Antwort von $baseUrl");

        // v1 liefert bei paginierten Endpunkten { results: [...], next_cursor: "..." }
        if (isset($data['results']) && is_array($data['results'])) {
            $all = array_merge($all, $data['results']);
            $cursor = $data['next_cursor'] ?? null;
        } elseif (is_array($data)) {
            // Falls der Endpunkt (noch) nicht paginiert antwortet
            $all = array_merge($all, $data);
            $cursor = null;
        } else {
            $cursor = null;
        }
    } while ($cursor);

    return $all;
}

function http_get(string $url, string $token, array $query = [], int $timeout = 15): array {
    if (!empty($query)) {
        $qs = http_build_query($query);
        $url .= (strpos($url, '?') === false ? '?' : '&') . $qs;
    }
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Accept: application/json'
        ],
        CURLOPT_TIMEOUT        => $timeout,
    ]);
    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);
    return [$code, $body, $err];
}

function as_string($v): string {
    if ($v === null) return '';
    if (is_bool($v)) return $v ? 'true' : 'false';
    if (is_array($v)) return implode(',', array_map('as_string', $v));
    return (string)$v;
}

function map_project_names(string $token, int $limit, int $timeout): array {
    $projects = http_get_json_paginated('https://api.todoist.com/api/v1/projects', $token, ['limit' => $limit], $timeout);
    $map = [];
    foreach ($projects as $p) {
        if (!isset($p['id'])) continue;
        $map[ (string)$p['id'] ] = $p['name'] ?? '';
    }
    return $map;
}

function enrich_and_flatten_task(array $t, array $projectNameById): array {
    // Todoist v1: priority 1..4 (4 = höchste Dringlichkeit). Im Client bedeutet P1 = höchste → clientPriority = 5 - apiPriority.
    $apiPr   = $t['priority'] ?? 1;
    $clientP = 5 - (int)$apiPr;

    $due        = $t['due']      ?? null; // Objekt oder null
    $deadline   = $t['deadline'] ?? null; // Objekt oder null

    $due_date      = is_array($due)      ? ($due['date']      ?? '') : '';
    $due_string    = is_array($due)      ? ($due['string']    ?? '') : '';
    $due_is_rec    = is_array($due)      ? ($due['is_recurring'] ?? false) : false;
    $deadline_date = is_array($deadline) ? ($deadline['date'] ?? '') : '';

    $labels = $t['labels'] ?? []; // Liste von Label-Namen in v1

    return [
        'id'               => $t['id'] ?? '',
        'content'          => $t['content'] ?? '',
        'description'      => $t['description'] ?? '',
        'project_id'       => $t['project_id'] ?? '',
        'project_name'     => $projectNameById[ (string)($t['project_id'] ?? '') ] ?? '',
        'section_id'       => $t['section_id'] ?? '',
        'parent_id'        => $t['parent_id'] ?? '',
        'priority_api'     => $apiPr,
        'priority_client'  => $clientP, // 1 = höchste (Clientsicht)
        'labels'           => implode(',', $labels),
        'due_date'         => $due_date,
        'due_text'         => $due_string,
        'due_recurring'    => $due_is_rec ? 'true' : 'false',
        'deadline_date'    => $deadline_date,
        'added_at'         => $t['added_at'] ?? '',
        'updated_at'       => $t['updated_at'] ?? '',
        'completed_at'     => $t['completed_at'] ?? '',
        'checked'          => isset($t['checked']) ? ($t['checked'] ? 'true' : 'false') : '',
        'url'              => $t['url'] ?? ''
    ];
}

function write_csv(string $filename, array $rows, string $delimiter = ';'): void {
    $fh = fopen($filename, 'wb');
    if (!$fh) throw new Exception("Kann CSV nicht schreiben: $filename");
    // UTF-8 BOM für Excel
    fwrite($fh, "\xEF\xBB\xBF");

    if (empty($rows)) {
        fclose($fh);
        return;
    }
    $headers = array_keys($rows[0]);
    fputcsv($fh, $headers, $delimiter);
    foreach ($rows as $r) {
        $line = [];
        foreach ($headers as $h) $line[] = as_string($r[$h] ?? '');
        fputcsv($fh, $line, $delimiter);
    }
    fclose($fh);
}

function write_html_table(string $filename, array $rows): void {
    $headers = !empty($rows) ? array_keys($rows[0]) : [];
    $html  = "<!doctype html>\n<html lang=\"de\">\n<head>\n<meta charset=\"utf-8\">";
    $html .= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    $html .= "<title>Todoist Tasks Export</title>";
    $html .= "<style>
        body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:24px;}
        h1{font-size:20px;margin:0 0 12px;}
        .meta{color:#666;margin:0 0 16px;}
        table{border-collapse:collapse;width:100%;font-size:14px;}
        th,td{border:1px solid #ddd;padding:8px;vertical-align:top;}
        th{background:#f3f4f6;text-align:left;position:sticky;top:0;}
        tr:nth-child(even){background:#fafafa;}
        code{background:#f6f8fa;padding:2px 4px;border-radius:4px;}
    </style>";
    $html .= "</head><body>";
    $html .= "<h1>Todoist Tasks Export</h1>";
    $html .= "<p class=\"meta\">Generiert am ".date('Y-m-d H:i:s')." – Zeilen: ".count($rows)."</p>";
    $html .= "<table><thead><tr>";
    foreach ($headers as $h) $html .= "<th>".htmlspecialchars($h, ENT_QUOTES, 'UTF-8')."</th>";
    $html .= "</tr></thead><tbody>";
    foreach ($rows as $r) {
        $html .= "<tr>";
        foreach ($headers as $h) {
            $val = isset($r[$h]) ? (string)$r[$h] : '';
            $html .= "<td>".htmlspecialchars($val, ENT_QUOTES, 'UTF-8')."</td>";
        }
        $html .= "</tr>";
    }
    $html .= "</tbody></table></body></html>";
    file_put_contents($filename, $html);
}

/*** === Ablauf =========================================================== ***/
try {
    if (empty($TODOIST_TOKEN) || $TODOIST_TOKEN === 'HIER_DEIN_TODOIST_TOKEN_EINTRAGEN') {
        throw new Exception('Bitte $TODOIST_TOKEN eintragen.');
    }

    // 1) Projektnamen holen (für schönere CSV/HTML)
    $projectNameById = map_project_names($TODOIST_TOKEN, $REQUEST_LIMIT, $TIMEOUT_SEC);

    // 2) Aktive Tasks holen (paginiert)
    $active = http_get_json_paginated(
        'https://api.todoist.com/api/v1/tasks',
        $TODOIST_TOKEN,
        ['limit' => $REQUEST_LIMIT],
        $TIMEOUT_SEC
    );

    $rows = [];
    foreach ($active as $t) {
        $rows[] = enrich_and_flatten_task($t, $projectNameById);
    }

    // 3) Optional: erledigte Tasks (nur lesend; by_completion_date ist auf ~3 Monate begrenzt)
    if ($INCLUDE_COMPLETED) {
        $completed = http_get_json_paginated(
            'https://api.todoist.com/api/v1/tasks/completed/by_completion_date',
            $TODOIST_TOKEN,
            [
                'since' => $COMPLETED_SINCE_ISO . 'T00:00:00Z',
                'limit' => $REQUEST_LIMIT
            ],
            $TIMEOUT_SEC
        );
        // completed liefert eine andere Struktur; normalisieren
        foreach ($completed as $c) {
            // Struktur laut v1: enthält Task-Felder ähnlich items + completed_at; absichern:
            $rows[] = enrich_and_flatten_task($c, $projectNameById);
        }
    }

    // 4) CSV + HTML schreiben
    write_csv($CSV_FILENAME, $rows, $CSV_DELIMITER);
    write_html_table($HTML_FILENAME, $rows);

    echo "Fertig.\nCSV:  $CSV_FILENAME\nHTML: $HTML_FILENAME\n";

} catch (Exception $e) {
    echo "Fehler: ".$e->getMessage();
}