diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/TeamsNetphoneLink.sln b/TeamsNetphoneLink.sln new file mode 100644 index 0000000..19024a3 --- /dev/null +++ b/TeamsNetphoneLink.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamsLocalLibary", "TeamsLocalAPI\TeamsLocalLibary.csproj", "{0075EE3E-C82F-4EAA-A9A3-32BED89C90EA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamsNetphoneLinkWPF", "TeamsNetphoneLinkWPF\TeamsNetphoneLinkWPF.csproj", "{5904F1F3-755E-4D10-8906-5FC69F85F389}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsNetphoneLinkUpdater", "TeamsNetphoneLinkUpdater\TeamsNetphoneLinkUpdater.csproj", "{BB18A636-95F8-4555-9E96-31575E9CB6A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0075EE3E-C82F-4EAA-A9A3-32BED89C90EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0075EE3E-C82F-4EAA-A9A3-32BED89C90EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0075EE3E-C82F-4EAA-A9A3-32BED89C90EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0075EE3E-C82F-4EAA-A9A3-32BED89C90EA}.Release|Any CPU.Build.0 = Release|Any CPU + {5904F1F3-755E-4D10-8906-5FC69F85F389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5904F1F3-755E-4D10-8906-5FC69F85F389}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5904F1F3-755E-4D10-8906-5FC69F85F389}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5904F1F3-755E-4D10-8906-5FC69F85F389}.Release|Any CPU.Build.0 = Release|Any CPU + {BB18A636-95F8-4555-9E96-31575E9CB6A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB18A636-95F8-4555-9E96-31575E9CB6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB18A636-95F8-4555-9E96-31575E9CB6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB18A636-95F8-4555-9E96-31575E9CB6A2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32340761-08D0-41C6-A769-B606A5B73F01} + EndGlobalSection +EndGlobal diff --git a/TeamsNetphoneLinkWPF/App.config b/TeamsNetphoneLinkWPF/App.config new file mode 100644 index 0000000..ed75900 --- /dev/null +++ b/TeamsNetphoneLinkWPF/App.config @@ -0,0 +1,63 @@ + + + + +
+
+
+ + + + + + + + + False + + + + + + + + + False + + + False + + + + + + + + + + + True + + + + + + + + + + + + + + True + + + + + + + + + + \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/App.xaml b/TeamsNetphoneLinkWPF/App.xaml new file mode 100644 index 0000000..53f520d --- /dev/null +++ b/TeamsNetphoneLinkWPF/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/TeamsNetphoneLinkWPF/App.xaml.cs b/TeamsNetphoneLinkWPF/App.xaml.cs new file mode 100644 index 0000000..2179704 --- /dev/null +++ b/TeamsNetphoneLinkWPF/App.xaml.cs @@ -0,0 +1,79 @@ +using Azure.Identity.Broker; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.NativeInterop; +using System; +using System.Configuration; +using System.Data; +using System.Printing; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using TeamsNetphoneLink.WPF; +using TeamsNetphoneLink.WPF.MVVM; + +namespace TeamsNetphoneLink +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllocConsole(); + + public App() + { +#if DEBUG + // Konsole für Debugging-Zwecke öffnen + AllocConsole(); +#endif + // Initialisierung der Anwendungskomponenten + InitializeApplication(); + } + + private void InitializeApplication() + { + UpdateCheck.UpgradeSettingsIfRequired(); + + + + // Erstellen der benötigten Instanzen + var TeamsLocalAPI = new Teams.TeamsLocalAPI(); + var Netphone = new Netphone.NetPhoneEvents(); + var TeamsGraph = new Teams.TeamsGraph(); + var dashboardViewModel = new DashboardViewModel(); + var LogEntries = new LogViewModel(); + var finishPanelViewModel = new FinishPanelViewModel(); + + var latestTag = UpdateCheck.GetLatestReleaseTagAsync().GetAwaiter().GetResult(); + var currentTag = UpdateCheck.GetCurrentVersion(); + dashboardViewModel.VersionText = currentTag; + + // Erstellen der Synchronisationsinstanz + var sync = new Syncronisation(Netphone, TeamsLocalAPI, TeamsGraph, dashboardViewModel, LogEntries, finishPanelViewModel); + + // Erstellen und Anzeigen des Dashboard-Fensters + var dashboard = new WPF.DashboardWindow(sync); + dashboard.Show(); + + if (UpdateCheck.IsVersionLower(currentTag, latestTag)) + { + UpdateCheck.UpdateAvailable = true; + dashboardViewModel.VersionText = String.Format("Version {0} verfügbar!", latestTag); + dashboardViewModel.VersionBackground = new SolidColorBrush(Colors.Orange); + + // Erstellen und Anzeigen des Update-Fensters + var updateWindow = new WPF.UpdateWindow(currentTag, latestTag); + updateWindow.Show(); + } + + // Asynchrone Initialisierung von Netphone und TeamsLocalAPI + sync.InitializeTeamsGraphAsync(); + sync.InitializeNetphoneAsync(); + sync.InitializeTeamsLocalAPIAsync(); + } + } +} \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/AssemblyInfo.cs b/TeamsNetphoneLinkWPF/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/TeamsNetphoneLinkWPF/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/TeamsNetphoneLinkWPF/Communication/ClientSdkEvents.cs b/TeamsNetphoneLinkWPF/Communication/ClientSdkEvents.cs new file mode 100644 index 0000000..8b6a2b8 --- /dev/null +++ b/TeamsNetphoneLinkWPF/Communication/ClientSdkEvents.cs @@ -0,0 +1,121 @@ +using CLMGRLib; + +namespace TeamsNetphoneLink.Netphone +{ + public class ClientSdkEventArgs : EventArgs + { + public CLMgrMessage Msg; + public int Param; + + public ClientSdkEventArgs(CLMgrMessage msg, int param) + { + Msg = msg; + Param = param; + } + } + + public delegate void LineManagerMessageHandler(ClientSdkEventArgs e); + + public class ClientSdkEventSink + { + private ClientLineMgrClass ConnectedLineManager; + private IClientLineMgrEventsPub_PubOnLineMgrNotificationEventHandler EventHandler; + private LineManagerMessageHandler LineManagerMessageDelegateOfForm; + + public ClientSdkEventSink() + { + EventHandler = new IClientLineMgrEventsPub_PubOnLineMgrNotificationEventHandler(clmgr_EventSink); + } + + public void Connect(ClientLineMgrClass lineManager, LineManagerMessageHandler lineManagerMessageDelegateOfForm) + { + ConnectedLineManager = lineManager; + LineManagerMessageDelegateOfForm = lineManagerMessageDelegateOfForm; + + //add eventhandler for the PubOnlineMgrNotification Events + ConnectedLineManager.PubOnLineMgrNotification += EventHandler; + } + + public void Disconnect() + { + //remove eventhandler for the PubOnlineMgrNotification Events + ConnectedLineManager.PubOnLineMgrNotification -= EventHandler; + ConnectedLineManager = null; + LineManagerMessageDelegateOfForm = null; + } + + private void clmgr_EventSink(int msg, int param) + { + //this method receives the COM events from the client line manger + if ((LineManagerMessageDelegateOfForm != null)) + { + LineManagerMessageDelegateOfForm(new ClientSdkEventArgs((CLMgrMessage)msg, param)); + } + } + } + + public enum CLMgrMessage + { + CLMgrLineStateChangedMessage = 0, //state of at least one line has changed + CLMgrLineSelectionChangedMessage = 1, //line in focus has changed + CLMgrLineDetailsChangedMessage = 2, //details of at least one line have changed + CLMgrCallDetailsMessage = 4, //details of last call are available, post mortem for logging purpose + CLMgrServerDownMessage = 5, //server goes down, keep line manager, wait for ServerUp message + CLMgrServerUpMessage = 6, //server is up again, keep interfaces to line manger + CLMgrWaveDeviceChanged = 7, //speaker / micro has been switched on / off + CLMgrGroupCallNotificationMessage = 8, //notification about group call + CLMgrNumberOfLinesChangedMessage = 10, //the number of lines has changed + CLMgrClientShutDownRequest = 11, //Client Line Manager requests client to shutdown and release all interfaces + CLMgrLineStateChangedMessageEx = 28, //state of certain line has changed, lParam: LOWORD: line index of line that changed its state (starting with 0) HIWORD: new state of this line + CLMgrSIPRegistrationStateChanged = 30, //registration state of SIP account has changed + //lParam: LOBYTE: Account index + // HIBYTE: new state + CLMgrWaveFilePlayed = 31, //wave file playback finished + //lParam: line index; + //if -1, the message is related to a LineMgr function PlaySoundFile or PlayToRtp + //if >=0 the message is related to a line function PlaySoundFile of line with this index + PubCLMgrFirstDataReceived = 32 //first RTP data received on line, might be silence + //lParam: line index; + } + + public enum LineState + { + Inactive = 0, //line is inactive + HookOffInternal = 1, //off hook, internal dialtone + HookOffExternal = 2, //off hook, external dialtone + Ringing = 3, //incoming call, ringing + Dialing = 4, //outgoing call, we are dialing, no sound + Alerting = 5, //outgoing call, alerting = ringing on destination + Knocking = 6, //outgoing call, knocking = second call ringing on destination + Busy = 7, //outgoing call, destination is busy + Active = 8, //incoming / outgoing call, logical and physical connection is established + OnHold = 9, //incoming / outgoing call, logical connection is established, destination gets music on hold + ConferenceActive = 10, //incoming / outgoing conference, logical and physical connection is established + ConferenceOnHold = 11, //incoming / outgoing conference, logical connection is established, not physcically connected + Terminated = 12, //incoming / outgoing connection / call has been disconnected + Transferring = 13, //special LSOnHold, call is awaiting to be transferred, peer gets special music on hold + Disabled = 14 //special LSInactive: wrap up time + } + + public enum DisconnectReason + { + Normal = 0, + Busy = 1, + Rejected = 2, + Cancelled = 3, + Transferred = 4, + JoinedConference = 5, + NoAnswer = 6, + TooLate = 7, + DirectCallImpossible = 8, + WrongNumber = 9, + Unreachable = 10, + CallDiverted = 11, + CallRoutingFailed = 12, + PermissionDenied = 13, + NetworkCongestion = 14, + NoChannelAvailable = 15, + NumberChanged = 16, + IncompatibleDestination = 17 + } +} diff --git a/TeamsNetphoneLinkWPF/Communication/NetPhone.cs b/TeamsNetphoneLinkWPF/Communication/NetPhone.cs new file mode 100644 index 0000000..b772753 --- /dev/null +++ b/TeamsNetphoneLinkWPF/Communication/NetPhone.cs @@ -0,0 +1,200 @@ +using CLMGRLib; +using System.Diagnostics; +using TeamsLocalLibary.EventArgs; + +namespace TeamsNetphoneLink.Netphone +{ + public delegate void LineStateChangedEventHandler(LineState newLineState); + public delegate void LoggedInStateEventHandler(bool loggedInState); + + public class NetPhoneEvents : IDisposable + { + private ClientLine SelectedLine; + private ClientLineMgrClass pCLMgr; + private ClientSdkEventSink MyEventSink; + private LineState LastLineState; + private bool lastLoggedInState; + + private CancellationTokenSource aliveCheckTokenSource; + private CancellationToken aliveCheckToken; + + private Dictionary> lineStateEvents = new Dictionary>(); + private List loggedInStateEvents = new List(); + + + private Timer loggedInStateTimer; + private Timer aliveTimer; + + public event EventHandler? TokenReceived; + + public NetPhoneEvents() + { + loggedInStateTimer = new Timer(CheckLoggedInState, null, Timeout.Infinite, 1000); + aliveTimer = new Timer(AliveCheck, null, Timeout.Infinite, 1000); + } + + public void Dispose() + { + loggedInStateTimer?.Change(Timeout.Infinite, 0); + loggedInStateTimer?.Dispose(); + RemoveAllEventHandlers(); + } + + public async Task Initialize(bool waitForNetphone = false, CancellationToken cancellationToken = default) + { + if (Process.GetProcessesByName("CLMgr").Length == 0 && !waitForNetphone) + return false; + + while (Process.GetProcessesByName("CLMgr").Length == 0) + await Task.Delay(1000, cancellationToken); + + + Console.WriteLine("new interface"); + pCLMgr = new ClientLineMgrClass(); + MyEventSink = new ClientSdkEventSink(); + MyEventSink.Connect(pCLMgr, new LineManagerMessageHandler(OnLineManagerMessage)); + + aliveTimer.Change(0, 1000); + loggedInStateTimer.Change(0, 1500); + + + return true; + } + + public void AliveCheck(object state) + { + if (Process.GetProcessesByName("CLMgr").Length == 0) + { + _ = Initialize(true).GetAwaiter().GetResult(); + } + } + + private void CheckLoggedInState(object state) + { + bool currentLoggedInState = false; + + try + { + currentLoggedInState = pCLMgr.DispIsLoggedIn != 0; + } + catch (Exception ex) + { + currentLoggedInState = false; + } + + if (currentLoggedInState != lastLoggedInState) + { + lastLoggedInState = currentLoggedInState; + OnLoggedInStateChanged(currentLoggedInState); + } + } + + + private void OnLoggedInStateChanged(bool isLoggedIn) + { + foreach (var handler in loggedInStateEvents) + { + handler?.Invoke(isLoggedIn); + } + } + + public void AddLineStateEventHandler(LineState lineState, LineStateChangedEventHandler handler) + { + lock (lineStateEvents) + { + if (!lineStateEvents.ContainsKey(lineState)) + { + lineStateEvents[lineState] = new List(); + } + lineStateEvents[lineState].Add(handler); + } + } + + public void AddLoggedInStateEventHandler(LoggedInStateEventHandler handler) + { + lock (loggedInStateEvents) + { + loggedInStateEvents.Add(handler); + } + } + + public void RemoveLineStateEventHandler(LineState lineState, LineStateChangedEventHandler handler) + { + lock (lineStateEvents) + { + if (lineStateEvents.ContainsKey(lineState)) + { + lineStateEvents[lineState].Remove(handler); + } + } + } + + public void RemoveLoggedInStateEventHandler(LoggedInStateEventHandler handler) + { + lock (loggedInStateEvents) + { + loggedInStateEvents.Remove(handler); + } + } + + public void RemoveAllEventHandlers() + { + lock (lineStateEvents) + { + lineStateEvents.Clear(); + } + lock (loggedInStateEvents) + { + loggedInStateEvents.Clear(); + } + } + + private void OnLineManagerMessage(ClientSdkEventArgs e) + { + SelectedLine = (ClientLine)pCLMgr.DispSelectedLine; + + if (e.Msg == CLMgrMessage.CLMgrClientShutDownRequest) + { + //aliveCheckTokenSource.Cancel(); + //MyEventSink.Disconnect(); + //OnConnectionStateChanged(false,true); + } + + if (e.Msg == CLMgrMessage.CLMgrLineStateChangedMessageEx) + { + int line = e.Param & 0xff; + int high = e.Param >> 8; + + LineState NewLineState = (LineState)high; + + if (LastLineState != NewLineState) + { + LastLineState = NewLineState; + + lock (lineStateEvents) + { + if (lineStateEvents.ContainsKey(NewLineState)) + { + foreach (var handler in lineStateEvents[NewLineState]) + { + handler?.Invoke(NewLineState); + } + } + } + } + } + } + + public void SetRichPresenceStatus(int away, int dnd, DateTime expires) + { + var clientConfig = (ClientConfig)this.pCLMgr.ClientConfig; + clientConfig.SetRichPresenceStatus(away, dnd, expires); + } + + public void SetAppointmentText(string appointmentText, DateTime expires) + { + var clientConfig = (ClientConfig)this.pCLMgr.ClientConfig; + clientConfig.SetAppointmentText(appointmentText, expires); + } + } +} \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/Communication/TeamsGraph.cs b/TeamsNetphoneLinkWPF/Communication/TeamsGraph.cs new file mode 100644 index 0000000..f4dd19e --- /dev/null +++ b/TeamsNetphoneLinkWPF/Communication/TeamsGraph.cs @@ -0,0 +1,290 @@ +using Azure.Core; +using Azure.Identity; +using Azure.Identity.Broker; +using Microsoft.Graph; +using Microsoft.Graph.Drives.Item.Items.Item.Workbook.Functions.Cosh; +using Microsoft.Graph.Me.Presence.SetUserPreferredPresence; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Broker; +using Microsoft.Identity.Client.Extensions.Msal; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; +using TeamsLocalLibary; +using static System.Formats.Asn1.AsnWriter; +using Microsoft.Identity.Client.NativeInterop; +using TeamsNetphoneLink.WPF; +using TeamsNetphoneLink.Communication; +using System; +using System.Text.RegularExpressions; + +namespace TeamsNetphoneLink.Teams +{ + public delegate void PresenceStausEventHandler(Microsoft.Graph.Models.Presence presence); + + public class TeamsGraph : TeamsGraphEventHandlers + { + private GraphServiceClient graphClient; + public bool Authenticated { get; private set; } + + private Timer presenceStatusTimer; + private Microsoft.Graph.Models.Presence lastPresence; + public void CheckPresenceStatusTimer() + { + presenceStatusTimer = new Timer(CheckPresenceStatus, null, Timeout.Infinite, 2000); + presenceStatusTimer.Change(0, 1000); + } + + public void CheckPresenceStatus(object state) + { + if (graphClient is not null && Authenticated) + { + try + { + var presence = graphClient.Me.Presence.GetAsync().GetAwaiter().GetResult(); + + // If lastPresence is null, initialize it with the current presence + if (lastPresence == null) + { + lastPresence = presence; + } + + // Check if Availability has changed + if (lastPresence.Availability != presence.Availability) + { + OnAvailabilityChanged(presence); + } + + // Check if Activity has changed + if (lastPresence.Activity != presence.Activity) + { + OnActivityChanged(presence); + } + + // Update lastPresence to the current presence + lastPresence = presence; + } + catch (Exception ex) + { + Console.WriteLine($"Error checking presence state: {ex.Message}"); + } + } + } + + public async Task IsCachedAccounts() + { + IPublicClientApplication app = PublicClientApplicationBuilder.Create(Settings.Default.AppID) + .WithDefaultRedirectUri() + .WithAuthority(String.Format("https://login.microsoftonline.com/{0}", Settings.Default.TenantID)) + .Build(); + + // Register MSAL cache + + var storage = new StorageCreationPropertiesBuilder("teamsnetphonelink.msal.cache", MsalCacheHelper.UserRootDirectory).Build(); + + var cacheHelper = await MsalCacheHelper.CreateAsync(storage); + + cacheHelper.RegisterCache(app.UserTokenCache); + + IEnumerable accounts = await app.GetAccountsAsync(); + + return accounts.Any(); + } + + // Authentifizierungsmethode mit Azure Identity + public async Task AuthenticateAsync(bool clearCache = false, bool silent = true) + { + try + { + // Check if caching is enabled in settings + bool useCache = Settings.Default.SaveEntraCredentials; + + IPublicClientApplication app = PublicClientApplicationBuilder.Create(Settings.Default.AppID) + .WithDefaultRedirectUri() + .WithAuthority(String.Format("https://login.microsoftonline.com/{0}", Settings.Default.TenantID)) + .Build(); + + // Register MSAL cache only if caching is enabled + if (useCache) + { + var storage = new StorageCreationPropertiesBuilder("teamsnetphonelink.msal.cache", MsalCacheHelper.UserRootDirectory).Build(); + + var cacheHelper = await MsalCacheHelper.CreateAsync(storage); + + cacheHelper.RegisterCache(app.UserTokenCache); + } + + IEnumerable accounts = await app.GetAccountsAsync(); + + //Alle Accounts aus dem Cache entfernen wenn clearCache gesetzt ist + if (clearCache) + { + foreach(var account in accounts) + await app.RemoveAsync(account); + } + + // Try to use the previously signed-in account from the cache + var existingAccount = accounts.FirstOrDefault(); + + AuthenticationResult authentication; + + if (existingAccount is not null && useCache && !clearCache) + { + Console.WriteLine("Attempting to acquire token silently using cached account."); + try + { + authentication = await app.AcquireTokenSilent(new[] { "Presence.ReadWrite", "offline_access" }, existingAccount) + .ExecuteAsync(); + + await InitializeGraphClient(authentication.AccessToken); + + return Authenticated; + } + catch (MsalUiRequiredException) + { + Console.WriteLine("Silent token acquisition failed. Falling back to interactive authentication."); + } + } + + // If no cached account or silent authentication fails, prompt the user for authentication + Console.WriteLine("Prompting user for authentication."); + authentication = await app.AcquireTokenInteractive(new[] { "Presence.ReadWrite", "offline_access" }) + .ExecuteAsync(); + + await InitializeGraphClient(authentication.AccessToken); + + return Authenticated; + } + catch (Exception ex) + { + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Fehler bei der Authentifizierung", ex.Message, ex.StackTrace); + Console.WriteLine($"Error during authentication: {ex.Message}"); + return Authenticated; + } + } + + // Helper method to initialize GraphServiceClient + private async Task InitializeGraphClient(string accessToken) + { + graphClient = new GraphServiceClient(new HttpClient(new AuthHandler(accessToken, new HttpClientHandler()))); + Authenticated = await TestAuthentication(); + } + + // Testung der Anmeldung und Zugriffsrechte + public async Task TestAuthentication() + { + try + { + // Test-Anfrage, um Authentifizierung zu validieren + await graphClient.Me.Presence.GetAsync(); + return true; // Anmeldung und Zugriffsrechte korrekt + } + catch (Exception ex) + { + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Authentifizierung fehlerhaft", ex.Message, ex.StackTrace); + return false; // Anmeldung und Zugriffsrechte felerhaft + } + } + + // Methode zum Setzen des Präsenzstatus + public async Task SetPresenceAsync(PresenceState presenceState) + { + if (graphClient is null || !Authenticated) + { + Console.WriteLine("Authentifizierung nicht abgeschlossen. Präsenz kann nicht gesetzt werden."); + return false; + } + + // Zuordnen des Präsenzstatus zu Verfügbarkeit und Aktivität + var presenceMap = new Dictionary + { + { PresenceState.Available, ("Available", "Available") }, + { PresenceState.Busy, ("Busy", "Busy") }, + { PresenceState.DoNotDisturb, ("DoNotDisturb", "DoNotDisturb") }, + { PresenceState.BeRightBack, ("BeRightBack", "BeRightBack") }, + { PresenceState.Away, ("Away", "Away") }, + { PresenceState.Offline, ("Offline", "OffWork") } + }; + + if (!presenceMap.ContainsKey(presenceState)) + { + Console.WriteLine("Ungültiger Präsenzstatus."); + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Ungültiger Präsenzstatus", presenceState.ToString()); + return false; // Ungültiger Status + } + + var (availability, activity) = presenceMap[presenceState]; + + var requestBody = new SetUserPreferredPresencePostRequestBody + { + Availability = availability, + Activity = activity, + ExpirationDuration = TimeSpan.FromHours(2) // Ablaufzeit der Präsenz + }; + + try + { + await graphClient.Me.Presence.SetUserPreferredPresence.PostAsync(requestBody); + return true; // Präsenz erfolgreich gesetzt + } + catch (Exception ex) + { + if(ex.GetType() != typeof(Microsoft.Graph.Models.ODataErrors.ODataError)) + { + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Fehler beim Setzen der Präsenz", ex.Message, ex.StackTrace); + } + return false; // Fehler beim Setzen der Präsenz + } + } + + // Enum für die verschiedenen Präsenzstatus + public enum PresenceState + { + Available, + Busy, + DoNotDisturb, + BeRightBack, + Away, + Offline + } + + private static Regex isGuid = + new Regex(@"^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$", RegexOptions.Compiled); + + public static bool IsGuid(string candidate) + { + bool isValid = false; + + if (candidate != null) + { + + if (isGuid.IsMatch(candidate)) + { + isValid = true; + } + } + return isValid; + } + } + + // Custom HttpMessageHandler to inject the access token + public class AuthHandler : DelegatingHandler + { + private readonly string _accessToken; + + public AuthHandler(string accessToken, HttpMessageHandler innerHandler) + : base(innerHandler) + { + _accessToken = accessToken; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Add the access token to the request headers + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); + return await base.SendAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/Communication/TeamsGraphEventHandlers.cs b/TeamsNetphoneLinkWPF/Communication/TeamsGraphEventHandlers.cs new file mode 100644 index 0000000..bae250c --- /dev/null +++ b/TeamsNetphoneLinkWPF/Communication/TeamsGraphEventHandlers.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TeamsNetphoneLink.Communication +{ + public class TeamsGraphEventHandlers + { + private readonly object _lock = new object(); + private readonly List> _activityChangedHandlers = new List>(); + private readonly List> _availabilityChangedHandlers = new List>(); + + public event EventHandler ActivityChanged + { + add + { + lock (_lock) + { + _activityChangedHandlers.Add(value); + } + } + remove + { + lock (_lock) + { + _activityChangedHandlers.Remove(value); + } + } + } + + public event EventHandler AvailabilityChanged + { + add + { + lock (_lock) + { + _availabilityChangedHandlers.Add(value); + } + } + remove + { + lock (_lock) + { + _availabilityChangedHandlers.Remove(value); + } + } + } + + protected void OnActivityChanged(Microsoft.Graph.Models.Presence presence) + { + lock (_lock) + { + var args = new PresenceChangedEventArgs(presence); + foreach (var handler in _activityChangedHandlers) + { + handler?.Invoke(this, args); + } + } + } + + protected void OnAvailabilityChanged(Microsoft.Graph.Models.Presence presence) + { + lock (_lock) + { + var args = new PresenceChangedEventArgs(presence); + foreach (var handler in _availabilityChangedHandlers) + { + handler?.Invoke(this, args); + } + } + } + + public void RemoveAllEventHandlers() + { + foreach (var handler in _activityChangedHandlers) + { + _availabilityChangedHandlers.Remove(handler); + } + foreach (var handler in _availabilityChangedHandlers) + { + _availabilityChangedHandlers.Remove(handler); + } + } + } + + public class PresenceChangedEventArgs : EventArgs + { + public Microsoft.Graph.Models.Presence Presence { get; } + + public PresenceChangedEventArgs(Microsoft.Graph.Models.Presence presence) + { + Presence = presence; + } + } +} diff --git a/TeamsNetphoneLinkWPF/Communication/TeamsLocalAPI.cs b/TeamsNetphoneLinkWPF/Communication/TeamsLocalAPI.cs new file mode 100644 index 0000000..ee096e9 --- /dev/null +++ b/TeamsNetphoneLinkWPF/Communication/TeamsLocalAPI.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net.WebSockets; +using System.Reflection; +using TeamsLocalLibary; +using TeamsLocalLibary.EventArgs; + +namespace TeamsNetphoneLink.Teams +{ + public class TeamsLocalAPI + { + public event EventHandler? ConnectionState; + public event EventHandler? TokenReceived; + + private Client teamsClient; + private string token = Settings.Default.Token != string.Empty ? Settings.Default.Token : null; + private Dictionary> propertyChangedHandlers = new Dictionary>(); + private CancellationTokenSource cts = new(); + + // Destructor + ~TeamsLocalAPI() + { + RemoveAllEventHandlers(); + } + + public async Task Initialize() + { + // Initialize the TeamsClient with token and no auto-connect + teamsClient = new Client(autoConnect: false, token: token) ?? throw new Exception("Could not create client"); + + // Event-handler for token reception + teamsClient.TokenReceived += (_, args) => + { + Settings.Default.Token = args.Token; + Settings.Default.Save(); + + TokenReceived?.Invoke(_, args); + }; + + WebSocketState lastConnectionState = WebSocketState.None; + teamsClient.ConnectionState += (sender, args) => + { + if (args.WebSocketState != lastConnectionState) + { + lastConnectionState = args.WebSocketState; + // Raise the ConnectionState event in TeamsLocalAPI + ConnectionState?.Invoke(sender, args); + } + }; + + teamsClient.PropertyChanged += HandlePropertyChanged; + + teamsClient.ErrorReceived += (_, args) => Console.WriteLine("Event: ErrorReceived: {0}", args.ErrorMessage); + + return await teamsClient.Connect(true, cts.Token); + } + + public void AddTokenRecievedHandler(EventHandler handler) + { + TokenReceived += handler; + } + + public void RemoveTokenRecievedHandler(EventHandler handler) + { + TokenReceived -= handler; + } + + public void AddConnectionStateHandler(EventHandler handler) + { + ConnectionState += handler; + } + + public void RemoveConnectionStateHandler(EventHandler handler) + { + ConnectionState -= handler; + } + + public async void SendDummyCommand() + { + var dummy = teamsClient.IsMuted = true; + } + + public void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not null && propertyChangedHandlers.ContainsKey(e.PropertyName)) + { + foreach (var handler in propertyChangedHandlers[e.PropertyName]) + { + handler?.Invoke(sender, e); + } + } + } + + public void AddEventHandler(string propertyName, PropertyChangedEventHandler handler) + { + if (!propertyChangedHandlers.ContainsKey(propertyName)) + { + propertyChangedHandlers[propertyName] = new List(); + } + + propertyChangedHandlers[propertyName].Add(handler); + } + + public void RemoveEventHandler(string propertyName, PropertyChangedEventHandler handler) + { + if (propertyChangedHandlers.ContainsKey(propertyName)) + { + propertyChangedHandlers[propertyName].Remove(handler); + } + } + + public void RemoveAllEventHandlers() + { + propertyChangedHandlers.Clear(); + } + } +} diff --git a/TeamsNetphoneLinkWPF/Settings.Designer.cs b/TeamsNetphoneLinkWPF/Settings.Designer.cs new file mode 100644 index 0000000..ce01015 --- /dev/null +++ b/TeamsNetphoneLinkWPF/Settings.Designer.cs @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +// +// Dieser Code wurde von einem Tool generiert. +// Laufzeitversion:4.0.30319.42000 +// +// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn +// der Code erneut generiert wird. +// +//------------------------------------------------------------------------------ + +namespace TeamsNetphoneLink { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string Token { + get { + return ((string)(this["Token"])); + } + set { + this["Token"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool TeamsPermission { + get { + return ((bool)(this["TeamsPermission"])); + } + set { + this["TeamsPermission"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string TenantID { + get { + return ((string)(this["TenantID"])); + } + set { + this["TenantID"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string AppID { + get { + return ((string)(this["AppID"])); + } + set { + this["AppID"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool SaveEntraCredentials { + get { + return ((bool)(this["SaveEntraCredentials"])); + } + set { + this["SaveEntraCredentials"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UseGraphForMeetingState { + get { + return ((bool)(this["UseGraphForMeetingState"])); + } + set { + this["UseGraphForMeetingState"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string CurrentVersion { + get { + return ((string)(this["CurrentVersion"])); + } + set { + this["CurrentVersion"] = value; + } + } + } +} diff --git a/TeamsNetphoneLinkWPF/Settings.settings b/TeamsNetphoneLinkWPF/Settings.settings new file mode 100644 index 0000000..87a5c26 --- /dev/null +++ b/TeamsNetphoneLinkWPF/Settings.settings @@ -0,0 +1,27 @@ + + + + + + + + + False + + + + + + + + + False + + + False + + + + + + \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/Syncronisation.cs b/TeamsNetphoneLinkWPF/Syncronisation.cs new file mode 100644 index 0000000..03ab84b --- /dev/null +++ b/TeamsNetphoneLinkWPF/Syncronisation.cs @@ -0,0 +1,273 @@ +using System.ComponentModel; +using System.Windows.Media; +using TeamsLocalLibary.EventArgs; +using TeamsNetphoneLink.Teams; +using TeamsNetphoneLink.Netphone; +using TeamsNetphoneLink.WPF.MVVM; +using TeamsNetphoneLink.Communication; +using System.Reflection; +using TeamsNetphoneLink.WPF; + +namespace TeamsNetphoneLink +{ + public class Syncronisation + { + public NetPhoneEvents NetphoneEvents { get; set; } + public TeamsGraph TeamsGraph { get; set; } + public TeamsLocalAPI TeamsEvents { get; set; } + + //MVVM View Models + public DashboardViewModel _dashboardViewModel { get; } + public LogViewModel LogEntries { get; } + public FinishPanelViewModel FinishPanelViewModel { get; } + + public readonly SemaphoreSlim TeamsGraphAuthenticationSemaphore = new(1); + + private bool _isCallActive = false; + private bool _isMeetingActive = false; + private bool NetphoneConnected = false; + + + public Syncronisation(NetPhoneEvents netPhoneEvents, TeamsLocalAPI teamsEvents, TeamsGraph teamsGraph, DashboardViewModel dashboardViewModel, LogViewModel logEntries, FinishPanelViewModel finishPanelViewModel) + { + _dashboardViewModel = dashboardViewModel ?? throw new ArgumentNullException(nameof(dashboardViewModel)); + LogEntries = logEntries ?? throw new ArgumentNullException(nameof(logEntries)); + FinishPanelViewModel = finishPanelViewModel ?? throw new ArgumentNullException(nameof(finishPanelViewModel)); + NetphoneEvents = netPhoneEvents ?? throw new ArgumentNullException(nameof(netPhoneEvents)); + TeamsEvents = teamsEvents ?? throw new ArgumentNullException(nameof(teamsEvents)); + TeamsGraph = teamsGraph ?? throw new ArgumentNullException(nameof(teamsGraph)); + + // Event-Handler für Netphone-Events registrieren + NetphoneEvents.AddLineStateEventHandler(LineState.Dialing, NetphoneEventCall); + NetphoneEvents.AddLineStateEventHandler(LineState.Ringing, NetphoneEventCall); + NetphoneEvents.AddLineStateEventHandler(LineState.Active, NetphoneEventCall); + NetphoneEvents.AddLineStateEventHandler(LineState.Inactive, NetphoneEventTerminated); + NetphoneEvents.AddLineStateEventHandler(LineState.Terminated, NetphoneEventTerminated); + NetphoneEvents.AddLoggedInStateEventHandler(NetphoneOnLoggedInStateChanged); + + if (!Settings.Default.UseGraphForMeetingState) + { + // Event-Handler für Teams-Events registrieren + TeamsEvents.AddEventHandler("IsInMeeting", TeamsIsInMeetingHandler); + TeamsEvents.AddConnectionStateHandler(TeamsOnConnectionStateChanged); + TeamsEvents.AddTokenRecievedHandler(TeamsTokenRecievedHandler); + } + else + { + TeamsGraph.ActivityChanged += TeamsGraphOnActivityChanged; + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsLocalAPIStatus, Colors.Orange, "Deaktiviert"); + } + } + + // Event-Handler für Änderungen des Verbindungsstatus in Teams + private void TeamsOnConnectionStateChanged(object? sender, ConnectionStateChangedEventArgs args) + { + var status = args.WebSocketState switch + { + System.Net.WebSockets.WebSocketState.Open when Settings.Default.TeamsPermission => (Colors.Green, "Verbunden"), + System.Net.WebSockets.WebSocketState.Open => (Colors.Orange, "Authorisieren (Klicken)"), + System.Net.WebSockets.WebSocketState.Connecting => (Colors.Blue, "Verbinden..."), + System.Net.WebSockets.WebSocketState.Closed => (Colors.Red, "Nicht verbunden"), + _ => (Colors.Gray, "Unbekannt") + }; + + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsLocalAPIStatus, status.Item1, status.Item2); + + if(Settings.Default.TeamsPermission) + LogEntries.AddLogEntry(args.WebSocketState == System.Net.WebSockets.WebSocketState.Open ? "Info" : "Warn", "TeamsLocalAPI", status.Item2); + } + + private void NetphoneOnLoggedInStateChanged(bool isLoggedIn) + { + NetphoneConnected = isLoggedIn; + + var state = isLoggedIn ? ( Colors.Green, "Verbunden" ) : ( Colors.Red, "Nicht verbunden" ); + + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.NetphoneCLMGRStatus, state.Item1, state.Item2); + LogEntries.AddLogEntry(isLoggedIn ? "Info" : "Warn", "Netphone", state.Item2); + } + + // Event-Handler für den Empfang eines Teams-Tokens + private void TeamsTokenRecievedHandler(object sender, TokenReceivedEventArgs e) + { + if (!Settings.Default.TeamsPermission) + { + Settings.Default.TeamsPermission = true; + Settings.Default.Save(); + + FinishPanelViewModel.FinishPanelEffect = null; + FinishPanelViewModel.FinishPanelEnabled = true; + FinishPanelViewModel.FinishPanelFinishTextText = "Verbindung erfolgreich"; + FinishPanelViewModel.SetFinishPanelFinishTextColor(Colors.Green); + + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsLocalAPIStatus, Colors.Green, "Verbunden"); + LogEntries.AddLogEntry("TeamsLocalAPI", "Token erfolgreich abgerufen"); + } + } + + // Event-Handler für eingehende Anrufe + private async void NetphoneEventCall(LineState newLineState) + { + if (_isCallActive) return; + + _isCallActive = true; + await TeamsGraph.SetPresenceAsync(TeamsGraph.PresenceState.Busy); + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.NetphoneCallStatus, Colors.DarkGreen, "Ongoing"); + LogEntries.AddLogEntry("Syncronisation", "Netphone Anruf gestartet"); + } + + // Event-Handler für beendete Anrufe + private async void NetphoneEventTerminated(LineState newLineState) + { + if (!_isCallActive) return; + + _isCallActive = false; + await TeamsGraph.SetPresenceAsync(TeamsGraph.PresenceState.Available); + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.NetphoneCallStatus, Colors.DarkOrange, "Not active"); + LogEntries.AddLogEntry("Syncronisation", "Netphone Anruf gestoppt"); + } + + + private void TeamsGraphOnActivityChanged(object sender, PresenceChangedEventArgs args) + { + // Define activities that indicate the user is in a meeting + var meetingActivities = new HashSet + { + "InACall", + "InAConferenceCall", + "InAMeeting", + "Presenting" + }; + + if(args.Presence.Activity is null) + return; + + // Check if the current activity is in the meetingActivities set + bool isInMeeting = meetingActivities.Contains(args.Presence.Activity); + + + if (!isInMeeting && _isMeetingActive) + { + _isMeetingActive = false; + SetNetphoneStatus(isInMeeting); + return; + } + + if (isInMeeting && !_isMeetingActive) + { + _isMeetingActive = true; + SetNetphoneStatus(isInMeeting); + } + } + + // Event-Handler für Änderungen des "IsInMeeting"-Status in Teams + private void TeamsIsInMeetingHandler(object sender, PropertyChangedEventArgs e) + { + var isInMeeting = (bool)(sender.GetType()?.GetProperty(e.PropertyName)?.GetValue(sender) ?? false); + SetNetphoneStatus(isInMeeting); + } + + private void SetNetphoneStatus(bool meeting) + { + if (meeting) + { + if (NetphoneConnected) + { + NetphoneEvents.SetRichPresenceStatus(0, 1, DateTime.MaxValue); + NetphoneEvents.SetAppointmentText("In einem Teams Meeting", DateTime.MaxValue); + } + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsMeetingStatus, Colors.DarkGreen, "Ongoing"); + LogEntries.AddLogEntry("Syncronisation", "Teams Meeting gestartet"); + } + else + { + if (NetphoneConnected) + { + NetphoneEvents.SetRichPresenceStatus(0, 0, DateTime.MaxValue); + NetphoneEvents.SetAppointmentText("", DateTime.MaxValue); + } + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsMeetingStatus, Colors.DarkOrange, "Not active"); + LogEntries.AddLogEntry("Syncronisation", "Teams Meeting gestoppt"); + } + } + + + + // Methode zur Authentifizierung bei der Graph API + public async Task GraphAuthenticate(bool clearCache = false) + { + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsGraphAPIStatus, Colors.Blue, "Anmeldung läuft..."); + LogEntries.AddLogEntry("TeamsGraphAPI", String.Format("Anmeldung läuft")); + + var authResult = await TeamsGraph.AuthenticateAsync(clearCache); + + var status = authResult ? (Colors.Green, "Angemeldet") : (Colors.Red, "Fehler - Anmelden (Klicken)"); + _dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsGraphAPIStatus, status.Item1, status.Item2); + LogEntries.AddLogEntry("TeamsGraphAPI", status.Item2); + + return authResult; + } + + public async void InitializeNetphoneAsync() + { + try + { + await this.NetphoneEvents.Initialize(waitForNetphone: true).ConfigureAwait(false); + } + catch (Exception ex) + { + // Fehlerbehandlung für die Netphone-Initialisierung + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Fehler bei der Initialisierung von der NetphoneAPI", ex.Message, ex.StackTrace); + } + } + + public async void InitializeTeamsGraphAsync() + { + if (!Teams.TeamsGraph.IsGuid(Settings.Default.AppID) || !Teams.TeamsGraph.IsGuid(Settings.Default.TenantID)) + { + this._dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsGraphAPIStatus, Colors.Red, "Konfiguration fehlt"); + return; + } + + try + { + await this.TeamsGraphAuthenticationSemaphore.WaitAsync().ConfigureAwait(false); + if (await this.TeamsGraph.IsCachedAccounts() && Settings.Default.SaveEntraCredentials) + { + await this.GraphAuthenticate().ConfigureAwait(false); ; + } + else + { + this._dashboardViewModel.UpdateStatusPanel(StatusPanelNames.TeamsGraphAPIStatus, Colors.Orange, "Anmelden"); + } + this.TeamsGraphAuthenticationSemaphore.Release(); + + if (Settings.Default.UseGraphForMeetingState) + { + this.TeamsGraph.CheckPresenceStatusTimer(); + } + } + catch (Exception ex) + { + // Fehlerbehandlung für die TeamsGraph-Initialisierung + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Fehler bei der Initialisierung von TeamsGraph", ex.Message, ex.StackTrace); + } + } + + public async void InitializeTeamsLocalAPIAsync() + { + if (!Settings.Default.UseGraphForMeetingState) + { + try + { + await this.TeamsEvents.Initialize().ConfigureAwait(false); + } + catch (Exception ex) + { + // Fehlerbehandlung für die TeamsLocalAPI-Initialisierung + ExceptionWindowHelper.Show(this.GetType().Name, MethodBase.GetCurrentMethod().Name, "Fehler bei der Initialisierung von der TeamsLocalAPI", ex.Message, ex.StackTrace); + } + } + } + } +} \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/TeamsNetphoneLinkWPF.csproj b/TeamsNetphoneLinkWPF/TeamsNetphoneLinkWPF.csproj new file mode 100644 index 0000000..08e2cd3 --- /dev/null +++ b/TeamsNetphoneLinkWPF/TeamsNetphoneLinkWPF.csproj @@ -0,0 +1,62 @@ + + + + WinExe + net8.0-windows + enable + enable + true + TeamsNetphoneLink + TeamsNetphoneSyncIcon.ico + ..\build\ + False + + + + + + {F8E552F7-4C00-11D3-80BC-00105A653379} + 2 + 0 + 0 + tlbimp + False + False + + + + + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + Code + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + diff --git a/TeamsNetphoneLinkWPF/TeamsNetphoneSyncIcon.ico b/TeamsNetphoneLinkWPF/TeamsNetphoneSyncIcon.ico new file mode 100644 index 0000000..df7009c Binary files /dev/null and b/TeamsNetphoneLinkWPF/TeamsNetphoneSyncIcon.ico differ diff --git a/TeamsNetphoneLinkWPF/UpdateCheck.cs b/TeamsNetphoneLinkWPF/UpdateCheck.cs new file mode 100644 index 0000000..c2a02da --- /dev/null +++ b/TeamsNetphoneLinkWPF/UpdateCheck.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows.Threading; +using TeamsNetphoneLink.WPF; + +namespace TeamsNetphoneLink +{ + public class UpdateCheck + { + public static bool UpdateAvailable; + + public static async Task GetLatestReleaseTagAsync() + { + using (var client = new HttpClient()) + { + var url = "https://git.jan.sx/api/v1/repos/krjan02/TeamsNetphoneLink/releases/latest"; + + try + { + var response = await client.GetStringAsync(url); + + var json = JsonDocument.Parse(response); + + if (json.RootElement.TryGetProperty("tag_name", out var tagName)) + { + return tagName.GetString(); + } + else + { + ExceptionWindowHelper.Show("UpdateCheck", MethodBase.GetCurrentMethod().Name, "Update Check nicht erfolgreich", "tag_name is not existing"); + return ""; + } + } + catch (Exception ex) + { + ExceptionWindowHelper.Show("UpdateCheck", MethodBase.GetCurrentMethod().Name, "Update Check nicht erfolgreich", ex.Message, ex.StackTrace); + return ""; + } + } + } + + public static bool IsVersionLower(string currentVersion, string releaseVersion) + { + if(releaseVersion.Length == 0) { return false; } + + Version current = Version.Parse(currentVersion); + Version release = Version.Parse(releaseVersion); + + return current.CompareTo(release) < 0; + } + + public static string GetCurrentVersion() + { + var assembly = Assembly.GetEntryAssembly(); + var versionAttribute = assembly.GetCustomAttribute(); + var productVersion = versionAttribute?.InformationalVersion; + + return productVersion.Split("+").FirstOrDefault(); ; + } + + public static void UpgradeSettingsIfRequired() + { + var version = GetCurrentVersion(); + if (Settings.Default.CurrentVersion != version) + { + Settings.Default.Upgrade(); + Settings.Default.CurrentVersion = version; + Settings.Default.Save(); + } + } + } +} diff --git a/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml b/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml new file mode 100644 index 0000000..bd2e1aa --- /dev/null +++ b/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml.cs b/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml.cs new file mode 100644 index 0000000..688d411 --- /dev/null +++ b/TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml.cs @@ -0,0 +1,95 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using TeamsNetphoneLink.WPF.MVVM; + +namespace TeamsNetphoneLink.WPF +{ + /// + /// Interaction logic for MainWindow.xaml + /// + /// + public partial class DashboardWindow : Window + { + private bool isDarkMode = true; + private Syncronisation sync; + private Task graphAuthenticationTask; + + public DashboardViewModel _dashboardViewModel { get; } + + public DashboardWindow(Syncronisation _syncronisation) + { + InitializeComponent(); + + sync = _syncronisation; + _dashboardViewModel = sync._dashboardViewModel; + DataContext = _dashboardViewModel; + // Set the ViewModel instance & Set the DataContext to the ViewModel + //Set the DataContext to the ViewModel + Log.DataContext = sync.LogEntries; + } + + private void ToggleDarkMode(object sender, RoutedEventArgs e) + { + isDarkMode = !isDarkMode; + + if (isDarkMode) + { + this.Style = (Style)FindResource("DarkModeStyle"); + } + else + { + this.Style = (Style)FindResource("LightModeStyle"); + } + } + + private void SettingsButton_Click(object sender, RoutedEventArgs e) + { + // Einstellungsfenster öffnen + SettingsWindow settingsWindow = new SettingsWindow(sync); + settingsWindow.ShowDialog(); + } + + private async void TeamsGraphAPIBorder_MouseDown(object sender, MouseButtonEventArgs e) + { + if (!Teams.TeamsGraph.IsGuid(Settings.Default.AppID) || !Teams.TeamsGraph.IsGuid(Settings.Default.TenantID)) + { + SettingsWindow settingsWindow = new SettingsWindow(sync); + settingsWindow.ShowDialog(); + return; + }; + + if (sync.TeamsGraphAuthenticationSemaphore.CurrentCount == 0) + return; + + bool saveCredentials = Settings.Default.SaveEntraCredentials; + if (!sync.TeamsGraph.Authenticated) + { + await sync.TeamsGraphAuthenticationSemaphore.WaitAsync(); + _ = await sync.GraphAuthenticate(saveCredentials); + sync.TeamsGraphAuthenticationSemaphore.Release(); + } + } + + private void TeamsLocalAPIBorder_MouseDown(object sender, MouseButtonEventArgs e) + { + if (!Settings.Default.TeamsPermission) + { + var setupLocalTeams = new WPF.SetupLocalTeamsAPI(sync); + setupLocalTeams.ShowDialog(); + } + } + + private void LogListView_UpdateColumnsWidth(object sender, SizeChangedEventArgs e) + { + ListView _ListView = sender as ListView; + GridView _GridView = _ListView.View as GridView; + var _ActualWidth = _ListView.ActualWidth - SystemParameters.VerticalScrollBarWidth; + for (Int32 i = 1; i < _GridView.Columns.Count; i++) + { + _ActualWidth = _ActualWidth - _GridView.Columns[i].ActualWidth; + } + _GridView.Columns[0].Width = _ActualWidth; + } + } +} \ No newline at end of file diff --git a/TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml b/TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml new file mode 100644 index 0000000..380b7b8 --- /dev/null +++ b/TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +