using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StarfaceOutlookSync.Models; namespace StarfaceOutlookSync.Services { public class StarfaceApiClient : IDisposable { private readonly HttpClient _http; private readonly StarfaceConnection _connection; private readonly string _baseUrl; private string _token; public StarfaceApiClient(StarfaceConnection connection) { _connection = connection; var handler = new HttpClientHandler(); // Self-signed Zertifikate der Starface akzeptieren handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true; _http = new HttpClient(handler); _http.DefaultRequestHeaders.Add("X-Version", "2"); _http.Timeout = TimeSpan.FromSeconds(30); var protocol = connection.UseSsl ? "https" : "http"; var portPart = (connection.UseSsl && connection.Port == 443) || (!connection.UseSsl && connection.Port == 80) ? "" : $":{connection.Port}"; _baseUrl = $"{protocol}://{connection.Host}{portPart}/rest"; } private static string Sha512(string input) { using (var sha = SHA512.Create()) { var bytes = Encoding.UTF8.GetBytes(input); var hash = sha.ComputeHash(bytes); var sb = new StringBuilder(128); foreach (var b in hash) sb.Append(b.ToString("x2")); return sb.ToString(); } } public async Task LoginAsync() { try { // Schritt 1: Nonce holen var nonceResp = await _http.GetAsync($"{_baseUrl}/login"); if (!nonceResp.IsSuccessStatusCode) return false; var nonceJson = JObject.Parse(await nonceResp.Content.ReadAsStringAsync()); var loginType = nonceJson["loginType"]?.ToString() ?? "Internal"; var nonce = nonceJson["nonce"]?.ToString() ?? ""; // Schritt 2: Secret berechnen var passwordHash = Sha512(_connection.Password); var combined = _connection.LoginId + nonce + passwordHash; var combinedHash = Sha512(combined); var secret = $"{_connection.LoginId}:{combinedHash}"; // Schritt 3: Login var loginBody = new { loginType, nonce, secret }; var content = new StringContent(JsonConvert.SerializeObject(loginBody), Encoding.UTF8, "application/json"); var loginResp = await _http.PostAsync($"{_baseUrl}/login", content); if (!loginResp.IsSuccessStatusCode) return false; var tokenJson = JObject.Parse(await loginResp.Content.ReadAsStringAsync()); _token = tokenJson["token"]?.ToString(); if (!string.IsNullOrEmpty(_token)) { _http.DefaultRequestHeaders.Remove("authToken"); _http.DefaultRequestHeaders.Add("authToken", _token); } return !string.IsNullOrEmpty(_token); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Starface login failed: {ex.Message}"); return false; } } public async Task LogoutAsync() { if (string.IsNullOrEmpty(_token)) return; try { await _http.DeleteAsync($"{_baseUrl}/login"); } catch { } _token = null; } public async Task GetCurrentUserIdAsync() { try { var resp = await _http.GetAsync($"{_baseUrl}/users/me"); if (!resp.IsSuccessStatusCode) return null; var json = JObject.Parse(await resp.Content.ReadAsStringAsync()); return json["id"]?.ToString(); } catch { return null; } } public async Task> GetAddressBooksAsync() { var books = new List(); books.Add(new StarfaceAddressBook { Type = "central", Name = "Zentrales Adressbuch" }); var userId = await GetCurrentUserIdAsync(); if (!string.IsNullOrEmpty(userId)) { books.Add(new StarfaceAddressBook { Type = "user", UserId = userId, Name = "Persoenliches Adressbuch" }); } // Tags als virtuelle Adressbuecher try { var resp = await _http.GetAsync($"{_baseUrl}/contacts/tags"); if (resp.IsSuccessStatusCode) { var tags = JArray.Parse(await resp.Content.ReadAsStringAsync()); foreach (var tag in tags) { books.Add(new StarfaceAddressBook { Type = "tag", TagId = tag["id"]?.ToString() ?? "", Name = $"Tag: {tag["name"]}" }); } } } catch { } return books; } public async Task> GetContactsAsync(StarfaceAddressBook book) { var contacts = new List(); int page = 0; const int pageSize = 200; while (true) { var query = $"page={page}&pagesize={pageSize}"; if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) query += $"&userId={book.UserId}"; if (book.Type == "tag" && !string.IsNullOrEmpty(book.TagId)) query += $"&tags={book.TagId}"; var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}"); if (!resp.IsSuccessStatusCode) break; var array = JArray.Parse(await resp.Content.ReadAsStringAsync()); if (array.Count == 0) break; foreach (var item in array) contacts.Add(MapFromStarface(item)); if (array.Count < pageSize) break; page++; } return contacts; } public async Task CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book) { var sfContact = MapToStarface(contact); var query = ""; if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) query = $"?userId={book.UserId}"; var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json"); var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content); if (!resp.IsSuccessStatusCode) return null; var created = JObject.Parse(await resp.Content.ReadAsStringAsync()); return MapFromStarface(created); } public async Task UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book) { var sfContact = MapToStarface(contact); sfContact["id"] = contactId; var query = ""; if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) query = $"?userId={book.UserId}"; var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json"); var resp = await _http.PutAsync($"{_baseUrl}/contacts/{contactId}{query}", content); return resp.IsSuccessStatusCode; } public async Task DeleteContactAsync(string contactId) { var resp = await _http.DeleteAsync($"{_baseUrl}/contacts/{contactId}"); return resp.IsSuccessStatusCode; } private UnifiedContact MapFromStarface(JToken item) { var contact = new UnifiedContact(); contact.StarfaceId = item["id"]?.ToString() ?? ""; var attrs = new Dictionary(); var blocks = item["blocks"] as JArray; if (blocks != null) { foreach (var block in blocks) { var blockAttrs = block["attributes"] as JArray; if (blockAttrs == null) continue; foreach (var attr in blockAttrs) { var key = attr["displayKey"]?.ToString() ?? ""; var val = attr["value"]?.ToString() ?? ""; if (!string.IsNullOrEmpty(val)) attrs[key] = val; } } } contact.FirstName = attrs.GetValueOrDefault("NAME", ""); contact.LastName = attrs.GetValueOrDefault("SURNAME", ""); contact.Company = attrs.GetValueOrDefault("COMPANY", ""); contact.JobTitle = attrs.GetValueOrDefault("JOB_TITLE", ""); contact.Email = attrs.GetValueOrDefault("EMAIL", ""); contact.PhoneWork = attrs.GetValueOrDefault("OFFICE_PHONE_NUMBER", ""); contact.PhoneMobile = attrs.GetValueOrDefault("MOBILE_PHONE_NUMBER", ""); contact.PhoneHome = attrs.GetValueOrDefault("PRIVATE_PHONE_NUMBER", ""); contact.Fax = attrs.GetValueOrDefault("FAX_NUMBER", ""); contact.Street = attrs.GetValueOrDefault("STREET", ""); contact.City = attrs.GetValueOrDefault("CITY", ""); contact.PostalCode = attrs.GetValueOrDefault("POSTAL_CODE", ""); contact.State = attrs.GetValueOrDefault("STATE", ""); contact.Country = attrs.GetValueOrDefault("COUNTRY", ""); contact.Website = attrs.GetValueOrDefault("URL", ""); contact.Notes = attrs.GetValueOrDefault("NOTE", ""); contact.Salutation = attrs.GetValueOrDefault("SALUTATION", ""); contact.Title = attrs.GetValueOrDefault("TITLE", ""); contact.Birthday = attrs.GetValueOrDefault("BIRTHDAY", ""); return contact; } private JObject MapToStarface(UnifiedContact contact) { var attrs = new JArray(); void AddAttr(string displayKey, string name, string value) { if (!string.IsNullOrEmpty(value)) attrs.Add(new JObject { ["displayKey"] = displayKey, ["name"] = name, ["value"] = value }); } AddAttr("NAME", "firstName", contact.FirstName); AddAttr("SURNAME", "lastName", contact.LastName); AddAttr("COMPANY", "company", contact.Company); AddAttr("JOB_TITLE", "jobTitle", contact.JobTitle); AddAttr("EMAIL", "email", contact.Email); AddAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.PhoneWork); AddAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.PhoneMobile); AddAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.PhoneHome); AddAttr("FAX_NUMBER", "fax", contact.Fax); AddAttr("STREET", "street", contact.Street); AddAttr("CITY", "city", contact.City); AddAttr("POSTAL_CODE", "postalCode", contact.PostalCode); AddAttr("STATE", "state", contact.State); AddAttr("COUNTRY", "country", contact.Country); AddAttr("URL", "website", contact.Website); AddAttr("NOTE", "notes", contact.Notes); AddAttr("SALUTATION", "salutation", contact.Salutation); AddAttr("TITLE", "title", contact.Title); AddAttr("BIRTHDAY", "birthday", contact.Birthday); return new JObject { ["id"] = contact.StarfaceId ?? "", ["blocks"] = new JArray { new JObject { ["name"] = "contact", ["resourceKey"] = "contact", ["attributes"] = attrs } } }; } public void Dispose() { _http?.Dispose(); } } internal static class DictionaryExtensions { public static TValue GetValueOrDefault(this Dictionary dict, TKey key, TValue defaultValue) { return dict.TryGetValue(key, out var value) ? value : defaultValue; } } }