442 lines
13 KiB
C#
442 lines
13 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Trello-Karten-Client für WinForms-Anwendungen (.NET Framework 4.8, C# 7.3)
|
|
/// Erstellt und abruft Karten via Trello REST API über HttpClient.
|
|
/// </summary>
|
|
public class TrelloCardClient : IDisposable
|
|
{
|
|
private readonly string _apiKey;
|
|
private readonly string _token;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
/// <summary>
|
|
/// Initialisiert einen neuen TrelloCardClient.
|
|
/// </summary>
|
|
/// <param name="apiKey">Trello API Key</param>
|
|
/// <param name="token">Trello API Token mit write-Recht</param>
|
|
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
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Erstellt eine neue Trello-Karte asynchron und gibt die Roh-JSON-Antwort zurück.
|
|
/// </summary>
|
|
public async Task<string> CreateCardAsync(CreateCardOptions options)
|
|
{
|
|
ValidateOptions(options);
|
|
|
|
var values = new Dictionary<string, string>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erstellt eine Karte und gibt das Ergebnis als TrelloCard-Objekt zurück.
|
|
/// </summary>
|
|
public async Task<TrelloCard> CreateCardAndParseAsync(CreateCardOptions options)
|
|
{
|
|
var json = await CreateCardAsync(options);
|
|
return ParseTrelloCard(json);
|
|
}
|
|
|
|
// ========================================================================
|
|
// NEUE METHODE: Karten aus Liste abrufen & nach Fälligkeitsdatum filteren
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Abrufen aller Karten aus einer Liste und Filterung nach Fälligkeitsdatum.
|
|
/// GET-Anfrage statt Webhook.
|
|
/// </summary>
|
|
/// <param name="listId">Die ID der Trello-Liste</param>
|
|
/// <param name="targetDueDate">Das gesuchte Fälligkeitsdatum (wird genau verglichen)</param>
|
|
/// <param name="includeClosed">Ob geschlossene Karten eingeschlossen werden (optional, default: false)</param>
|
|
/// <returns>Liste von TrelloCard-Objekten, die dem Fälligkeitsdatum entsprechen</returns>
|
|
public async Task<List<TrelloCard>> 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<string, string>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abrufen aller Karten aus einer Liste (ohne Fälligkeitsdatum-Filter).
|
|
/// </summary>
|
|
/// <param name="listId">Die ID der Trello-Liste</param>
|
|
/// <param name="includeClosed">Ob geschlossene Karten eingeschlossen werden</param>
|
|
/// <returns>Liste von allen TrelloCard-Objekten in der Liste</returns>
|
|
public async Task<List<TrelloCard>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON-Array von Karten parsen (mehrere Karten).
|
|
/// </summary>
|
|
private List<TrelloCard> ParseCardsArray(string json)
|
|
{
|
|
var cards = new List<TrelloCard>();
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON-Array in einzelne Objekte teilen.
|
|
/// </summary>
|
|
private List<string> SplitJsonArray(string json)
|
|
{
|
|
var result = new List<string>();
|
|
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)
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Minimaler Parser ohne System.Text.Json (für .NET Framework 4.8)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Berechnet die Tiefe (depth) an einer bestimmten Position im JSON
|
|
/// depth = Anzahl der offenen "{" vor dieser Position
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
} |