Fonaria als zweite Anlage neben Starface
Die SyncEngine kennt jetzt nur noch IContactBackend. Was dahinter steckt, entscheidet das Profil — der Ablauf mit getrennten Baselines, Wiederzuordnung und Konfliktaufloesung bleibt unangetastet. In ihm steckt die Erfahrung, die man nicht zweimal machen will. FonariaApiClient spricht deren REST-API. Der Kern ist das Feld `verwaltet`, das jeder Schreibvorgang mitschickt: Fonaria haelt den Kontakt als vollstaendige vCard, dieses Sync-Modell bildet davon nur einen Ausschnitt ab. Ohne diese Liste haette ein aus Outlook gespiegelter Kontakt nach dem ersten Abgleich kein Bild, keine Kategorien und keinen Jahrestag mehr — nicht weil jemand sie geloescht haette, sondern weil wir sie nicht kannten. Zwei Verluste lagen darunter und sind gefunden worden, weil der Client gegen eine echte Anlage lief: - Die Abteilung steckt im zweiten Teil von ORG. Wer nur die Firma schickt, loescht sie mit. - Neben der geschaeftlichen Anschrift kann eine private stehen. Das Sync-Modell traegt nur eine. Beides wird jetzt aus dem gelesenen Stand unveraendert zurueckgeschrieben. Umbenannt: StarfaceConnection -> SystemConnection, StarfaceAddressBook -> RemoteAddressBook. Die JSON-Namen bleiben ueber JsonProperty erhalten, sonst verloeren bestehende Profile ihre Verbindungsdaten. SyncMapping.StarfaceId behaelt seinen Namen aus demselben Grund. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace StarfaceOutlookSync.Models
|
||||
{
|
||||
/// <summary>Welche Telefonanlage auf der anderen Seite steht.</summary>
|
||||
public enum PhoneSystem
|
||||
{
|
||||
Starface,
|
||||
Fonaria
|
||||
}
|
||||
|
||||
public enum SyncDirection
|
||||
{
|
||||
Both,
|
||||
@@ -7,8 +16,12 @@ namespace StarfaceOutlookSync.Models
|
||||
StarfaceToOutlook
|
||||
}
|
||||
|
||||
public class StarfaceConnection
|
||||
public class SystemConnection
|
||||
{
|
||||
// Voreinstellung Starface, damit Profile aus der Zeit vor Fonaria
|
||||
// unveraendert weiterlaufen: Was im JSON fehlt, ist eine Starface.
|
||||
public PhoneSystem System { get; set; } = PhoneSystem.Starface;
|
||||
|
||||
public string Host { get; set; } = "";
|
||||
public int Port { get; set; } = 443;
|
||||
public bool UseSsl { get; set; } = true;
|
||||
@@ -16,7 +29,16 @@ namespace StarfaceOutlookSync.Models
|
||||
public string Password { get; set; } = "";
|
||||
}
|
||||
|
||||
public class StarfaceAddressBook
|
||||
/// <summary>
|
||||
/// Ein Adressbuch auf der Gegenseite.
|
||||
///
|
||||
/// Die Felder heissen noch nach der Starface, weil dort ihre Bedeutung
|
||||
/// herkommt: <c>Type</c> unterscheidet zentrales, persoenliches und
|
||||
/// Tag-Adressbuch, <c>TagId</c> traegt die Kennung. Bei Fonaria steht in
|
||||
/// <c>TagId</c> schlicht die Nummer des Adressbuchs und in <c>Type</c>
|
||||
/// dessen Art (private, shared, system).
|
||||
/// </summary>
|
||||
public class RemoteAddressBook
|
||||
{
|
||||
public string Type { get; set; } = "central"; // central, user, tag
|
||||
public string UserId { get; set; } = "";
|
||||
@@ -30,8 +52,13 @@ namespace StarfaceOutlookSync.Models
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public StarfaceConnection StarfaceConnection { get; set; } = new StarfaceConnection();
|
||||
public StarfaceAddressBook StarfaceAddressBook { get; set; } = new StarfaceAddressBook();
|
||||
// Die JSON-Namen bleiben, wie sie waren — sonst verloeren alle
|
||||
// bestehenden Profile beim ersten Start ihre Verbindungsdaten.
|
||||
[JsonProperty("StarfaceConnection")]
|
||||
public SystemConnection Connection { get; set; } = new SystemConnection();
|
||||
|
||||
[JsonProperty("StarfaceAddressBook")]
|
||||
public RemoteAddressBook AddressBook { get; set; } = new RemoteAddressBook();
|
||||
public string OutlookFolderPath { get; set; } = "";
|
||||
public string OutlookFolderName { get; set; } = "Kontakte";
|
||||
public SyncDirection SyncDirection { get; set; } = SyncDirection.Both;
|
||||
@@ -44,6 +71,10 @@ namespace StarfaceOutlookSync.Models
|
||||
{
|
||||
public string ProfileId { get; set; } = "";
|
||||
public string OutlookEntryId { get; set; } = "";
|
||||
|
||||
// Die Kennung auf der Gegenseite — bei Fonaria die Kontakt-ID. Der
|
||||
// Name bleibt: Er steht in jeder bereits gespeicherten Zuordnung, und
|
||||
// ein Umbenennen wuerde sie alle entwerten.
|
||||
public string StarfaceId { get; set; } = "";
|
||||
|
||||
// Getrennte Baselines pro Seite. Outlook und Starface stellen denselben
|
||||
|
||||
@@ -2,6 +2,22 @@ namespace StarfaceOutlookSync.Models
|
||||
{
|
||||
public class UnifiedContact
|
||||
{
|
||||
/// <summary>
|
||||
/// Welche vCard-Eigenschaften dieses Modell abbildet.
|
||||
///
|
||||
/// Fonaria fuehrt den Kontakt im vollen vCard-Umfang — Kategorien,
|
||||
/// Jahrestag, Spitzname, Bild, zweite Anschrift. Hier steht davon nur
|
||||
/// ein Ausschnitt, weil die Starface nicht mehr kann. Beim Schreiben
|
||||
/// nach Fonaria wird diese Liste mitgeschickt: Sie sagt der Anlage,
|
||||
/// wofuer wir zustaendig sind. Alles andere bleibt dort unangetastet,
|
||||
/// statt geloescht zu werden, nur weil wir es nicht kennen.
|
||||
/// </summary>
|
||||
public static readonly string[] VcardFelder =
|
||||
{
|
||||
"FN", "N", "ORG", "TITLE", "TEL", "EMAIL", "ADR", "URL", "NOTE", "BDAY"
|
||||
};
|
||||
|
||||
|
||||
public string OutlookEntryId { get; set; } = "";
|
||||
public string StarfaceId { get; set; } = "";
|
||||
public string FirstName { get; set; } = "";
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StarfaceOutlookSync.Models;
|
||||
|
||||
namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Die Gegenseite Fonaria.
|
||||
///
|
||||
/// Anders als die Starface haelt Fonaria jeden Kontakt als vollstaendige
|
||||
/// vCard vor — mit Bild, Kategorien, Jahrestag, mehreren Anschriften. Das
|
||||
/// Sync-Modell dieser Anwendung bildet davon nur einen Ausschnitt ab.
|
||||
/// Deshalb schickt jeder Schreibvorgang <c>verwaltet</c> mit: die Liste der
|
||||
/// Eigenschaften, fuer die wir uns zustaendig erklaeren. Die Anlage
|
||||
/// schreibt genau diese in die vorhandene Karte und laesst den Rest stehen.
|
||||
///
|
||||
/// Ohne das waere der erste Abgleich verheerend: Ein aus Outlook
|
||||
/// gespiegelter Kontakt haette hinterher kein Bild und keine Kategorien
|
||||
/// mehr — nicht weil jemand sie geloescht haette, sondern weil wir sie
|
||||
/// nicht kannten.
|
||||
/// </summary>
|
||||
public class FonariaApiClient : IContactBackend
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly SystemConnection _connection;
|
||||
private readonly string _baseUrl;
|
||||
private string _token;
|
||||
|
||||
public event Action<string> OnDebug;
|
||||
|
||||
public string SystemName => "Fonaria";
|
||||
|
||||
public IReadOnlyList<string> VerwalteteFelder => UnifiedContact.VcardFelder;
|
||||
|
||||
public FonariaApiClient(SystemConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
|
||||
var handler = new HttpClientHandler();
|
||||
// Wie bei der Starface: Anlagen im eigenen Netz tragen oft ein
|
||||
// selbst ausgestelltes Zertifikat.
|
||||
handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
|
||||
|
||||
_http = new HttpClient(handler);
|
||||
_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}/api/v1";
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Anmeldung
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
public async Task<bool> LoginAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var body = new { username = _connection.LoginId, password = _connection.Password };
|
||||
var content = new StringContent(
|
||||
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||
|
||||
var resp = await _http.PostAsync($"{_baseUrl}/auth/login", content);
|
||||
var respBody = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
OnDebug?.Invoke($"Fonaria-Anmeldung fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_token = JObject.Parse(respBody)["access_token"]?.ToString();
|
||||
if (string.IsNullOrEmpty(_token)) return false;
|
||||
|
||||
_http.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", _token);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnDebug?.Invoke($"Fonaria-Anmeldung fehlgeschlagen: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task LogoutAsync()
|
||||
{
|
||||
// Fonaria-Token laufen von selbst ab; es gibt nichts abzumelden.
|
||||
_token = null;
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Lesen
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
public async Task<List<RemoteAddressBook>> GetAddressBooksAsync()
|
||||
{
|
||||
var buecher = new List<RemoteAddressBook>();
|
||||
var resp = await _http.GetAsync($"{_baseUrl}/addressbooks");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
OnDebug?.Invoke($"Adressbuecher nicht abrufbar: {(int)resp.StatusCode}");
|
||||
return buecher;
|
||||
}
|
||||
|
||||
foreach (var eintrag in JArray.Parse(await resp.Content.ReadAsStringAsync()))
|
||||
{
|
||||
var art = eintrag["kind"]?.ToString() ?? "shared";
|
||||
// Das Benutzerverzeichnis pflegt die Anlage selbst und weist
|
||||
// jeden Schreibversuch ab. Als Sync-Ziel waere es eine Falle.
|
||||
if (art == "system") continue;
|
||||
|
||||
buecher.Add(new RemoteAddressBook
|
||||
{
|
||||
Type = art,
|
||||
TagId = eintrag["id"]?.ToString() ?? "",
|
||||
Name = eintrag["name"]?.ToString() ?? "",
|
||||
UserId = eintrag["owner_id"]?.ToString() ?? ""
|
||||
});
|
||||
}
|
||||
return buecher;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book)
|
||||
{
|
||||
// Bewusst mit full=1: Die Kurzform der Liste traegt nur Name, Firma
|
||||
// und Nummern. Wer damit abgleicht, haelt jeden Kontakt fuer
|
||||
// veraendert und schreibt bei jedem Durchlauf alles neu.
|
||||
var url = $"{_baseUrl}/addressbooks/{book.TagId}/contacts?full=1&limit=5000";
|
||||
|
||||
HttpResponseMessage resp = null;
|
||||
for (var versuch = 0; versuch < 3; versuch++)
|
||||
{
|
||||
try
|
||||
{
|
||||
resp = await _http.GetAsync(url);
|
||||
if (resp.IsSuccessStatusCode) break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnDebug?.Invoke($"Kontaktabruf gescheitert (Versuch {versuch + 1}): {ex.Message}");
|
||||
}
|
||||
await Task.Delay(250 * (versuch + 1));
|
||||
}
|
||||
|
||||
// Mit einer halben Liste weiterzuarbeiten ist gefaehrlicher als
|
||||
// abzubrechen: Fehlende Kontakte gaelten als geloescht.
|
||||
if (resp == null || !resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception(
|
||||
"Die Kontakte aus Fonaria konnten nicht geladen werden. " +
|
||||
"Synchronisation abgebrochen, um Dubletten und Loeschungen zu vermeiden.");
|
||||
}
|
||||
|
||||
var kontakte = new List<UnifiedContact>();
|
||||
foreach (var eintrag in JArray.Parse(await resp.Content.ReadAsStringAsync()))
|
||||
kontakte.Add(AusFonaria(eintrag));
|
||||
|
||||
OnDebug?.Invoke($"{kontakte.Count} Kontakte aus Fonaria-Adressbuch {book.Name} geladen");
|
||||
return kontakte;
|
||||
}
|
||||
|
||||
public async Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await _http.GetAsync(
|
||||
$"{_baseUrl}/addressbooks/{book.TagId}/contacts/{contactId}");
|
||||
if (resp.StatusCode == HttpStatusCode.NotFound) return null;
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
return AusFonaria(JObject.Parse(await resp.Content.ReadAsStringAsync()));
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Schreiben
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book)
|
||||
{
|
||||
var body = NachFonaria(contact, null).ToString();
|
||||
OnDebug?.Invoke($"POST /addressbooks/{book.TagId}/contacts");
|
||||
|
||||
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
var resp = await _http.PostAsync($"{_baseUrl}/addressbooks/{book.TagId}/contacts", content);
|
||||
var respBody = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
OnDebug?.Invoke($"Anlegen fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||
return null;
|
||||
}
|
||||
return AusFonaria(JObject.Parse(respBody));
|
||||
}
|
||||
|
||||
public async Task<UnifiedContact> UpdateContactAsync(
|
||||
string contactId, UnifiedContact contact, RemoteAddressBook book)
|
||||
{
|
||||
// Den bekannten Stand mitgeben, damit nichts verlorengeht, was
|
||||
// innerhalb einer verwalteten Eigenschaft liegt, aber im
|
||||
// Sync-Modell nicht vorkommt.
|
||||
JToken roh;
|
||||
if (!_rohdaten.TryGetValue(contactId, out roh))
|
||||
{
|
||||
var vorher = await GetContactAsync(contactId, book);
|
||||
_rohdaten.TryGetValue(contactId, out roh);
|
||||
}
|
||||
|
||||
var body = NachFonaria(contact, roh).ToString();
|
||||
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
var resp = await _http.PutAsync(
|
||||
$"{_baseUrl}/addressbooks/{book.TagId}/contacts/{contactId}", content);
|
||||
var respBody = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
OnDebug?.Invoke($"Aendern von {contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Der Stand *nach* dem Schreiben ist massgeblich fuer die Baseline —
|
||||
// sonst gilt der Kontakt beim naechsten Lauf erneut als geaendert.
|
||||
return AusFonaria(JObject.Parse(respBody));
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteContactAsync(string contactId)
|
||||
{
|
||||
// Ohne Adressbuch im Pfad geht es bei Fonaria nicht. Die Engine
|
||||
// ruft nur mit Kontakten des Profil-Buches, deshalb reicht das
|
||||
// zuletzt benutzte.
|
||||
if (_letztesBuch == null) return false;
|
||||
var resp = await _http.DeleteAsync(
|
||||
$"{_baseUrl}/addressbooks/{_letztesBuch}/contacts/{contactId}");
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
private string _letztesBuch;
|
||||
|
||||
/// <summary>
|
||||
/// Die Rohdaten der zuletzt gelesenen Kontakte, nach Kennung.
|
||||
///
|
||||
/// Sie werden beim Schreiben gebraucht: Zwei Dinge liegen innerhalb
|
||||
/// einer Eigenschaft, die wir verwalten, gehoeren aber trotzdem nicht
|
||||
/// uns — die Abteilung steckt im zweiten Teil von ORG, und neben der
|
||||
/// geschaeftlichen Anschrift kann eine private stehen. Beides kennt das
|
||||
/// Sync-Modell nicht. Wer es nicht zurueckschreibt, loescht es.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, JToken> _rohdaten = new Dictionary<string, JToken>();
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Umwandlung
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
private UnifiedContact AusFonaria(JToken item)
|
||||
{
|
||||
_letztesBuch = item["address_book_id"]?.ToString() ?? _letztesBuch;
|
||||
var kennung = item["id"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(kennung)) _rohdaten[kennung] = item;
|
||||
|
||||
var kontakt = new UnifiedContact
|
||||
{
|
||||
StarfaceId = item["id"]?.ToString() ?? "",
|
||||
FirstName = item["first_name"]?.ToString() ?? "",
|
||||
LastName = item["last_name"]?.ToString() ?? "",
|
||||
Company = item["organization"]?.ToString() ?? "",
|
||||
JobTitle = item["title"]?.ToString() ?? "",
|
||||
Salutation = item["prefix"]?.ToString() ?? "",
|
||||
Notes = item["notes"]?.ToString() ?? "",
|
||||
Birthday = item["birthday"]?.ToString() ?? ""
|
||||
};
|
||||
|
||||
foreach (var mail in Eintraege(item["emails"]))
|
||||
{
|
||||
if (string.IsNullOrEmpty(kontakt.Email)) kontakt.Email = mail.Item1;
|
||||
else if (string.IsNullOrEmpty(kontakt.EmailSecondary)) kontakt.EmailSecondary = mail.Item1;
|
||||
}
|
||||
// Die schlanke Liste fuehrt nur ein einzelnes Feld.
|
||||
if (string.IsNullOrEmpty(kontakt.Email))
|
||||
kontakt.Email = item["email"]?.ToString() ?? "";
|
||||
|
||||
foreach (var nummer in Nummern(item["numbers"]))
|
||||
{
|
||||
switch (nummer.Item2)
|
||||
{
|
||||
case "mobil": if (string.IsNullOrEmpty(kontakt.PhoneMobile)) kontakt.PhoneMobile = nummer.Item1; break;
|
||||
case "home": if (string.IsNullOrEmpty(kontakt.PhoneHome)) kontakt.PhoneHome = nummer.Item1; break;
|
||||
case "fax": if (string.IsNullOrEmpty(kontakt.Fax)) kontakt.Fax = nummer.Item1; break;
|
||||
default: if (string.IsNullOrEmpty(kontakt.PhoneWork)) kontakt.PhoneWork = nummer.Item1; break;
|
||||
}
|
||||
}
|
||||
|
||||
var webadresse = Eintraege(item["urls"]).FirstOrDefault();
|
||||
if (webadresse != null) kontakt.Website = webadresse.Item1;
|
||||
|
||||
// Von mehreren Anschriften traegt das Modell nur eine. Die
|
||||
// geschaeftliche hat Vorrang — das ist die, um die es im
|
||||
// Telefonbuch einer Anlage geht.
|
||||
var anschriften = item["addresses"] as JArray;
|
||||
if (anschriften != null && anschriften.Count > 0)
|
||||
{
|
||||
var gewaehlt = anschriften.FirstOrDefault(a => a["art"]?.ToString() == "work")
|
||||
?? anschriften[0];
|
||||
kontakt.Street = gewaehlt["strasse"]?.ToString() ?? "";
|
||||
kontakt.City = gewaehlt["ort"]?.ToString() ?? "";
|
||||
kontakt.PostalCode = gewaehlt["plz"]?.ToString() ?? "";
|
||||
kontakt.State = gewaehlt["region"]?.ToString() ?? "";
|
||||
kontakt.Country = gewaehlt["land"]?.ToString() ?? "";
|
||||
}
|
||||
|
||||
return kontakt;
|
||||
}
|
||||
|
||||
private static List<Tuple<string, string>> Eintraege(JToken liste)
|
||||
{
|
||||
var raus = new List<Tuple<string, string>>();
|
||||
if (!(liste is JArray felder)) return raus;
|
||||
foreach (var f in felder)
|
||||
{
|
||||
var wert = f["value"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrEmpty(wert))
|
||||
raus.Add(Tuple.Create(wert, f["kind"]?.ToString() ?? ""));
|
||||
}
|
||||
return raus;
|
||||
}
|
||||
|
||||
private static List<Tuple<string, string>> Nummern(JToken liste)
|
||||
{
|
||||
var raus = new List<Tuple<string, string>>();
|
||||
if (!(liste is JArray felder)) return raus;
|
||||
foreach (var f in felder)
|
||||
{
|
||||
// Die Kurzform der Liste nennt das Feld "number", die
|
||||
// Vollansicht "value".
|
||||
var wert = f["number"]?.ToString() ?? f["value"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrEmpty(wert))
|
||||
raus.Add(Tuple.Create(wert, f["kind"]?.ToString() ?? "work"));
|
||||
}
|
||||
return raus;
|
||||
}
|
||||
|
||||
private JObject NachFonaria(UnifiedContact contact, JToken bekannt)
|
||||
{
|
||||
var nummern = new JArray();
|
||||
void Nummer(string wert, string art)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(wert))
|
||||
nummern.Add(new JObject { ["value"] = wert, ["kind"] = art });
|
||||
}
|
||||
Nummer(contact.PhoneWork, "work");
|
||||
Nummer(contact.PhoneMobile, "mobil");
|
||||
Nummer(contact.PhoneHome, "home");
|
||||
Nummer(contact.Fax, "fax");
|
||||
|
||||
var mails = new JArray();
|
||||
if (!string.IsNullOrWhiteSpace(contact.Email))
|
||||
mails.Add(new JObject { ["value"] = contact.Email, ["kind"] = "work" });
|
||||
if (!string.IsNullOrWhiteSpace(contact.EmailSecondary))
|
||||
mails.Add(new JObject { ["value"] = contact.EmailSecondary, ["kind"] = "home" });
|
||||
|
||||
var webadressen = new JArray();
|
||||
if (!string.IsNullOrWhiteSpace(contact.Website))
|
||||
webadressen.Add(new JObject { ["value"] = contact.Website, ["kind"] = "work" });
|
||||
|
||||
// Das Sync-Modell traegt genau eine Anschrift, die geschaeftliche.
|
||||
// Alle uebrigen der Anlage werden unveraendert wieder mitgeschickt.
|
||||
var anschriften = new JArray();
|
||||
if (!string.IsNullOrWhiteSpace(contact.Street) ||
|
||||
!string.IsNullOrWhiteSpace(contact.City) ||
|
||||
!string.IsNullOrWhiteSpace(contact.PostalCode))
|
||||
{
|
||||
anschriften.Add(new JObject
|
||||
{
|
||||
["art"] = "work",
|
||||
["strasse"] = contact.Street ?? "",
|
||||
["ort"] = contact.City ?? "",
|
||||
["plz"] = contact.PostalCode ?? "",
|
||||
["region"] = contact.State ?? "",
|
||||
["land"] = contact.Country ?? ""
|
||||
});
|
||||
}
|
||||
if (bekannt?["addresses"] is JArray vorhandene)
|
||||
{
|
||||
foreach (var a in vorhandene)
|
||||
if (a["art"]?.ToString() != "work")
|
||||
anschriften.Add(a.DeepClone());
|
||||
}
|
||||
|
||||
// Dasselbe fuer die Abteilung: Sie steckt im zweiten Teil von ORG,
|
||||
// das Sync-Modell kennt nur die Firma.
|
||||
var abteilung = bekannt?["department"]?.ToString() ?? "";
|
||||
|
||||
return new JObject
|
||||
{
|
||||
// Der Kern: Nur diese Eigenschaften fasst Fonaria an.
|
||||
["verwaltet"] = new JArray(VerwalteteFelder),
|
||||
|
||||
["display_name"] = contact.DisplayName,
|
||||
["prefix"] = contact.Salutation ?? "",
|
||||
["first_name"] = contact.FirstName ?? "",
|
||||
["last_name"] = contact.LastName ?? "",
|
||||
["organization"] = contact.Company ?? "",
|
||||
["department"] = abteilung,
|
||||
["title"] = contact.JobTitle ?? "",
|
||||
["numbers"] = nummern,
|
||||
["emails"] = mails,
|
||||
["urls"] = webadressen,
|
||||
["addresses"] = anschriften,
|
||||
["birthday"] = contact.Birthday ?? "",
|
||||
["notes"] = string.IsNullOrEmpty(contact.Notes) ? null : contact.Notes
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using StarfaceOutlookSync.Models;
|
||||
|
||||
namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Die Gegenseite eines Abgleichs — eine Telefonanlage mit Adressbuechern.
|
||||
///
|
||||
/// Die SyncEngine kennt nur dieses Interface. Was dahinter steckt, eine
|
||||
/// Starface oder eine Fonaria, entscheidet allein das Profil. Ohne diese
|
||||
/// Trennung muesste die Engine fuer jede weitere Anlage angefasst werden —
|
||||
/// und in ihr steckt die ganze Erfahrung mit getrennten Baselines,
|
||||
/// Wiederzuordnung und Konfliktaufloesung, die man nicht zweimal haben will.
|
||||
/// </summary>
|
||||
public interface IContactBackend : IDisposable
|
||||
{
|
||||
/// <summary>Ausfuehrliche Meldungen fuer das Protokoll.</summary>
|
||||
event Action<string> OnDebug;
|
||||
|
||||
/// <summary>Welche Anlage ist das? Nur fuer Meldungen an den Anwender.</summary>
|
||||
string SystemName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Welche vCard-Eigenschaften diese Anlage ueberhaupt fuehrt.
|
||||
///
|
||||
/// Entscheidend beim Schreiben: Fonaria haelt den Kontakt im vollen
|
||||
/// vCard-Umfang. Ein Abgleich mit einer Starface darf deshalb nicht
|
||||
/// die ganze Karte ersetzen — die Starface kennt weder Kategorien noch
|
||||
/// Jahrestag noch Bild und wuerde sie beim ersten Durchlauf
|
||||
/// mitloeschen, nur weil sie sie nicht mitschickt. Diese Liste sagt der
|
||||
/// Gegenseite, wofuer wir uns zustaendig fuehlen.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> VerwalteteFelder { get; }
|
||||
|
||||
Task<bool> LoginAsync();
|
||||
Task LogoutAsync();
|
||||
|
||||
Task<List<RemoteAddressBook>> GetAddressBooksAsync();
|
||||
Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book);
|
||||
Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book);
|
||||
|
||||
Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book);
|
||||
Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, RemoteAddressBook book);
|
||||
Task<bool> DeleteContactAsync(string contactId);
|
||||
}
|
||||
|
||||
/// <summary>Waehlt die Umsetzung, die zum Profil passt.</summary>
|
||||
public static class ContactBackendFactory
|
||||
{
|
||||
public static IContactBackend Create(SystemConnection connection)
|
||||
{
|
||||
switch (connection.System)
|
||||
{
|
||||
case PhoneSystem.Fonaria:
|
||||
return new FonariaApiClient(connection);
|
||||
default:
|
||||
return new StarfaceApiClient(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,25 @@ using StarfaceOutlookSync.Models;
|
||||
|
||||
namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
public class StarfaceApiClient : IDisposable
|
||||
public class StarfaceApiClient : IContactBackend
|
||||
{
|
||||
public string SystemName => "Starface";
|
||||
|
||||
/// <summary>
|
||||
/// Was eine Starface an Feldern kennt — mehr gibt ihr Datenmodell nicht
|
||||
/// her. Beim Schreiben nach Fonaria zaehlt genau diese Liste: Alles
|
||||
/// andere in der dortigen vCard bleibt unangetastet, statt hier
|
||||
/// mangels Kenntnis geloescht zu werden.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> VerwalteteFelder => UnifiedContact.VcardFelder;
|
||||
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly StarfaceConnection _connection;
|
||||
private readonly SystemConnection _connection;
|
||||
private readonly string _baseUrl;
|
||||
private string _token;
|
||||
|
||||
public StarfaceApiClient(StarfaceConnection connection)
|
||||
public StarfaceApiClient(SystemConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
|
||||
@@ -112,9 +123,9 @@ namespace StarfaceOutlookSync.Services
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
public async Task<List<StarfaceAddressBook>> GetAddressBooksAsync()
|
||||
public async Task<List<RemoteAddressBook>> GetAddressBooksAsync()
|
||||
{
|
||||
var books = new List<StarfaceAddressBook>();
|
||||
var books = new List<RemoteAddressBook>();
|
||||
|
||||
// Alle Tags laden - die Starface nutzt Tags als Adressbuch-Zuordnung
|
||||
var allTags = new JArray();
|
||||
@@ -134,7 +145,7 @@ namespace StarfaceOutlookSync.Services
|
||||
// Zentrales Adressbuch (folder/all)
|
||||
var allTag = allTags.FirstOrDefault(t => t["name"]?.ToString() == "folder/all"
|
||||
|| t["alias"]?.ToString()?.Contains("folder.all") == true);
|
||||
books.Add(new StarfaceAddressBook
|
||||
books.Add(new RemoteAddressBook
|
||||
{
|
||||
Type = "central",
|
||||
TagId = allTag?["id"]?.ToString() ?? "",
|
||||
@@ -149,7 +160,7 @@ namespace StarfaceOutlookSync.Services
|
||||
(t["name"]?.ToString() == "folder/private" || t["alias"]?.ToString()?.Contains("folder.private") == true)
|
||||
&& t["owner"]?.ToString() == userId);
|
||||
|
||||
books.Add(new StarfaceAddressBook
|
||||
books.Add(new RemoteAddressBook
|
||||
{
|
||||
Type = "user",
|
||||
UserId = userId,
|
||||
@@ -165,7 +176,7 @@ namespace StarfaceOutlookSync.Services
|
||||
// folder/all und folder/private bereits oben erfasst
|
||||
if (tagName == "folder/all" || tagName == "folder/private") continue;
|
||||
|
||||
books.Add(new StarfaceAddressBook
|
||||
books.Add(new RemoteAddressBook
|
||||
{
|
||||
Type = "tag",
|
||||
TagId = tag["id"]?.ToString() ?? "",
|
||||
@@ -178,7 +189,7 @@ namespace StarfaceOutlookSync.Services
|
||||
|
||||
public event Action<string> OnDebug;
|
||||
|
||||
public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book)
|
||||
public async Task<List<UnifiedContact>> GetContactsAsync(RemoteAddressBook book)
|
||||
{
|
||||
var contacts = new List<UnifiedContact>();
|
||||
int page = 0;
|
||||
@@ -314,7 +325,7 @@ namespace StarfaceOutlookSync.Services
|
||||
$"Synchronisation abgebrochen, um Dubletten zu vermeiden.");
|
||||
}
|
||||
|
||||
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
|
||||
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, RemoteAddressBook book)
|
||||
{
|
||||
var sfContact = MapToStarface(contact);
|
||||
|
||||
@@ -354,7 +365,7 @@ namespace StarfaceOutlookSync.Services
|
||||
/// Hash-Baseline benoetigt, damit der Kontakt beim naechsten Sync nicht
|
||||
/// erneut faelschlich als geaendert gilt.
|
||||
/// </summary>
|
||||
public async Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book)
|
||||
public async Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, RemoteAddressBook book)
|
||||
{
|
||||
var sfContact = MapToStarface(contact);
|
||||
sfContact["id"] = contactId;
|
||||
@@ -394,11 +405,16 @@ namespace StarfaceOutlookSync.Services
|
||||
}
|
||||
catch { }
|
||||
|
||||
return await GetContactAsync(contactId) ?? contact;
|
||||
return await GetContactAsync(contactId, book) ?? contact;
|
||||
}
|
||||
|
||||
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.</summary>
|
||||
public async Task<UnifiedContact> GetContactAsync(string contactId)
|
||||
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.
|
||||
///
|
||||
/// Das Adressbuch spielt hier keine Rolle: Die Starface findet einen
|
||||
/// Kontakt ueber alle Buecher hinweg. Genau darauf beruht die Regel,
|
||||
/// dass ein nicht in der Liste gefundener Kontakt nur verschoben und
|
||||
/// nicht geloescht ist.</summary>
|
||||
public async Task<UnifiedContact> GetContactAsync(string contactId, RemoteAddressBook book = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace StarfaceOutlookSync.Services
|
||||
try
|
||||
{
|
||||
Log("Verbinde mit Starface...");
|
||||
using (var starface = new StarfaceApiClient(profile.StarfaceConnection))
|
||||
using (var starface = ContactBackendFactory.Create(profile.Connection))
|
||||
{
|
||||
starface.OnDebug += (msg) => Log(msg);
|
||||
var loginOk = await starface.LoginAsync();
|
||||
@@ -127,7 +127,7 @@ namespace StarfaceOutlookSync.Services
|
||||
Log($"{outlookContacts.Count} Outlook-Kontakte geladen");
|
||||
|
||||
Log("Lade Starface-Kontakte...");
|
||||
var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook);
|
||||
var starfaceContacts = await starface.GetContactsAsync(profile.AddressBook);
|
||||
Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
|
||||
|
||||
// Bestehende Mappings laden
|
||||
@@ -236,7 +236,7 @@ namespace StarfaceOutlookSync.Services
|
||||
// (sonst Dublette).
|
||||
// (b) per ID 404 -> in Starface WIRKLICH geloescht.
|
||||
bool stillExists = !string.IsNullOrEmpty(mapping.StarfaceId)
|
||||
&& await starface.GetContactAsync(mapping.StarfaceId) != null;
|
||||
&& await starface.GetContactAsync(mapping.StarfaceId, profile.AddressBook) != null;
|
||||
|
||||
if (stillExists)
|
||||
{
|
||||
@@ -324,7 +324,7 @@ namespace StarfaceOutlookSync.Services
|
||||
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
|
||||
{
|
||||
// Outlook hat sich geaendert -> Starface updaten
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.AddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
SetBaseline(mapping, oc, updated);
|
||||
@@ -355,7 +355,7 @@ namespace StarfaceOutlookSync.Services
|
||||
var (merged, conflicts) = ContactMerger.Merge(
|
||||
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
|
||||
|
||||
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.StarfaceAddressBook);
|
||||
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.AddressBook);
|
||||
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
|
||||
|
||||
if (updatedSf != null || updatedOl != null)
|
||||
@@ -382,7 +382,7 @@ namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
// Fallback ohne Snapshot bzw. OutlookToStarface:
|
||||
// Outlook gewinnt komplett.
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.AddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
SetBaseline(mapping, oc, updated);
|
||||
@@ -432,7 +432,7 @@ namespace StarfaceOutlookSync.Services
|
||||
if (match != null)
|
||||
{
|
||||
// Existiert schon -> verknuepfen und updaten
|
||||
var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.AddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
newMappings.Add(new SyncMapping
|
||||
@@ -455,7 +455,7 @@ namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
// Neu -> in Starface erstellen
|
||||
Log($" Erstelle in Starface: {oc.DisplayName}");
|
||||
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
|
||||
var created = await starface.CreateContactAsync(oc, profile.AddressBook);
|
||||
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
|
||||
{
|
||||
newMappings.Add(new SyncMapping
|
||||
|
||||
@@ -241,7 +241,7 @@ namespace StarfaceOutlookSync.UI
|
||||
var item = new ListViewItem(new[]
|
||||
{
|
||||
p.Name,
|
||||
$"{p.StarfaceConnection.Host} / {p.StarfaceAddressBook.Name}",
|
||||
$"{p.Connection.Host} / {p.AddressBook.Name}",
|
||||
p.OutlookFolderName,
|
||||
dirText,
|
||||
lastSync
|
||||
|
||||
@@ -24,10 +24,30 @@ namespace StarfaceOutlookSync.UI
|
||||
private Label _lblTestResult;
|
||||
private Label _lblDirectionHint;
|
||||
|
||||
private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>();
|
||||
private List<RemoteAddressBook> _addressBooks = new List<RemoteAddressBook>();
|
||||
private List<string> _outlookFolderPaths = new List<string>();
|
||||
private string _selectedOutlookPath = "";
|
||||
private string _selectedOutlookName = "";
|
||||
private ComboBox _cmbSystem;
|
||||
private Label _lblAnlage;
|
||||
private Label _lblLoginId;
|
||||
private Label _lblAddressBook;
|
||||
|
||||
private string AnlagenName => GewaehlteAnlage == PhoneSystem.Fonaria ? "Fonaria" : "Starface";
|
||||
|
||||
private PhoneSystem GewaehlteAnlage =>
|
||||
_cmbSystem != null && _cmbSystem.SelectedIndex == 1 ? PhoneSystem.Fonaria : PhoneSystem.Starface;
|
||||
|
||||
/// <summary>Beschriftungen, die je nach Anlage anders heissen.</summary>
|
||||
private void UpdateSystemLabels()
|
||||
{
|
||||
var fonaria = GewaehlteAnlage == PhoneSystem.Fonaria;
|
||||
if (_lblLoginId != null)
|
||||
_lblLoginId.Text = fonaria ? "Benutzername:" : "Login-ID:";
|
||||
if (_lblAddressBook != null)
|
||||
_lblAddressBook.Text = fonaria ? "Fonaria-Adressbuch:" : "Starface-Adressbuch:";
|
||||
UpdateDirectionHint();
|
||||
}
|
||||
|
||||
public ProfileEditorForm(SyncProfile profile)
|
||||
{
|
||||
@@ -54,8 +74,26 @@ namespace StarfaceOutlookSync.UI
|
||||
panel.Controls.Add(MakeLabel("Profilname:", 12, y)); y += 22;
|
||||
_txtName = new TextBox { Left = 12, Top = y, Width = 420 }; panel.Controls.Add(_txtName); y += 32;
|
||||
|
||||
// === Starface ===
|
||||
panel.Controls.Add(MakeSectionLabel("Starface-Verbindung", 12, y)); y += 26;
|
||||
// === Anlage ===
|
||||
_lblAnlage = MakeSectionLabel("Verbindung zur Telefonanlage", 12, y); y += 26;
|
||||
panel.Controls.Add(_lblAnlage);
|
||||
|
||||
panel.Controls.Add(MakeLabel("Anlage:", 12, y)); y += 22;
|
||||
_cmbSystem = new ComboBox { Left = 12, Top = y, Width = 200, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
_cmbSystem.Items.AddRange(new object[] { "Starface", "Fonaria" });
|
||||
_cmbSystem.SelectedIndex = 0;
|
||||
// Beide sprechen ihre eigene Schnittstelle; der Vorgabeport ist
|
||||
// derselbe, aber die Adressbuecher heissen anders. Beim Wechsel
|
||||
// muss die Auswahl darum neu geladen werden.
|
||||
_cmbSystem.SelectedIndexChanged += (s, e) =>
|
||||
{
|
||||
_addressBooks.Clear();
|
||||
_cmbAddressBook.Items.Clear();
|
||||
_lblTestResult.Text = "Anlage gewechselt - bitte Adressbuecher neu laden.";
|
||||
_lblTestResult.ForeColor = Color.FromArgb(150, 80, 0);
|
||||
UpdateSystemLabels();
|
||||
};
|
||||
panel.Controls.Add(_cmbSystem); y += 32;
|
||||
|
||||
panel.Controls.Add(MakeLabel("Host / IP-Adresse:", 12, y)); y += 22;
|
||||
_txtHost = new TextBox { Left = 12, Top = y, Width = 320 };
|
||||
@@ -67,7 +105,8 @@ namespace StarfaceOutlookSync.UI
|
||||
_chkSsl = new CheckBox { Text = "HTTPS verwenden", Left = 12, Top = y, Checked = true, AutoSize = true };
|
||||
panel.Controls.Add(_chkSsl); y += 28;
|
||||
|
||||
panel.Controls.Add(MakeLabel("Login-ID:", 12, y)); y += 22;
|
||||
_lblLoginId = MakeLabel("Login-ID:", 12, y);
|
||||
panel.Controls.Add(_lblLoginId); y += 22;
|
||||
_txtLoginId = new TextBox { Left = 12, Top = y, Width = 200 }; panel.Controls.Add(_txtLoginId); y += 32;
|
||||
|
||||
panel.Controls.Add(MakeLabel("Kennwort:", 12, y)); y += 22;
|
||||
@@ -84,7 +123,8 @@ namespace StarfaceOutlookSync.UI
|
||||
_lblTestResult = new Label { Left = 12, Top = y, Width = 420, Height = 20, ForeColor = Color.Gray };
|
||||
panel.Controls.Add(_lblTestResult); y += 26;
|
||||
|
||||
panel.Controls.Add(MakeLabel("Starface-Adressbuch:", 12, y)); y += 22;
|
||||
_lblAddressBook = MakeLabel("Adressbuch der Anlage:", 12, y);
|
||||
panel.Controls.Add(_lblAddressBook); y += 22;
|
||||
_cmbAddressBook = new ComboBox { Left = 12, Top = y, Width = 420, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
panel.Controls.Add(_cmbAddressBook); y += 32;
|
||||
|
||||
@@ -100,7 +140,7 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
panel.Controls.Add(MakeLabel("Sync-Richtung:", 12, y)); y += 22;
|
||||
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
_cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Starface", "Starface -> Outlook" });
|
||||
_cmbDirection.Items.AddRange(new object[] { "Bidirektional", "Outlook -> Anlage", "Anlage -> Outlook" });
|
||||
_cmbDirection.SelectedIndex = 0;
|
||||
panel.Controls.Add(_cmbDirection); y += 28;
|
||||
|
||||
@@ -138,11 +178,11 @@ namespace StarfaceOutlookSync.UI
|
||||
{
|
||||
switch (_cmbDirection.SelectedIndex)
|
||||
{
|
||||
case 1: // Outlook -> Starface
|
||||
_lblDirectionHint.Text = "Achtung: Das Starface-Adressbuch wird zur exakten Kopie von Outlook.\nKontakte, die nur in Starface existieren, werden geloescht.";
|
||||
case 1: // Outlook -> Anlage
|
||||
_lblDirectionHint.Text = $"Achtung: Das Adressbuch der {AnlagenName} wird zur exakten Kopie von Outlook.\nKontakte, die nur dort existieren, werden geloescht.";
|
||||
break;
|
||||
case 2: // Starface -> Outlook
|
||||
_lblDirectionHint.Text = "Achtung: Der Outlook-Ordner wird zur exakten Kopie von Starface.\nKontakte, die nur in Outlook existieren, werden geloescht.";
|
||||
case 2: // Anlage -> Outlook
|
||||
_lblDirectionHint.Text = $"Achtung: Der Outlook-Ordner wird zur exakten Kopie der {AnlagenName}.\nKontakte, die nur in Outlook existieren, werden geloescht.";
|
||||
break;
|
||||
default: // Bidirektional
|
||||
_lblDirectionHint.Text = "Aenderungen werden in beide Richtungen abgeglichen.";
|
||||
@@ -182,15 +222,17 @@ namespace StarfaceOutlookSync.UI
|
||||
if (_existingProfile != null)
|
||||
{
|
||||
_txtName.Text = _existingProfile.Name;
|
||||
_txtHost.Text = _existingProfile.StarfaceConnection.Host;
|
||||
_txtPort.Text = _existingProfile.StarfaceConnection.Port.ToString();
|
||||
_chkSsl.Checked = _existingProfile.StarfaceConnection.UseSsl;
|
||||
_txtLoginId.Text = _existingProfile.StarfaceConnection.LoginId;
|
||||
_txtPassword.Text = _existingProfile.StarfaceConnection.Password;
|
||||
_cmbSystem.SelectedIndex = _existingProfile.Connection.System == PhoneSystem.Fonaria ? 1 : 0;
|
||||
_txtHost.Text = _existingProfile.Connection.Host;
|
||||
_txtPort.Text = _existingProfile.Connection.Port.ToString();
|
||||
_chkSsl.Checked = _existingProfile.Connection.UseSsl;
|
||||
_txtLoginId.Text = _existingProfile.Connection.LoginId;
|
||||
_txtPassword.Text = _existingProfile.Connection.Password;
|
||||
_chkEnabled.Checked = _existingProfile.Enabled;
|
||||
_numAutoSync.Value = _existingProfile.AutoSyncIntervalMinutes;
|
||||
|
||||
_cmbDirection.SelectedIndex = (int)_existingProfile.SyncDirection;
|
||||
UpdateSystemLabels();
|
||||
|
||||
// Outlook-Ordner
|
||||
_selectedOutlookPath = _existingProfile.OutlookFolderPath;
|
||||
@@ -198,10 +240,10 @@ namespace StarfaceOutlookSync.UI
|
||||
_txtOutlookFolder.Text = _selectedOutlookPath;
|
||||
|
||||
// Adressbuch
|
||||
if (_existingProfile.StarfaceAddressBook != null)
|
||||
if (_existingProfile.AddressBook != null)
|
||||
{
|
||||
_addressBooks.Add(_existingProfile.StarfaceAddressBook);
|
||||
_cmbAddressBook.Items.Add(_existingProfile.StarfaceAddressBook.Name);
|
||||
_addressBooks.Add(_existingProfile.AddressBook);
|
||||
_cmbAddressBook.Items.Add(_existingProfile.AddressBook.Name);
|
||||
_cmbAddressBook.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -227,10 +269,11 @@ namespace StarfaceOutlookSync.UI
|
||||
}
|
||||
}
|
||||
|
||||
private StarfaceConnection GetConnection()
|
||||
private SystemConnection GetConnection()
|
||||
{
|
||||
return new StarfaceConnection
|
||||
return new SystemConnection
|
||||
{
|
||||
System = GewaehlteAnlage,
|
||||
Host = _txtHost.Text.Trim(),
|
||||
Port = int.TryParse(_txtPort.Text, out var p) ? p : 443,
|
||||
UseSsl = _chkSsl.Checked,
|
||||
@@ -247,7 +290,7 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
try
|
||||
{
|
||||
using (var client = new StarfaceApiClient(GetConnection()))
|
||||
using (var client = ContactBackendFactory.Create(GetConnection()))
|
||||
{
|
||||
var ok = await client.LoginAsync();
|
||||
if (ok)
|
||||
@@ -280,7 +323,7 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
try
|
||||
{
|
||||
using (var client = new StarfaceApiClient(GetConnection()))
|
||||
using (var client = ContactBackendFactory.Create(GetConnection()))
|
||||
{
|
||||
var ok = await client.LoginAsync();
|
||||
if (!ok)
|
||||
@@ -350,8 +393,8 @@ namespace StarfaceOutlookSync.UI
|
||||
{
|
||||
Id = _existingProfile?.Id ?? _pm.GenerateId(),
|
||||
Name = _txtName.Text.Trim(),
|
||||
StarfaceConnection = GetConnection(),
|
||||
StarfaceAddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
|
||||
Connection = GetConnection(),
|
||||
AddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
|
||||
OutlookFolderPath = _selectedOutlookPath,
|
||||
OutlookFolderName = _selectedOutlookName,
|
||||
SyncDirection = (SyncDirection)_cmbDirection.SelectedIndex,
|
||||
@@ -367,8 +410,8 @@ namespace StarfaceOutlookSync.UI
|
||||
else
|
||||
{
|
||||
// Wenn Adressbuch gewechselt wurde, Mappings zuruecksetzen
|
||||
if (_existingProfile.StarfaceAddressBook?.TagId != profile.StarfaceAddressBook?.TagId
|
||||
|| _existingProfile.StarfaceAddressBook?.Type != profile.StarfaceAddressBook?.Type)
|
||||
if (_existingProfile.AddressBook?.TagId != profile.AddressBook?.TagId
|
||||
|| _existingProfile.AddressBook?.Type != profile.AddressBook?.Type)
|
||||
{
|
||||
_pm.SaveMappings(profile.Id, new List<SyncMapping>());
|
||||
profile.LastSync = "";
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
var infoLabel = new Label
|
||||
{
|
||||
Text = $"{_profile.StarfaceConnection.Host} ({_profile.StarfaceAddressBook.Name}) <-> {_profile.OutlookFolderName}",
|
||||
Text = $"{_profile.Connection.Host} ({_profile.AddressBook.Name}) <-> {_profile.OutlookFolderName}",
|
||||
Left = 12, Top = 12, Width = 460, AutoSize = false, Height = 20
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user