TeamsNetphoneLink/TeamsLocalAPI/Client.cs

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);
}