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