From 724c410f36c1675be8f4618fb517daf55ec32365 Mon Sep 17 00:00:00 2001 From: krjan02 Date: Tue, 25 Mar 2025 22:43:13 +0100 Subject: [PATCH] Added Main Project --- .gitignore | 454 ++++++++++++++++++ TeamsNetphoneLink.sln | 37 ++ TeamsNetphoneLinkWPF/App.config | 63 +++ TeamsNetphoneLinkWPF/App.xaml | 9 + TeamsNetphoneLinkWPF/App.xaml.cs | 79 +++ TeamsNetphoneLinkWPF/AssemblyInfo.cs | 10 + .../Communication/ClientSdkEvents.cs | 121 +++++ .../Communication/NetPhone.cs | 200 ++++++++ .../Communication/TeamsGraph.cs | 290 +++++++++++ .../Communication/TeamsGraphEventHandlers.cs | 97 ++++ .../Communication/TeamsLocalAPI.cs | 118 +++++ TeamsNetphoneLinkWPF/Settings.Designer.cs | 110 +++++ TeamsNetphoneLinkWPF/Settings.settings | 27 ++ TeamsNetphoneLinkWPF/Syncronisation.cs | 273 +++++++++++ .../TeamsNetphoneLinkWPF.csproj | 62 +++ .../TeamsNetphoneSyncIcon.ico | Bin 0 -> 11949 bytes TeamsNetphoneLinkWPF/UpdateCheck.cs | 79 +++ TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml | 263 ++++++++++ .../WPF/DashboardWindow.xaml.cs | 95 ++++ TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml | 123 +++++ .../WPF/ExceptionWindow.xaml.cs | 67 +++ .../WPF/MVVM/DashboardViewModel.cs | 250 ++++++++++ .../WPF/MVVM/FinishPanelViewModel.cs | 94 ++++ TeamsNetphoneLinkWPF/WPF/MVVM/LogViewModel.cs | 75 +++ TeamsNetphoneLinkWPF/WPF/SettingsWindow.xaml | 143 ++++++ .../WPF/SettingsWindow.xaml.cs | 132 +++++ .../WPF/SetupLocalTeamsAPI.xaml | 83 ++++ .../WPF/SetupLocalTeamsAPI.xaml.cs | 83 ++++ TeamsNetphoneLinkWPF/WPF/UpdateWindow.xaml | 183 +++++++ TeamsNetphoneLinkWPF/WPF/UpdateWindow.xaml.cs | 229 +++++++++ 30 files changed, 3849 insertions(+) create mode 100644 .gitignore create mode 100644 TeamsNetphoneLink.sln create mode 100644 TeamsNetphoneLinkWPF/App.config create mode 100644 TeamsNetphoneLinkWPF/App.xaml create mode 100644 TeamsNetphoneLinkWPF/App.xaml.cs create mode 100644 TeamsNetphoneLinkWPF/AssemblyInfo.cs create mode 100644 TeamsNetphoneLinkWPF/Communication/ClientSdkEvents.cs create mode 100644 TeamsNetphoneLinkWPF/Communication/NetPhone.cs create mode 100644 TeamsNetphoneLinkWPF/Communication/TeamsGraph.cs create mode 100644 TeamsNetphoneLinkWPF/Communication/TeamsGraphEventHandlers.cs create mode 100644 TeamsNetphoneLinkWPF/Communication/TeamsLocalAPI.cs create mode 100644 TeamsNetphoneLinkWPF/Settings.Designer.cs create mode 100644 TeamsNetphoneLinkWPF/Settings.settings create mode 100644 TeamsNetphoneLinkWPF/Syncronisation.cs create mode 100644 TeamsNetphoneLinkWPF/TeamsNetphoneLinkWPF.csproj create mode 100644 TeamsNetphoneLinkWPF/TeamsNetphoneSyncIcon.ico create mode 100644 TeamsNetphoneLinkWPF/UpdateCheck.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml create mode 100644 TeamsNetphoneLinkWPF/WPF/DashboardWindow.xaml.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml create mode 100644 TeamsNetphoneLinkWPF/WPF/ExceptionWindow.xaml.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/MVVM/DashboardViewModel.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/MVVM/FinishPanelViewModel.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/MVVM/LogViewModel.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/SettingsWindow.xaml create mode 100644 TeamsNetphoneLinkWPF/WPF/SettingsWindow.xaml.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/SetupLocalTeamsAPI.xaml create mode 100644 TeamsNetphoneLinkWPF/WPF/SetupLocalTeamsAPI.xaml.cs create mode 100644 TeamsNetphoneLinkWPF/WPF/UpdateWindow.xaml create mode 100644 TeamsNetphoneLinkWPF/WPF/UpdateWindow.xaml.cs 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 0000000000000000000000000000000000000000..df7009c7144082da85ef6c19486149d3d82b77d0 GIT binary patch literal 11949 zcmW++1ymH@7oJ@fSaRvEC8Sdc$)%BQkX%ZTMoFn%8eB?B1rd-CkdjuqTv1^`5F`Wy z0g=w7|NQ5WofGi2(q^O*`4xP@963h4iLLaZg9X z^uMqFT~MN%o86@ay_cT<^Lua;L5nE?l{rF^357!McxZ|^Y z>*wLIo2*-7zJW~g!y$$&56lovI4B%0ARtu2#qg3n+)vVUlOB}Z`JLH(lfI!~ryyi| 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 F{s%9e&jbJf literal 0 HcmV?d00001 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +