446 lines
18 KiB
C#
446 lines
18 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using TeamsLocalLibary.EventArgs;
|
|
|
|
namespace TeamsLocalLibary;
|
|
|
|
public class Client : INotifyPropertyChanged
|
|
{
|
|
public bool IsBackgroundBlurred { get => isBackgroundBlurred; set { if (value != IsBackgroundBlurred) _ = SendCommand(value ? MeetingAction.BlurBackground : MeetingAction.UnblurBackground); } }
|
|
public bool IsHandRaised { get => isHandRaised; set { if (value != IsHandRaised) _ = SendCommand(value ? MeetingAction.RaiseHand : MeetingAction.LowerHand); } }
|
|
public bool IsMuted { get => isMuted; set { if (value != IsMuted) _ = SendCommand(value ? MeetingAction.Mute : MeetingAction.Unmute); } }
|
|
public bool IsVideoOn { get => isVideoOn; set { if (value != IsVideoOn) _ = SendCommand(value ? MeetingAction.ShowVideo : MeetingAction.HideVideo); } }
|
|
public bool IsSharing { get => isSharing; set { if (value != IsSharing) _ = value ? SendCommand(MeetingAction.ToggleUI, ClientMessageParameterType.ToggleUiSharing) : _ = SendCommand(MeetingAction.StopSharing); } }
|
|
|
|
public bool IsRecordingOn { get => isRecordingOn; }
|
|
public bool IsInMeeting { get => isInMeeting; }
|
|
|
|
public bool HasUnreadMessages { get => hasUnreadMessages; }
|
|
|
|
public bool IsConnected => ws is not null && ws.State == WebSocketState.Open;
|
|
|
|
public bool CanToggleBlur { get; private set; }
|
|
public bool CanToggleHand { get; private set; }
|
|
public bool CanToggleMute { get; private set; }
|
|
public bool CanToggleShareTray { get; private set; }
|
|
public bool CanToggleVideo { get; private set; }
|
|
|
|
public bool CanToggleChat { get; private set; }
|
|
public bool CanStopSharing { get; private set; }
|
|
|
|
public bool CanLeave { get; private set; }
|
|
public bool CanPair { get; private set; }
|
|
public bool CanReact { get; private set; }
|
|
|
|
private bool isMuted;
|
|
private bool isVideoOn;
|
|
private bool isHandRaised;
|
|
private bool isInMeeting;
|
|
private bool isRecordingOn;
|
|
private bool isBackgroundBlurred;
|
|
private bool hasUnreadMessages;
|
|
private bool isSharing;
|
|
|
|
private readonly SemaphoreSlim sendSemaphore = new(1);
|
|
private readonly SemaphoreSlim receiveSemaphore = new(1);
|
|
private readonly SemaphoreSlim connectingSemaphore = new(1);
|
|
|
|
private readonly JsonSerializerOptions jsonSerializerOptions = new()
|
|
{
|
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
};
|
|
|
|
private ClientInfo clientInfo;
|
|
|
|
private ClientWebSocket? ws;
|
|
|
|
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionState;
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
public event EventHandler<TokenReceivedEventArgs>? TokenReceived;
|
|
public event EventHandler<SuccessReceivedEventArgs>? SuccessReceived;
|
|
public event EventHandler<ErrorReceivedEventArgs>? ErrorReceived;
|
|
|
|
public Client(bool autoConnect = false, string manufacturer = "krjan02", string device = "TeamsLocalLibary", string app = "NetPhone Teams Synchronisation", string appVersion = "1.0.0", string? token = null, CancellationToken cancellationToken = default)
|
|
{
|
|
clientInfo = new() {
|
|
App = app,
|
|
AppVersion = appVersion,
|
|
Device = device,
|
|
Manufacturer = manufacturer,
|
|
Token = token
|
|
};
|
|
|
|
if (autoConnect)
|
|
_ = Connect(true, cancellationToken);
|
|
}
|
|
|
|
public async Task Disconnect(CancellationToken cancellationToken = default)
|
|
{
|
|
if (ws is null) return;
|
|
|
|
var initialState = ws.State;
|
|
|
|
switch (ws.State)
|
|
{
|
|
case WebSocketState.Open:
|
|
case WebSocketState.Connecting:
|
|
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
|
|
break;
|
|
case WebSocketState.CloseSent:
|
|
await WaitClose(cancellationToken);
|
|
break;
|
|
case WebSocketState.CloseReceived:
|
|
case WebSocketState.Aborted:
|
|
case WebSocketState.Closed:
|
|
case WebSocketState.None:
|
|
break;
|
|
}
|
|
|
|
ws.Dispose();
|
|
ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(ws.State));
|
|
ws = null;
|
|
}
|
|
|
|
public async Task Reconnect(bool waitForTeams, CancellationToken cancellationToken = default)
|
|
{
|
|
await Disconnect(cancellationToken);
|
|
await Connect(waitForTeams, cancellationToken);
|
|
}
|
|
|
|
private async Task WaitClose(CancellationToken cancellationToken = default)
|
|
{
|
|
if (ws is null) return;
|
|
|
|
while (ws is not null && ws.State != WebSocketState.Closed && ws.State != WebSocketState.Aborted)
|
|
await Task.Delay(25, cancellationToken);
|
|
}
|
|
|
|
private async Task WaitOpen(CancellationToken cancellationToken = default)
|
|
{
|
|
if (ws is null) throw new InvalidOperationException();
|
|
|
|
while (ws.State == WebSocketState.Connecting)
|
|
await Task.Delay(25, cancellationToken);
|
|
}
|
|
|
|
private async void Receive(CancellationToken cancellationToken = default)
|
|
{
|
|
if (ws is null || ws.State != WebSocketState.Open)
|
|
throw new InvalidOperationException("Not connected");
|
|
|
|
try
|
|
{
|
|
await receiveSemaphore.WaitAsync(cancellationToken);
|
|
|
|
var buffer = new byte[1024];
|
|
|
|
while (true)
|
|
{
|
|
var result = await ws.ReceiveAsync(buffer, cancellationToken);
|
|
|
|
if (result.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
if (ws.CloseStatus != WebSocketCloseStatus.NormalClosure)
|
|
{
|
|
ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(this.ws.State));
|
|
_ = Reconnect(true, cancellationToken);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!result.EndOfMessage || result.Count == 0)
|
|
throw new NotImplementedException("Invalid Message received");
|
|
|
|
var data = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
|
|
|
var message = JsonSerializer.Deserialize<ServerMessage>(data, jsonSerializerOptions) ?? throw new InvalidDataException();
|
|
|
|
if (message.Response == "Success")
|
|
SuccessReceived?.Invoke(this, new SuccessReceivedEventArgs(message.RequestId ?? 0));
|
|
|
|
if (!string.IsNullOrEmpty(message.ErrorMsg))
|
|
{
|
|
if (message.ErrorMsg.EndsWith("no active call"))
|
|
continue;
|
|
|
|
ErrorReceived?.Invoke(this, new ErrorReceivedEventArgs(message.ErrorMsg));
|
|
}
|
|
|
|
if (message.TokenRefresh is not null)
|
|
{
|
|
clientInfo.Token = message.TokenRefresh;
|
|
TokenReceived?.Invoke(this, new TokenReceivedEventArgs(message.TokenRefresh));
|
|
}
|
|
|
|
if (message.MeetingUpdate is null || message.MeetingUpdate.MeetingState is null)
|
|
continue;
|
|
|
|
if (isSharing != message.MeetingUpdate.MeetingState.IsSharing)
|
|
{
|
|
isSharing = message.MeetingUpdate.MeetingState.IsSharing;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSharing)));
|
|
}
|
|
|
|
if (isMuted != message.MeetingUpdate.MeetingState.IsMuted)
|
|
{
|
|
isMuted = message.MeetingUpdate.MeetingState.IsMuted;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMuted)));
|
|
}
|
|
|
|
if (hasUnreadMessages != message.MeetingUpdate.MeetingState.HasUnreadMessages)
|
|
{
|
|
hasUnreadMessages = message.MeetingUpdate.MeetingState.HasUnreadMessages;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasUnreadMessages)));
|
|
}
|
|
|
|
if (isVideoOn != message.MeetingUpdate.MeetingState.IsVideoOn)
|
|
{
|
|
isVideoOn = message.MeetingUpdate.MeetingState.IsVideoOn;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsVideoOn)));
|
|
}
|
|
|
|
if (isHandRaised != message.MeetingUpdate.MeetingState.IsHandRaised)
|
|
{
|
|
isHandRaised = message.MeetingUpdate.MeetingState.IsHandRaised;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHandRaised)));
|
|
}
|
|
|
|
if (isInMeeting != message.MeetingUpdate.MeetingState.IsInMeeting)
|
|
{
|
|
isInMeeting = message.MeetingUpdate.MeetingState.IsInMeeting;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsInMeeting)));
|
|
}
|
|
|
|
if (isRecordingOn != message.MeetingUpdate.MeetingState.IsRecordingOn)
|
|
{
|
|
isRecordingOn = message.MeetingUpdate.MeetingState.IsRecordingOn;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecordingOn)));
|
|
}
|
|
|
|
if (isBackgroundBlurred != message.MeetingUpdate.MeetingState.IsBackgroundBlurred)
|
|
{
|
|
isBackgroundBlurred = message.MeetingUpdate.MeetingState.IsBackgroundBlurred;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsBackgroundBlurred)));
|
|
}
|
|
|
|
if (CanToggleMute != message.MeetingUpdate.MeetingPermissions.CanToggleMute)
|
|
{
|
|
CanToggleMute = message.MeetingUpdate.MeetingPermissions.CanToggleMute;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleMute)));
|
|
}
|
|
|
|
if (CanToggleShareTray != message.MeetingUpdate.MeetingPermissions.CanToggleShareTray)
|
|
{
|
|
CanToggleShareTray = message.MeetingUpdate.MeetingPermissions.CanToggleShareTray;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleShareTray)));
|
|
}
|
|
|
|
if (CanToggleVideo != message.MeetingUpdate.MeetingPermissions.CanToggleVideo)
|
|
{
|
|
CanToggleVideo = message.MeetingUpdate.MeetingPermissions.CanToggleVideo;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleVideo)));
|
|
}
|
|
|
|
if (CanToggleHand != message.MeetingUpdate.MeetingPermissions.CanToggleHand)
|
|
{
|
|
CanToggleHand = message.MeetingUpdate.MeetingPermissions.CanToggleHand;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleHand)));
|
|
}
|
|
|
|
if (CanToggleBlur != message.MeetingUpdate.MeetingPermissions.CanToggleBlur)
|
|
{
|
|
CanToggleBlur = message.MeetingUpdate.MeetingPermissions.CanToggleBlur;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleBlur)));
|
|
}
|
|
|
|
if (CanToggleChat != message.MeetingUpdate.MeetingPermissions.CanToggleChat)
|
|
{
|
|
CanToggleChat = message.MeetingUpdate.MeetingPermissions.CanToggleChat;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanToggleChat)));
|
|
}
|
|
|
|
if (CanStopSharing != message.MeetingUpdate.MeetingPermissions.CanStopSharing)
|
|
{
|
|
CanStopSharing = message.MeetingUpdate.MeetingPermissions.CanStopSharing;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanStopSharing)));
|
|
}
|
|
|
|
if (CanLeave != message.MeetingUpdate.MeetingPermissions.CanLeave)
|
|
{
|
|
CanLeave = message.MeetingUpdate.MeetingPermissions.CanLeave;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanLeave)));
|
|
}
|
|
|
|
if (CanPair != message.MeetingUpdate.MeetingPermissions.CanPair)
|
|
{
|
|
CanPair = message.MeetingUpdate.MeetingPermissions.CanPair;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanPair)));
|
|
}
|
|
|
|
if (CanReact != message.MeetingUpdate.MeetingPermissions.CanReact)
|
|
{
|
|
CanReact = message.MeetingUpdate.MeetingPermissions.CanReact;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanReact)));
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
finally
|
|
{
|
|
receiveSemaphore.Release();
|
|
|
|
Console.WriteLine(ws == null);
|
|
|
|
if (ws is not null && ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting)
|
|
_ = Reconnect(true, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> Connect(bool waitForTeams = true, CancellationToken cancellationToken = default)
|
|
{
|
|
bool connectionSuccess = false;
|
|
|
|
try
|
|
{
|
|
await connectingSemaphore.WaitAsync(cancellationToken);
|
|
|
|
if (Process.GetProcessesByName("ms-teams").Length == 0 && !waitForTeams)
|
|
return false;
|
|
|
|
while (Process.GetProcessesByName("ms-teams").Length == 0)
|
|
await Task.Delay(1000, cancellationToken);
|
|
|
|
if (ws is not null)
|
|
{
|
|
switch (ws.State)
|
|
{
|
|
case WebSocketState.Open:
|
|
connectionSuccess = true;
|
|
break;
|
|
case WebSocketState.Connecting:
|
|
await WaitOpen(cancellationToken);
|
|
connectionSuccess = true;
|
|
break;
|
|
case WebSocketState.Aborted:
|
|
case WebSocketState.Closed:
|
|
case WebSocketState.None:
|
|
await Disconnect(cancellationToken);
|
|
break;
|
|
case WebSocketState.CloseReceived:
|
|
case WebSocketState.CloseSent:
|
|
await WaitClose(cancellationToken);
|
|
await Disconnect(cancellationToken);
|
|
break;
|
|
}
|
|
}
|
|
|
|
ws = new();
|
|
|
|
var url = clientInfo.GetServerUrl();
|
|
|
|
await ws.ConnectAsync(url, cancellationToken);
|
|
ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(ws.State));
|
|
|
|
await WaitOpen(cancellationToken);
|
|
|
|
if (ws.State != WebSocketState.Open)
|
|
throw new WebSocketException($"Invalid state after connect: ${ws.State}");
|
|
|
|
connectionSuccess = true;
|
|
ConnectionState?.Invoke(this, new ConnectionStateChangedEventArgs(this.ws.State));
|
|
Receive(cancellationToken);
|
|
await SendCommand(MeetingAction.QueryMeetingState, null, cancellationToken);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
connectionSuccess = false;
|
|
}
|
|
finally
|
|
{
|
|
connectingSemaphore.Release();
|
|
}
|
|
|
|
if (!connectionSuccess)
|
|
Connect(true, cancellationToken); //CS4014 I dont want to wait btw :)
|
|
|
|
return connectionSuccess;
|
|
}
|
|
|
|
|
|
private async Task SendCommand(MeetingAction action, ClientMessageParameterType? type = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ws is null)
|
|
{
|
|
await Connect(true, cancellationToken);
|
|
}
|
|
|
|
switch (ws!.State)
|
|
{
|
|
case WebSocketState.Aborted:
|
|
case WebSocketState.CloseReceived:
|
|
await Connect(true, cancellationToken);
|
|
break;
|
|
case WebSocketState.Connecting:
|
|
await WaitOpen(cancellationToken);
|
|
break;
|
|
case WebSocketState.CloseSent:
|
|
case WebSocketState.Closed:
|
|
throw new InvalidOperationException("Not Connected");
|
|
case WebSocketState.Open:
|
|
case WebSocketState.None:
|
|
break;
|
|
}
|
|
|
|
var message = JsonSerializer.Serialize(new ClientMessage()
|
|
{
|
|
Action = action,
|
|
Parameters = type is null ? null : new ClientMessageParameter
|
|
{
|
|
Type = type.Value
|
|
}
|
|
}, jsonSerializerOptions);
|
|
|
|
try
|
|
{
|
|
await sendSemaphore.WaitAsync(cancellationToken);
|
|
|
|
await ws.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
sendSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task LeaveCall(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.LeaveCall, null, cancellationToken);
|
|
|
|
public async Task ReactApplause(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactApplause, cancellationToken);
|
|
|
|
public async Task ReactLaugh(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLaugh, cancellationToken);
|
|
|
|
public async Task ReactLike(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLike, cancellationToken);
|
|
|
|
public async Task ReactLove(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactLove, cancellationToken);
|
|
|
|
public async Task ReactWow(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.React, ClientMessageParameterType.ReactWow, cancellationToken);
|
|
|
|
public async Task UpdateState(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.QueryMeetingState, null, cancellationToken);
|
|
|
|
public async Task ToggleChat(CancellationToken cancellationToken = default)
|
|
=> await SendCommand(MeetingAction.ToggleUI, ClientMessageParameterType.ToggleUiChat, cancellationToken);
|
|
}
|