SwyxTray und Firefox-Plugin in das Repository aufnehmen
Windows-Tray-Anwendung (CLMgr-Anbindung an SwyxIt!) samt WebSocket-Zugang, Firefox-Erweiterung und Browser-Beispielclient.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SwyxTray.Swyx;
|
||||
|
||||
namespace SwyxTray.Web;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal static class Protocol
|
||||
{
|
||||
public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public static string Serialize<T>(T message) => JsonSerializer.Serialize(message, Json);
|
||||
}
|
||||
|
||||
/// <summary>Nachricht der Webseite an die App.</summary>
|
||||
internal sealed class ClientCommand
|
||||
{
|
||||
/// <summary>Frei waehlbar; wird in der Antwort zurueckgegeben.</summary>
|
||||
public int? Id { get; set; }
|
||||
|
||||
public string? Cmd { get; set; }
|
||||
|
||||
/// <summary>Nur bei <c>call</c>.</summary>
|
||||
public string? Number { get; set; }
|
||||
|
||||
/// <summary>1-basierte Leitung; fehlt sie, waehlt die App selbst.</summary>
|
||||
public int? Line { get; set; }
|
||||
|
||||
/// <summary>Nur bei <c>focus</c>: gesuchter Fenstertitel.</summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bei <c>focus</c> und <c>opentab</c>: die zu oeffnende Adresse.
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>Nur bei <c>tabresult</c> (Plugin-Port): Auftrag ausgefuehrt?</summary>
|
||||
public bool? Ok { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bei <c>closetab</c> (Hauptport): der zu schliessende Tab. Bei
|
||||
/// <c>tabresult</c> auf <c>open</c> (Plugin-Port): Id des neuen Tabs.
|
||||
/// </summary>
|
||||
public int? TabId { get; set; }
|
||||
|
||||
/// <summary>Nur bei <c>tabresult</c> auf <c>list</c>: die offenen Tabs.</summary>
|
||||
public List<TabInfo>? Tabs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Ein offener Tab, wie ihn das Plugin bei <c>list</c> meldet.</summary>
|
||||
internal sealed class TabInfo
|
||||
{
|
||||
public int? Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public bool? Active { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Begruessung unmittelbar nach dem Verbindungsaufbau.</summary>
|
||||
internal sealed record HelloMessage(string App, string Version, int Protocol, int Session)
|
||||
{
|
||||
public string Type => "hello";
|
||||
}
|
||||
|
||||
/// <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>.
|
||||
/// </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)
|
||||
{
|
||||
public string Type => "result";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Drei Aktionen:
|
||||
/// <c>list</c> (offene Tabs melden), <c>open</c> (neuen Tab mit <c>Url</c>
|
||||
/// oeffnen), <c>close</c> (Tab <c>TabId</c> schliessen). Das Plugin antwortet
|
||||
/// jeweils mit <c>{"cmd":"tabresult","id":…,"ok":…}</c> — bei <c>list</c>
|
||||
/// zusaetzlich mit <c>tabs</c>, bei <c>open</c> mit <c>tabId</c>. Die Id
|
||||
/// stammt hier vom Server, nicht von einer Webseite.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <c>Event</c> ist <c>incoming</c> (Leitung beginnt zu klingeln),
|
||||
/// <c>outgoing</c> (Wahl beginnt), <c>connected</c> (Gespraech steht) oder
|
||||
/// <c>ended</c> (Leitung wieder frei). Bei <c>connected</c> steht in
|
||||
/// <c>Direction</c>, ob der Ruf ein- oder ausgehend war, sofern erkennbar;
|
||||
/// bei <c>ended</c> stammen die Peer-Angaben aus dem letzten belegten Zustand.
|
||||
/// </summary>
|
||||
internal sealed record CallEventMessage(
|
||||
string Event,
|
||||
int Line,
|
||||
string Peer,
|
||||
string PeerNumber,
|
||||
string PeerName,
|
||||
string? Direction = null)
|
||||
{
|
||||
public string Type => "call";
|
||||
|
||||
public static IReadOnlyList<CallEventMessage> Diff(SwyxSnapshot previous, SwyxSnapshot current)
|
||||
{
|
||||
var events = new List<CallEventMessage>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vollstaendiger Zustand. Wird nach dem hello, bei jeder Aenderung und auf
|
||||
/// <c>status</c> gesendet — die Webseite muss also nie einen Zustand mitfuehren.
|
||||
/// </summary>
|
||||
internal sealed record SnapshotMessage(
|
||||
bool Connected,
|
||||
bool ServerUp,
|
||||
string Overall,
|
||||
string StatusText,
|
||||
string User,
|
||||
string Server,
|
||||
IReadOnlyList<LineMessage> 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());
|
||||
}
|
||||
Reference in New Issue
Block a user