using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace TrelloIntegration
{
///
/// Trello-Karten-Client für WinForms-Anwendungen (.NET Framework 4.8, C# 7.3)
/// Erstellt und abruft Karten via Trello REST API über HttpClient.
///
public class TrelloCardClient : IDisposable
{
private readonly string _apiKey;
private readonly string _token;
private readonly HttpClient _httpClient;
///
/// Initialisiert einen neuen TrelloCardClient.
///
/// Trello API Key
/// Trello API Token mit write-Recht
public TrelloCardClient(string apiKey, string token)
{
_apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
_token = token ?? throw new ArgumentNullException(nameof(token));
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.trello.com/1/")
};
}
// ========================================================================
// BESTEHENDE METHODE: Karte erstellen
// ========================================================================
///
/// Erstellt eine neue Trello-Karte asynchron und gibt die Roh-JSON-Antwort zurück.
///
public async Task CreateCardAsync(CreateCardOptions options)
{
ValidateOptions(options);
var values = new Dictionary
{
["key"] = _apiKey,
["token"] = _token,
["idList"] = options.IdList,
["name"] = options.Name
};
if (!string.IsNullOrWhiteSpace(options.Desc))
values["desc"] = options.Desc;
if (!string.IsNullOrWhiteSpace(options.Pos))
values["pos"] = options.Pos;
if (options.IdLabels != null && options.IdLabels.Count > 0)
values["idLabels"] = string.Join(",", options.IdLabels);
if (options.DueDate.HasValue)
values["due"] = options.DueDate.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm");
if (options.StartDate.HasValue)
values["start"] = options.StartDate.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm");
var content = new FormUrlEncodedContent(values);
var response = await _httpClient.PostAsync("cards", content);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Trello API Fehler {response.StatusCode}: {responseBody}");
}
return responseBody;
}
///
/// Erstellt eine Karte und gibt das Ergebnis als TrelloCard-Objekt zurück.
///
public async Task CreateCardAndParseAsync(CreateCardOptions options)
{
var json = await CreateCardAsync(options);
return ParseTrelloCard(json);
}
// ========================================================================
// NEUE METHODE: Karten aus Liste abrufen & nach Fälligkeitsdatum filteren
// ========================================================================
///
/// Abrufen aller Karten aus einer Liste und Filterung nach Fälligkeitsdatum.
/// GET-Anfrage statt Webhook.
///
/// Die ID der Trello-Liste
/// Das gesuchte Fälligkeitsdatum (wird genau verglichen)
/// Ob geschlossene Karten eingeschlossen werden (optional, default: false)
/// Liste von TrelloCard-Objekten, die dem Fälligkeitsdatum entsprechen
public async Task> GetCardsByDueDateAsync(
string listId,
DateTime targetDueDate,
bool includeClosed = false)
{
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
// GET-Request: Alle Karten aus der Liste abrufen
var values = new Dictionary
{
["key"] = _apiKey,
["token"] = _token
};
// Optional: geschlossene Karten einschließen
if (includeClosed)
values["closed"] = "true";
var url = $"lists/{listId}/cards?key={_apiKey}&token={_token}";
// Ergänzeclosed-Parameter falls benötigt
if (includeClosed)
url += "&closed=true";
var response = await _httpClient.GetAsync(url);
var jsonResponse = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Trello API Fehler {response.StatusCode}: {jsonResponse}");
}
// JSON parsen (Array von Karten)
var cards = ParseCardsArray(jsonResponse);
// Nach Fälligkeitsdatum filtern
var filteredCards = cards.Where(c =>
c.DueDate != null &&
c.DueDate.Value.Date == targetDueDate.Date
).ToList();
return filteredCards;
}
///
/// Abrufen aller Karten aus einer Liste (ohne Fälligkeitsdatum-Filter).
///
/// Die ID der Trello-Liste
/// Ob geschlossene Karten eingeschlossen werden
/// Liste von allen TrelloCard-Objekten in der Liste
public async Task> GetCardsFromListAsync(
string listId,
bool includeClosed = false)
{
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
var url = $"lists/{listId}/cards?key={_apiKey}&token={_token}";
if (includeClosed)
url += "&closed=true";
var response = await _httpClient.GetAsync(url);
var jsonResponse = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Trello API Fehler {response.StatusCode}: {jsonResponse}");
}
return ParseCardsArray(jsonResponse);
}
///
/// JSON-Array von Karten parsen (mehrere Karten).
///
private List ParseCardsArray(string json)
{
var cards = new List();
// Einfachster Parser für JSON-Array: [{"id":"...", "name":"..."}, {...}]
// Entfernung der äußeren [ ]
json = json.Trim();
if (json.StartsWith("[") && json.EndsWith("]"))
json = json.Substring(1, json.Length - 2);
if (string.IsNullOrWhiteSpace(json))
return cards;
// Jede Karte durch {" "} trennen (einfache Division)
var cardStrings = SplitJsonArray(json);
foreach (var cardJson in cardStrings)
{
if (!string.IsNullOrWhiteSpace(cardJson))
{
cards.Add(ParseTrelloCard(cardJson));
}
}
return cards;
}
///
/// JSON-Array in einzelne Objekte teilen.
///
private List SplitJsonArray(string json)
{
var result = new List();
var current = new StringBuilder();
var inString = false;
var depth = 0;
for (int i = 0; i < json.Length; i++)
{
var ch = json[i];
if (ch == '"' && (i == 0 || json[i - 1] != '\\'))
inString = !inString;
if (!inString)
{
if (ch == '{')
{
depth++;
if (depth == 1)
current.Clear();
}
else if (ch == '}')
{
depth--;
if (depth == 0)
{
current.Append(ch);
result.Add(current.ToString());
current.Clear();
continue;
}
}
else if (ch == ',' && depth == 0)
{
continue; // Array-Trenner überspringen
}
}
if (depth >= 1)
current.Append(ch);
}
return result;
}
// ========================================================================
// BESTEHENDE HELPER-METHODEN (unverändert)
// ========================================================================
///
/// Minimaler Parser ohne System.Text.Json (für .NET Framework 4.8)
///
private static TrelloCard ParseTrelloCard(string json)
{
var card = new TrelloCard();
card.Id = ExtractJsonValue(json, "id");
card.Name = ExtractJsonValue(json, "name");
card.Desc = ExtractJsonValue(json, "desc");
card.IdList = ExtractJsonValue(json, "idList");
card.ShortUrl = ExtractJsonValue(json, "shortUrl");
card.IdBoard = ExtractJsonValue(json, "idBoard");
var due = ExtractJsonValue(json, "due");
if (!string.IsNullOrWhiteSpace(due))
{
if (DateTime.TryParse(due, out var dtDue))
card.DueDate = dtDue;
else
card.DueDate = null;
}
var start = ExtractJsonValue(json, "start");
if (!string.IsNullOrWhiteSpace(start))
{
if (DateTime.TryParse(start, out var dtStart))
card.StartDate = dtStart;
else
card.StartDate = null;
}
return card;
}
private static string ExtractJsonValue(string json, string key)
{
// Suche nach "\"key\":" auf oberster Ebene (nicht in nested Objekten)
var searchKey = "\"" + key + "\"";
var startIndex = 0;
while (startIndex < json.Length)
{
// Finde nächstes "\"key\""
var keyIndex = json.IndexOf(searchKey, startIndex, StringComparison.Ordinal);
if (keyIndex < 0)
return null;
// Prüfen ob direkt danach ":" folgt (evtl. mit Whitespace)
var afterKey = keyIndex + searchKey.Length;
// Muss innerhalb des JSON sein
if (afterKey >= json.Length)
return null;
// Whitespace überspringen
while (afterKey < json.Length && char.IsWhiteSpace(json[afterKey]))
afterKey++;
if (afterKey >= json.Length)
return null;
// Prüfen ob ":" folgt
if (json[afterKey] == ':')
{
// WICHTIG: Prüfen ob wir in einem nested Objekt sind (labels, badges, etc.)
// Wir müssen die Tiefe zählen von Anfang des JSON
var depth = CalculateDepth(json, keyIndex);
// Nur oberste Ebene (depth == 1, weil wir bei "{" beginnen)
if (depth == 1)
{
// Wert extrahieren
afterKey++; // Nach ":"
// Whitespace überspringen
while (afterKey < json.Length && char.IsWhiteSpace(json[afterKey]))
afterKey++;
if (afterKey >= json.Length)
return null;
// null prüfen
if (json[afterKey] == 'n' && json.Substring(afterKey, 4) == "null")
return null;
// String mit "
if (json[afterKey] == '"')
{
afterKey++; // Erstes " überspringen
var endIndex = afterKey;
while (endIndex < json.Length)
{
if (json[endIndex] == '"' && (endIndex == 0 || json[endIndex - 1] != '\\'))
break;
endIndex++;
}
if (endIndex >= json.Length)
return null;
return json.Substring(afterKey, endIndex - afterKey);
}
// Andere Werte
var endPos = afterKey;
while (endPos < json.Length && !IsDelimiter(json[endPos]))
endPos++;
return json.Substring(afterKey, endPos - afterKey).Trim();
}
}
// Weiter suchen (nächstes occurrences)
startIndex = keyIndex + 1;
}
return null;
}
///
/// Berechnet die Tiefe (depth) an einer bestimmten Position im JSON
/// depth = Anzahl der offenen "{" vor dieser Position
///
private static int CalculateDepth(string json, int position)
{
var depth = 0;
var inString = false;
for (int i = 0; i < position && i < json.Length; i++)
{
var ch = json[i];
if (ch == '\\' && i + 1 < json.Length)
{
i++; // Escape char überspringen
continue;
}
if (ch == '"')
inString = !inString;
if (!inString)
{
if (ch == '{')
depth++;
else if (ch == '}')
depth--;
}
}
return depth;
}
private static bool IsDelimiter(char ch)
{
return ch == ',' || ch == '}' || ch == ']' || ch == ' ' ||
ch == '\n' || ch == '\r' || ch == '\t';
}
private void ValidateOptions(CreateCardOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.Name))
throw new ArgumentException("Name ist erforderlich.", nameof(options.Name));
if (string.IsNullOrWhiteSpace(options.IdList))
throw new ArgumentException("IdList ist erforderlich.", nameof(options.IdList));
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
}