diff --git a/TeamsLocalAPI/Client.cs b/TeamsLocalAPI/Client.cs new file mode 100644 index 0000000..ba0ec4e --- /dev/null +++ b/TeamsLocalAPI/Client.cs @@ -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? ConnectionState; + + public event PropertyChangedEventHandler? PropertyChanged; + + public event EventHandler? TokenReceived; + public event EventHandler? SuccessReceived; + public event EventHandler? 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(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 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); +} diff --git a/TeamsLocalAPI/ClientInfo.cs b/TeamsLocalAPI/ClientInfo.cs new file mode 100644 index 0000000..2c569b1 --- /dev/null +++ b/TeamsLocalAPI/ClientInfo.cs @@ -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; + } +} diff --git a/TeamsLocalAPI/ClientMessage.cs b/TeamsLocalAPI/ClientMessage.cs new file mode 100644 index 0000000..c8663d7 --- /dev/null +++ b/TeamsLocalAPI/ClientMessage.cs @@ -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, + +} diff --git a/TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs b/TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs new file mode 100644 index 0000000..3b5981e --- /dev/null +++ b/TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs @@ -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; + } + } + +} diff --git a/TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs b/TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs new file mode 100644 index 0000000..93636ae --- /dev/null +++ b/TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs @@ -0,0 +1,6 @@ +namespace TeamsLocalLibary.EventArgs; + +public class ErrorReceivedEventArgs(string errorMessage) : System.EventArgs +{ + public string ErrorMessage { get; } = errorMessage; +} diff --git a/TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs b/TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs new file mode 100644 index 0000000..7e436b9 --- /dev/null +++ b/TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs @@ -0,0 +1,6 @@ +namespace TeamsLocalLibary.EventArgs; + +public class SuccessReceivedEventArgs(int requestId) : System.EventArgs +{ + public int RequestId { get; } = requestId; +} diff --git a/TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs b/TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs new file mode 100644 index 0000000..454f1bf --- /dev/null +++ b/TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs @@ -0,0 +1,6 @@ +namespace TeamsLocalLibary.EventArgs; + +public class TokenReceivedEventArgs(string token) : System.EventArgs +{ + public string Token { get; } = token; +} diff --git a/TeamsLocalAPI/Icon.png b/TeamsLocalAPI/Icon.png new file mode 100644 index 0000000..375a5b2 Binary files /dev/null and b/TeamsLocalAPI/Icon.png differ diff --git a/TeamsLocalAPI/LICENSE b/TeamsLocalAPI/LICENSE new file mode 100644 index 0000000..78efe40 --- /dev/null +++ b/TeamsLocalAPI/LICENSE @@ -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. diff --git a/TeamsLocalAPI/README.md b/TeamsLocalAPI/README.md new file mode 100644 index 0000000..0d3108d --- /dev/null +++ b/TeamsLocalAPI/README.md @@ -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 +``` diff --git a/TeamsLocalAPI/ServerMessage.cs b/TeamsLocalAPI/ServerMessage.cs new file mode 100644 index 0000000..ab19fb2 --- /dev/null +++ b/TeamsLocalAPI/ServerMessage.cs @@ -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; } +} diff --git a/TeamsLocalAPI/TeamsLocalLibary.csproj b/TeamsLocalAPI/TeamsLocalLibary.csproj new file mode 100644 index 0000000..c5fee92 --- /dev/null +++ b/TeamsLocalAPI/TeamsLocalLibary.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + $(MSBuildProjectName.Replace(" ", "_")) + RoundRobin + True + snupkg + True + API Client for the client side Teams API + https://github.com/MrRoundRobin/TeamsLocalApi.git + git + Microsoft Teams; API + improved resilience & error handling + $(AssemblyName) + API Client for the client side Teams API + https://github.com/MrRoundRobin/TeamsLocalApi + 0.4.0.0 + $(AssemblyVersion) + $(AssemblyVersion) + + + + + + + + + + + + + + + diff --git a/TeamsLocalAPI/TeamsStoragePartial.cs b/TeamsLocalAPI/TeamsStoragePartial.cs new file mode 100644 index 0000000..8e1fdc8 --- /dev/null +++ b/TeamsLocalAPI/TeamsStoragePartial.cs @@ -0,0 +1,8 @@ +namespace TeamsLocalLibary +{ + internal class TeamsStoragePartial + { + public bool? EnableThirdPartyDevicesService { get; set; } + public string? TpdApiTokenString { get; set; } + } +} diff --git a/TeamsLocalAPI/UnixEpochDateTimeConverter.cs b/TeamsLocalAPI/UnixEpochDateTimeConverter.cs new file mode 100644 index 0000000..3d0642f --- /dev/null +++ b/TeamsLocalAPI/UnixEpochDateTimeConverter.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +sealed class UnixEpochDateTimeConverter : JsonConverter +{ + 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); + } +} \ No newline at end of file