Added TeamsLocalAPI Project

This commit is contained in:
krjan02 2025-03-22 13:20:32 +01:00
parent a1d7487548
commit 3272781a12
14 changed files with 748 additions and 0 deletions

445
TeamsLocalAPI/Client.cs Normal file
View 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);
}

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

View 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,
}

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

View File

@ -0,0 +1,6 @@
namespace TeamsLocalLibary.EventArgs;
public class ErrorReceivedEventArgs(string errorMessage) : System.EventArgs
{
public string ErrorMessage { get; } = errorMessage;
}

View File

@ -0,0 +1,6 @@
namespace TeamsLocalLibary.EventArgs;
public class SuccessReceivedEventArgs(int requestId) : System.EventArgs
{
public int RequestId { get; } = requestId;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

21
TeamsLocalAPI/LICENSE Normal file
View 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
View 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
```

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

View 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 &amp; 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>

View File

@ -0,0 +1,8 @@
namespace TeamsLocalLibary
{
internal class TeamsStoragePartial
{
public bool? EnableThirdPartyDevicesService { get; set; }
public string? TpdApiTokenString { get; set; }
}
}

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