Adressdaten aus dem Swyx-Client; core-Modul und Firefox-Plugin ausgelagert

SwyxTray liest jetzt die Telefonbuecher (global und persoenlich) des
SwyxIt!-Clients in einen Adress-Cache, bietet Kontaktsuche ueber
FulltextSearchInContactsEx und Nummernaufloesung ueber DispResolveNumber;
alles ueber den WebSocket-Zugang abrufbar.

Das Spring-Boot-Modul core/ und das SwyxFFPlugin werden als eigene
Projekte gepflegt und aus diesem Repository entfernt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 15:10:13 +02:00
co-authored by Claude Fable 5
parent cd355c4e21
commit fcd0d62d78
48 changed files with 427 additions and 17014 deletions
+20
View File
@@ -30,6 +30,10 @@ internal sealed class CommandExecutor
public Task<SwyxSnapshot> GetSnapshotAsync(CancellationToken cancellationToken)
=> RunAsync(() => _client.Current, cancellationToken);
/// <summary>Die zwischengespeicherten Adressdaten aus dem Swyx-Client.</summary>
public Task<IReadOnlyList<SwyxContact>> GetAddressesAsync(CancellationToken cancellationToken)
=> RunAsync(() => _client.Addresses, cancellationToken);
public async Task<ResultMessage> ExecuteAsync(ClientCommand command, CancellationToken cancellationToken)
{
try
@@ -59,6 +63,22 @@ internal sealed class CommandExecutor
Line: await RunAsync(() => _client.Hold(command.Line), cancellationToken)
.ConfigureAwait(false));
case "addresses":
return new ResultMessage(command.Id, true,
Addresses: (await GetAddressesAsync(cancellationToken).ConfigureAwait(false))
.Select(ContactMessage.From).ToList());
case "contacts":
return new ResultMessage(command.Id, true,
Contacts: (await RunAsync(() => _client.SearchContacts(command.Query), cancellationToken)
.ConfigureAwait(false))
.Select(ContactMessage.From).ToList());
case "resolve":
return new ResultMessage(command.Id, true,
Name: await RunAsync(() => _client.ResolveNumber(command.Number), cancellationToken)
.ConfigureAwait(false));
case "focus":
return new ResultMessage(command.Id, true,
Focused: await FocusAsync(command, cancellationToken).ConfigureAwait(false));
+25 -21
View File
@@ -59,27 +59,6 @@ internal sealed class LocalWebSocketServer : IDisposable
public bool IsRemote => _config.AllowRemoteAccess;
/// <summary>
/// Fragt die offenen Tabs beim Firefox-Plugin ab. <c>null</c>, wenn kein
/// Plugin verbunden ist, keine Antwort kommt oder es den Auftrag ablehnt.
/// </summary>
public Task<IReadOnlyList<TabInfo>?> TryListTabsAsync(CancellationToken cancellationToken)
=> _tabChannel.TryListTabsAsync(cancellationToken);
/// <summary>
/// Oeffnet ueber das Firefox-Plugin einen neuen Tab mit der URL.
/// <c>null</c> bei fehlendem Plugin, Zeitablauf oder Ablehnung.
/// </summary>
public Task<int?> TryOpenTabAsync(string url, CancellationToken cancellationToken)
=> _tabChannel.TryOpenTabAsync(url, cancellationToken);
/// <summary>
/// Schliesst ueber das Firefox-Plugin den Tab mit dieser Id.
/// <c>true</c> nur bei bestaetigtem Erfolg.
/// </summary>
public Task<bool> TryCloseTabAsync(int tabId, CancellationToken cancellationToken)
=> _tabChannel.TryCloseTabAsync(tabId, cancellationToken);
public void Start()
{
if (!_config.Enabled)
@@ -336,6 +315,31 @@ internal sealed class LocalWebSocketServer : IDisposable
/// <summary>Verteilt nur den Zustand, ohne Anruf-Ereignisse abzuleiten.</summary>
public void Broadcast(SwyxSnapshot snapshot) => Broadcast(snapshot, snapshot);
/// <summary>
/// Verteilt die neu eingelesenen Adressdaten an alle Verbundenen — nur an
/// den Hauptport, wie die Zustandsmeldungen. Neue Verbindungen bekommen
/// den Cache ohnehin direkt nach dem snapshot zugestellt.
/// </summary>
public void BroadcastAddresses(IReadOnlyList<SwyxContact> addresses)
{
if (_sessions.IsEmpty)
{
return;
}
var message = Protocol.Serialize(AddressesMessage.From(addresses));
foreach (var session in _sessions.Values)
{
if (session.IsPlugin)
{
continue;
}
_ = session.TrySendManyAsync([message], _shutdown.Token);
}
}
/// <summary>
/// Verteilt einen Zustand samt der daraus abgeleiteten Anruf-Ereignisse an
/// alle Verbundenen — erst der snapshot, dann die Ereignisse, damit der
+33 -3
View File
@@ -29,9 +29,12 @@ internal sealed class ClientCommand
public string? Cmd { get; set; }
/// <summary>Nur bei <c>call</c>.</summary>
/// <summary>Bei <c>call</c> und <c>resolve</c>.</summary>
public string? Number { get; set; }
/// <summary>Nur bei <c>contacts</c>: der Suchbegriff (Name oder Nummer).</summary>
public string? Query { get; set; }
/// <summary>1-basierte Leitung; fehlt sie, waehlt die App selbst.</summary>
public int? Line { get; set; }
@@ -71,15 +74,42 @@ internal sealed record HelloMessage(string App, string Version, int Protocol, in
public string Type => "hello";
}
/// <summary>Ein Adressdatensatz in der Antwort auf <c>contacts</c> und in
/// der <c>addresses</c>-Nachricht.</summary>
internal sealed record ContactMessage(string Name, string Number, string Description)
{
public static ContactMessage From(SwyxContact contact)
=> new(contact.Name, contact.Number, contact.Description);
}
/// <summary>
/// Die zwischengespeicherten Adressdaten aus dem Swyx-Client (globales und
/// persoenliches Telefonbuch). Wird nach dem <c>snapshot</c> beim Verbinden,
/// auf <c>status</c> und nach jedem Neueinlesen mit Aenderung gesendet — die
/// Seite muss die Daten also nie selbst anfordern.
/// </summary>
internal sealed record AddressesMessage(IReadOnlyList<ContactMessage> Addresses)
{
public string Type => "addresses";
public static AddressesMessage From(IEnumerable<SwyxContact> addresses)
=> new(addresses.Select(ContactMessage.From).ToList());
}
/// <summary>
/// Antwort auf genau ein Kommando. <c>Focused</c> wird nur bei <c>focus</c>
/// gesetzt: <c>true</c> = Fenster in den Vordergrund geholt, <c>false</c> =
/// stattdessen den Browser mit der URL gestartet. <c>Tabs</c> steht nur in
/// der Antwort auf <c>tabs</c>, <c>TabId</c> nur in der auf <c>opentab</c>.
/// der Antwort auf <c>tabs</c>, <c>TabId</c> nur in der auf <c>opentab</c>,
/// <c>Contacts</c> nur in der auf <c>contacts</c>, <c>Name</c> nur in der
/// auf <c>resolve</c> (leer, wenn der Client die Nummer nicht kennt),
/// <c>Addresses</c> nur in der auf <c>addresses</c> (der komplette Cache).
/// </summary>
internal sealed record ResultMessage(
int? Id, bool Ok, string? Error = null, int? Line = null, bool? Focused = null,
int? TabId = null, IReadOnlyList<TabInfo>? Tabs = null)
int? TabId = null, IReadOnlyList<TabInfo>? Tabs = null,
IReadOnlyList<ContactMessage>? Contacts = null, string? Name = null,
IReadOnlyList<ContactMessage>? Addresses = null)
{
public string Type => "result";
}
+9 -1
View File
@@ -28,8 +28,16 @@ internal sealed class WebSocketConfig
/// <c>open</c> und <c>close</c> aufgeteilt.
/// Version 6: die Tab-Verwaltung steht als <c>tabs</c>, <c>opentab</c>
/// und <c>closetab</c> auch Webseiten auf dem Hauptport offen.
/// Version 7: Adressdaten aus dem Swyx-Client — <c>contacts</c>
/// (Kontaktsuche in Telefonbuechern und Kontakt-Plugins) und
/// <c>resolve</c> (Rufnummer zu Name).
/// Version 8: <c>addresses</c>-Nachricht mit den zwischengespeicherten
/// Telefonbuecher-Eintraegen — beim Verbinden, auf <c>status</c> und nach
/// jedem Neueinlesen (Start, Neuverbindung, alle 60 Minuten). Dazu das
/// Kommando <c>addresses</c>, mit dem eine Seite den Cache jederzeit
/// selbst abrufen kann.
/// </summary>
public const int ProtocolVersion = 6;
public const int ProtocolVersion = 8;
/// <summary>
/// ZUR ZEIT ABGESCHALTET: Token-, Origin- und Host-Pruefung entfallen —
+13 -2
View File
@@ -54,6 +54,7 @@ internal sealed class WebSocketSession : IDisposable
if (!IsPlugin)
{
await SendSnapshotAsync(cancellationToken).ConfigureAwait(false);
await SendAddressesAsync(cancellationToken).ConfigureAwait(false);
}
var buffer = new byte[8 * 1024];
@@ -128,11 +129,13 @@ internal sealed class WebSocketSession : IDisposable
return;
}
// 'status' beantwortet der Server mit dem vollen Zustand; die Quittung
// danach haelt den Ablauf fuer den Client einheitlich.
// 'status' beantwortet der Server mit dem vollen Zustand samt der
// zwischengespeicherten Adressdaten; die Quittung danach haelt den
// Ablauf fuer den Client einheitlich.
if (command.Cmd.Equals("status", StringComparison.OrdinalIgnoreCase))
{
await SendSnapshotAsync(cancellationToken).ConfigureAwait(false);
await SendAddressesAsync(cancellationToken).ConfigureAwait(false);
await SendAsync(Protocol.Serialize(new ResultMessage(command.Id, true)), cancellationToken)
.ConfigureAwait(false);
return;
@@ -181,6 +184,14 @@ internal sealed class WebSocketSession : IDisposable
.ConfigureAwait(false);
}
/// <summary>Stellt der Seite die zwischengespeicherten Adressdaten zu.</summary>
private async Task SendAddressesAsync(CancellationToken cancellationToken)
{
var addresses = await _executor.GetAddressesAsync(cancellationToken).ConfigureAwait(false);
await SendAsync(Protocol.Serialize(AddressesMessage.From(addresses)), cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Auf einem WebSocket darf immer nur ein Sendevorgang gleichzeitig laufen.
/// Da Zustandsmeldungen aus dem UI-Thread und Antworten aus dem