using System.Text.Json;
using System.Text.Json.Serialization;
using SwyxTray.Swyx;
namespace SwyxTray.Web;
///
/// Die ueber den WebSocket ausgetauschten Nachrichten. Alles ist UTF-8-JSON,
/// eine Nachricht je WebSocket-Frame.
///
/// Leitungen werden nach aussen 1-basiert gezaehlt ("Leitung 1"), so wie sie im
/// Kontextmenue und in SwyxIt! erscheinen — intern sind sie 0-basiert.
///
internal static class Protocol
{
public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public static string Serialize(T message) => JsonSerializer.Serialize(message, Json);
}
/// Nachricht der Webseite an die App.
internal sealed class ClientCommand
{
/// Frei waehlbar; wird in der Antwort zurueckgegeben.
public int? Id { get; set; }
public string? Cmd { get; set; }
/// Bei call und resolve.
public string? Number { get; set; }
/// Nur bei contacts: der Suchbegriff (Name oder Nummer).
public string? Query { get; set; }
/// 1-basierte Leitung; fehlt sie, waehlt die App selbst.
public int? Line { get; set; }
/// Nur bei focus: gesuchter Fenstertitel.
public string? Title { get; set; }
///
/// Bei focus und opentab: die zu oeffnende Adresse.
///
public string? Url { get; set; }
/// Nur bei tabresult (Plugin-Port): Auftrag ausgefuehrt?
public bool? Ok { get; set; }
///
/// Bei closetab (Hauptport): der zu schliessende Tab. Bei
/// tabresult auf open (Plugin-Port): Id des neuen Tabs.
///
public int? TabId { get; set; }
/// Nur bei tabresult auf list: die offenen Tabs.
public List? Tabs { get; set; }
}
/// Ein offener Tab, wie ihn das Plugin bei list meldet.
internal sealed class TabInfo
{
public int? Id { get; set; }
public string? Title { get; set; }
public string? Url { get; set; }
public bool? Active { get; set; }
}
/// Begruessung unmittelbar nach dem Verbindungsaufbau.
internal sealed record HelloMessage(string App, string Version, int Protocol, int Session)
{
public string Type => "hello";
}
/// Ein Adressdatensatz in der Antwort auf contacts und in
/// der addresses-Nachricht.
internal sealed record ContactMessage(string Name, string Number, string Description)
{
public static ContactMessage From(SwyxContact contact)
=> new(contact.Name, contact.Number, contact.Description);
}
///
/// Die zwischengespeicherten Adressdaten aus dem Swyx-Client (globales und
/// persoenliches Telefonbuch). Wird nach dem snapshot beim Verbinden,
/// auf status und nach jedem Neueinlesen mit Aenderung gesendet — die
/// Seite muss die Daten also nie selbst anfordern.
///
internal sealed record AddressesMessage(IReadOnlyList Addresses)
{
public string Type => "addresses";
public static AddressesMessage From(IEnumerable addresses)
=> new(addresses.Select(ContactMessage.From).ToList());
}
///
/// Antwort auf genau ein Kommando. Focused wird nur bei focus
/// gesetzt: true = Fenster in den Vordergrund geholt, false =
/// stattdessen den Browser mit der URL gestartet. Tabs steht nur in
/// der Antwort auf tabs, TabId nur in der auf opentab,
/// Contacts nur in der auf contacts, Name nur in der
/// auf resolve (leer, wenn der Client die Nummer nicht kennt),
/// Addresses nur in der auf addresses (der komplette Cache).
///
internal sealed record ResultMessage(
int? Id, bool Ok, string? Error = null, int? Line = null, bool? Focused = null,
int? TabId = null, IReadOnlyList? Tabs = null,
IReadOnlyList? Contacts = null, string? Name = null,
IReadOnlyList? Addresses = null)
{
public string Type => "result";
}
///
/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Drei Aktionen:
/// list (offene Tabs melden), open (neuen Tab mit Url
/// oeffnen), close (Tab TabId schliessen). Das Plugin antwortet
/// jeweils mit {"cmd":"tabresult","id":…,"ok":…} — bei list
/// zusaetzlich mit tabs, bei open mit tabId. Die Id
/// stammt hier vom Server, nicht von einer Webseite.
///
internal sealed record TabRequestMessage(int Id, string Action, string? Url = null, int? TabId = null)
{
public string Type => "tab";
}
internal sealed record LineMessage(
int Line,
string State,
int StateCode,
string StateText,
string Peer,
string PeerNumber,
string PeerName,
bool Busy,
bool Selected);
///
/// Anruf-Ereignis, abgeleitet aus dem Vergleich zweier aufeinanderfolgender
/// Zustaende. Wird zusaetzlich zum snapshot gesendet, damit eine Seite auf
/// eingehende Rufe reagieren kann, ohne selbst Zustaende zu vergleichen.
///
/// Event ist incoming (Leitung beginnt zu klingeln),
/// outgoing (Wahl beginnt), connected (Gespraech steht) oder
/// ended (Leitung wieder frei). Bei connected steht in
/// Direction, ob der Ruf ein- oder ausgehend war, sofern erkennbar;
/// bei ended stammen die Peer-Angaben aus dem letzten belegten Zustand.
///
internal sealed record CallEventMessage(
string Event,
int Line,
string Peer,
string PeerNumber,
string PeerName,
string? Direction = null)
{
public string Type => "call";
public static IReadOnlyList Diff(SwyxSnapshot previous, SwyxSnapshot current)
{
var events = new List();
var before = previous.Lines.ToDictionary(l => l.Index);
foreach (var line in current.Lines)
{
before.TryGetValue(line.Index, out var prev);
var wasRinging = prev?.State.IsRinging() ?? false;
var wasDialing = prev?.State.IsDialing() ?? false;
var wasTalking = prev is not null && (prev.State.IsActive() || prev.State.IsOnHold());
if (line.State.IsRinging() && !wasRinging)
{
events.Add(From("incoming", line));
}
else if (line.State.IsDialing() && !wasDialing && !wasRinging && !wasTalking)
{
events.Add(From("outgoing", line));
}
else if (line.State.IsActive() && !wasTalking)
{
// Halten und Zurueckholen ist kein neues Gespraech; der Wechsel
// aus Klingeln oder Wahl dagegen schon.
events.Add(From("connected", line,
wasRinging ? "incoming" : wasDialing ? "outgoing" : null));
}
}
var after = current.Lines.ToDictionary(l => l.Index);
foreach (var prev in previous.Lines.Where(l => l.IsBusy))
{
if (!after.TryGetValue(prev.Index, out var line) || !line.IsBusy)
{
events.Add(From("ended", prev));
}
}
return events;
}
private static CallEventMessage From(string evt, SwyxLineInfo line, string? direction = null)
=> new(evt, line.Index + 1, line.PeerDisplayLong, line.PeerNumber, line.PeerName, direction);
}
///
/// Vollstaendiger Zustand. Wird nach dem hello, bei jeder Aenderung und auf
/// status gesendet — die Webseite muss also nie einen Zustand mitfuehren.
///
internal sealed record SnapshotMessage(
bool Connected,
bool ServerUp,
string Overall,
string StatusText,
string User,
string Server,
IReadOnlyList Lines)
{
public string Type => "snapshot";
public static SnapshotMessage From(SwyxSnapshot snapshot) => new(
Connected: snapshot.IsConnected,
ServerUp: snapshot.IsServerUp,
Overall: snapshot.Overall.ToString(),
StatusText: snapshot.StatusText,
User: snapshot.UserName,
Server: snapshot.ServerName,
Lines: snapshot.Lines.Select(l => new LineMessage(
Line: l.Index + 1,
State: l.State.ToString(),
StateCode: (int)l.State,
StateText: l.State.ToDisplayText(),
Peer: l.PeerDisplayLong,
PeerNumber: l.PeerNumber,
PeerName: l.PeerName,
Busy: l.IsBusy,
Selected: l.IsSelected)).ToList());
}