Added TeamsLocalAPI Project
This commit is contained in:
parent
a1d7487548
commit
3272781a12
445
TeamsLocalAPI/Client.cs
Normal file
445
TeamsLocalAPI/Client.cs
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
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);
|
||||||
|
}
|
31
TeamsLocalAPI/ClientInfo.cs
Normal file
31
TeamsLocalAPI/ClientInfo.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace TeamsLocalLibary;
|
||||||
|
|
||||||
|
internal struct ClientInfo
|
||||||
|
{
|
||||||
|
public string Manufacturer { init; get; }
|
||||||
|
public string Device { init; get; }
|
||||||
|
public string App { init; get; }
|
||||||
|
public string AppVersion { init; get; }
|
||||||
|
public string? Token { set; get; }
|
||||||
|
|
||||||
|
public readonly Uri GetServerUrl()
|
||||||
|
{
|
||||||
|
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||||
|
|
||||||
|
query["protocol-version"] = "2.0.0";
|
||||||
|
query["manufacturer"] = Manufacturer;
|
||||||
|
query["device"] = Device;
|
||||||
|
query["app"] = App;
|
||||||
|
query["app-version"] = AppVersion;
|
||||||
|
|
||||||
|
if (Token is not null)
|
||||||
|
query["token"] = Token;
|
||||||
|
|
||||||
|
return new UriBuilder("ws://127.0.0.1:8124")
|
||||||
|
{
|
||||||
|
Query = query.ToString()
|
||||||
|
}.Uri;
|
||||||
|
}
|
||||||
|
}
|
97
TeamsLocalAPI/ClientMessage.cs
Normal file
97
TeamsLocalAPI/ClientMessage.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace TeamsLocalLibary;
|
||||||
|
|
||||||
|
internal class ClientMessage
|
||||||
|
{
|
||||||
|
public MeetingAction Action { get; set; }
|
||||||
|
|
||||||
|
public ClientMessageParameter? Parameters { get; set; }
|
||||||
|
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClientMessageParameter
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public ClientMessageParameterType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
|
||||||
|
internal enum MeetingAction
|
||||||
|
{
|
||||||
|
[JsonPropertyName("none")]
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
[JsonPropertyName("query-state")]
|
||||||
|
QueryMeetingState = 0b0000_0001_0000_0000,
|
||||||
|
|
||||||
|
[JsonPropertyName("mute")]
|
||||||
|
Mute = 0b0000_0010_0000_0000,
|
||||||
|
[JsonPropertyName("unmute")]
|
||||||
|
Unmute = 0b0000_0010_0000_0001,
|
||||||
|
[JsonPropertyName("toggle-mute")]
|
||||||
|
ToggleMute = 0b0000_0010_0000_0010,
|
||||||
|
|
||||||
|
[JsonPropertyName("hide-video")]
|
||||||
|
HideVideo = 0b0000_0011_0000_0000,
|
||||||
|
[JsonPropertyName("show-video")]
|
||||||
|
ShowVideo = 0b0000_0011_0000_0001,
|
||||||
|
[JsonPropertyName("toggle-video")]
|
||||||
|
ToggleVideo = 0b0000_0011_0000_0010,
|
||||||
|
|
||||||
|
[JsonPropertyName("unblur-background")]
|
||||||
|
UnblurBackground = 0b0000_0100_0000_0000,
|
||||||
|
[JsonPropertyName("blur-background")]
|
||||||
|
BlurBackground = 0b0000_0100_0000_0001,
|
||||||
|
[JsonPropertyName("toggle-background-blur")]
|
||||||
|
ToggleBlurBackground = 0b0000_0100_0000_0010,
|
||||||
|
|
||||||
|
[JsonPropertyName("lower-hand")]
|
||||||
|
LowerHand = 0b0000_0101_0000_0000,
|
||||||
|
[JsonPropertyName("raise-hand")]
|
||||||
|
RaiseHand = 0b0000_0101_0000_0001,
|
||||||
|
[JsonPropertyName("toggle-hand")]
|
||||||
|
ToggleHand = 0b0000_0101_0000_0010,
|
||||||
|
|
||||||
|
//[JsonPropertyName("stop-recording")]
|
||||||
|
//StopRecording = 0b0000_0110_0000_0000,
|
||||||
|
//[JsonPropertyName("start-recording")]
|
||||||
|
//StartRecording = 0b0000_0110_0000_0001,
|
||||||
|
//[JsonPropertyName("toggle-recording")]
|
||||||
|
//ToggleRecording = 0b0000_0110_0000_0010,
|
||||||
|
|
||||||
|
[JsonPropertyName("leave-call")]
|
||||||
|
LeaveCall = 0b0000_0111_0000_0000,
|
||||||
|
|
||||||
|
[JsonPropertyName("send-react")]
|
||||||
|
React = 0b0000_1000_0000_0000,
|
||||||
|
|
||||||
|
[JsonPropertyName("toggle-ui")]
|
||||||
|
ToggleUI = 0b0000_1001_0000_0000,
|
||||||
|
|
||||||
|
[JsonPropertyName("stop-sharing")]
|
||||||
|
StopSharing = 0b0000_1010_0000_0000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
|
||||||
|
internal enum ClientMessageParameterType
|
||||||
|
{
|
||||||
|
[JsonPropertyName("applause")]
|
||||||
|
ReactApplause = 0b0000_0111_0001_0000,
|
||||||
|
[JsonPropertyName("laugh")]
|
||||||
|
ReactLaugh = 0b0000_0111_0001_0001,
|
||||||
|
[JsonPropertyName("like")]
|
||||||
|
ReactLike = 0b0000_0111_0001_0010,
|
||||||
|
[JsonPropertyName("love")]
|
||||||
|
ReactLove = 0b0000_0111_0001_0011,
|
||||||
|
[JsonPropertyName("wow")]
|
||||||
|
ReactWow = 0b0000_0111_0001_0100,
|
||||||
|
|
||||||
|
[JsonPropertyName("chat")]
|
||||||
|
ToggleUiChat = 0b0000_1001_0000_0001,
|
||||||
|
[JsonPropertyName("sharing-tray")]
|
||||||
|
ToggleUiSharing = 0b0000_1001_0000_0010,
|
||||||
|
|
||||||
|
}
|
20
TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs
Normal file
20
TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TeamsLocalLibary.EventArgs
|
||||||
|
{
|
||||||
|
public class ConnectionStateChangedEventArgs : System.EventArgs
|
||||||
|
{
|
||||||
|
public WebSocketState WebSocketState { get; }
|
||||||
|
|
||||||
|
public ConnectionStateChangedEventArgs(WebSocketState webSocketState)
|
||||||
|
{
|
||||||
|
WebSocketState = webSocketState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs
Normal file
6
TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace TeamsLocalLibary.EventArgs;
|
||||||
|
|
||||||
|
public class ErrorReceivedEventArgs(string errorMessage) : System.EventArgs
|
||||||
|
{
|
||||||
|
public string ErrorMessage { get; } = errorMessage;
|
||||||
|
}
|
6
TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs
Normal file
6
TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace TeamsLocalLibary.EventArgs;
|
||||||
|
|
||||||
|
public class SuccessReceivedEventArgs(int requestId) : System.EventArgs
|
||||||
|
{
|
||||||
|
public int RequestId { get; } = requestId;
|
||||||
|
}
|
6
TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs
Normal file
6
TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace TeamsLocalLibary.EventArgs;
|
||||||
|
|
||||||
|
public class TokenReceivedEventArgs(string token) : System.EventArgs
|
||||||
|
{
|
||||||
|
public string Token { get; } = token;
|
||||||
|
}
|
BIN
TeamsLocalAPI/Icon.png
Normal file
BIN
TeamsLocalAPI/Icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
21
TeamsLocalAPI/LICENSE
Normal file
21
TeamsLocalAPI/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Robin Müller
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
9
TeamsLocalAPI/README.md
Normal file
9
TeamsLocalAPI/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Third-party app API Client
|
||||||
|
|
||||||
|
Use this Package to communicate with the [Microsoft Teams Third-party app API](https://support.microsoft.com/en-us/office/connect-third-party-devices-to-teams-aabca9f2-47bb-407f-9f9b-81a104a883d6)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet add package Ro.Teams.LocalApi --version 0.3.0 --prerelease
|
||||||
|
```
|
43
TeamsLocalAPI/ServerMessage.cs
Normal file
43
TeamsLocalAPI/ServerMessage.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
namespace TeamsLocalLibary;
|
||||||
|
|
||||||
|
internal class ServerMessage
|
||||||
|
{
|
||||||
|
public int? RequestId = 0;
|
||||||
|
public string? Response { get; set; }
|
||||||
|
public string? ErrorMsg { get; set; }
|
||||||
|
public string? TokenRefresh { get; set; }
|
||||||
|
public MeetingUpdate? MeetingUpdate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MeetingUpdate
|
||||||
|
{
|
||||||
|
public MeetingState? MeetingState { get; set; }
|
||||||
|
public MeetingPermissions MeetingPermissions { get; set; } = new();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MeetingState
|
||||||
|
{
|
||||||
|
public bool IsMuted { get; set; }
|
||||||
|
public bool IsHandRaised { get; set; }
|
||||||
|
public bool IsInMeeting { get; set; }
|
||||||
|
public bool IsRecordingOn { get; set; }
|
||||||
|
public bool IsBackgroundBlurred { get; set; }
|
||||||
|
public bool IsSharing { get; set; }
|
||||||
|
public bool HasUnreadMessages { get; set; }
|
||||||
|
public bool IsVideoOn { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MeetingPermissions
|
||||||
|
{
|
||||||
|
public bool CanToggleMute { get; set; }
|
||||||
|
public bool CanToggleVideo { get; set; }
|
||||||
|
public bool CanToggleHand { get; set; }
|
||||||
|
public bool CanToggleBlur { get; set; }
|
||||||
|
public bool CanLeave { get; set; }
|
||||||
|
public bool CanReact { get; set; }
|
||||||
|
public bool CanToggleShareTray { get; set; }
|
||||||
|
public bool CanToggleChat { get; set; }
|
||||||
|
public bool CanStopSharing { get; set; }
|
||||||
|
public bool CanPair { get; set; }
|
||||||
|
}
|
37
TeamsLocalAPI/TeamsLocalLibary.csproj
Normal file
37
TeamsLocalAPI/TeamsLocalLibary.csproj
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
|
<Authors>RoundRobin</Authors>
|
||||||
|
<IncludeSymbols>True</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<Title>API Client for the client side Teams API</Title>
|
||||||
|
<RepositoryUrl>https://github.com/MrRoundRobin/TeamsLocalApi.git</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageTags>Microsoft Teams; API</PackageTags>
|
||||||
|
<PackageReleaseNotes>improved resilience & error handling</PackageReleaseNotes>
|
||||||
|
<PackageId>$(AssemblyName)</PackageId>
|
||||||
|
<Description>API Client for the client side Teams API</Description>
|
||||||
|
<PackageProjectUrl>https://github.com/MrRoundRobin/TeamsLocalApi</PackageProjectUrl>
|
||||||
|
<AssemblyVersion>0.4.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
|
<Version>$(AssemblyVersion)</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="LICENSE" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Macross.Json.Extensions" Version="3.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="LICENSE" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
8
TeamsLocalAPI/TeamsStoragePartial.cs
Normal file
8
TeamsLocalAPI/TeamsStoragePartial.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace TeamsLocalLibary
|
||||||
|
{
|
||||||
|
internal class TeamsStoragePartial
|
||||||
|
{
|
||||||
|
public bool? EnableThirdPartyDevicesService { get; set; }
|
||||||
|
public string? TpdApiTokenString { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
TeamsLocalAPI/UnixEpochDateTimeConverter.cs
Normal file
19
TeamsLocalAPI/UnixEpochDateTimeConverter.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
sealed class UnixEpochDateTimeConverter : JsonConverter<DateTime>
|
||||||
|
{
|
||||||
|
static readonly DateTime s_epoch = new(1970, 1, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
long unixTime = reader.GetInt64();
|
||||||
|
return s_epoch.AddMilliseconds(unixTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
long unixTime = Convert.ToInt64((value - s_epoch).TotalMilliseconds);
|
||||||
|
writer.WriteNumberValue(unixTime);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user