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:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user