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(); } } }