From 3272781a12ec76b527bd3a618008ee29ab1920b7 Mon Sep 17 00:00:00 2001 From: krjan02 Date: Sat, 22 Mar 2025 13:20:32 +0100 Subject: [PATCH] Added TeamsLocalAPI Project --- TeamsLocalAPI/Client.cs | 445 ++++++++++++++++++ TeamsLocalAPI/ClientInfo.cs | 31 ++ TeamsLocalAPI/ClientMessage.cs | 97 ++++ .../ConnectionStateChangedEventArgs.cs | 20 + .../EventArgs/ErrorReceivedEventArgs.cs | 6 + .../EventArgs/SuccessReceivedEventArgs.cs | 6 + .../EventArgs/TokenReceivedEventArgs.cs | 6 + TeamsLocalAPI/Icon.png | Bin 0 -> 3788 bytes TeamsLocalAPI/LICENSE | 21 + TeamsLocalAPI/README.md | 9 + TeamsLocalAPI/ServerMessage.cs | 43 ++ TeamsLocalAPI/TeamsLocalLibary.csproj | 37 ++ TeamsLocalAPI/TeamsStoragePartial.cs | 8 + TeamsLocalAPI/UnixEpochDateTimeConverter.cs | 19 + 14 files changed, 748 insertions(+) create mode 100644 TeamsLocalAPI/Client.cs create mode 100644 TeamsLocalAPI/ClientInfo.cs create mode 100644 TeamsLocalAPI/ClientMessage.cs create mode 100644 TeamsLocalAPI/EventArgs/ConnectionStateChangedEventArgs.cs create mode 100644 TeamsLocalAPI/EventArgs/ErrorReceivedEventArgs.cs create mode 100644 TeamsLocalAPI/EventArgs/SuccessReceivedEventArgs.cs create mode 100644 TeamsLocalAPI/EventArgs/TokenReceivedEventArgs.cs create mode 100644 TeamsLocalAPI/Icon.png create mode 100644 TeamsLocalAPI/LICENSE create mode 100644 TeamsLocalAPI/README.md create mode 100644 TeamsLocalAPI/ServerMessage.cs create mode 100644 TeamsLocalAPI/TeamsLocalLibary.csproj create mode 100644 TeamsLocalAPI/TeamsStoragePartial.cs create mode 100644 TeamsLocalAPI/UnixEpochDateTimeConverter.cs 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 0000000000000000000000000000000000000000..375a5b2c0463f7d57a8fb48b9366eebeeb19d55f GIT binary patch literal 3788 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 + + + 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