Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PowerShell store apps #59

Merged
merged 14 commits into from
May 3, 2024
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:

- name: 📝 Code Coverage report
run: |
dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.1.23
reportgenerator -reports:${{github.workspace}}/coverage.cobertura.xml -targetdir:${{github.workspace}}/report -reporttypes:MarkdownSummaryGithub -filefilters:-*.g.cs "-classfilters:-WixSharp.*;-WingetIntune.Os.*;-WingetIntune.Internal.MsStore.*" -verbosity:Warning
dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.2.5
reportgenerator -reports:${{github.workspace}}/coverage.cobertura.xml -targetdir:${{github.workspace}}/report -reporttypes:MarkdownSummaryGithub -filefilters:-*.g.cs "-classfilters:-WixSharp.*;-WingetIntune.Os.*;-WingetIntune.Internal.MsStore.Models.*" -verbosity:Warning
sed -i 's/# Summary/## 📝 Code Coverage/g' ${{github.workspace}}/report/SummaryGithub.md
sed -i 's/## Coverage/### 📝 Code Coverage details/g' ${{github.workspace}}/report/SummaryGithub.md
cat ${{github.workspace}}/report/*.md >> $GITHUB_STEP_SUMMARY
Expand Down
2 changes: 1 addition & 1 deletion WingetIntune.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrooij.WinTuner.CmdLets",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4E745B8F-95AD-42CC-A462-EBA6896413E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Winget.CommunityRepository.Ef", "src\Winget.CommunityRepository.Ef\Winget.CommunityRepository.Ef.csproj", "{516169B3-872B-443B-8736-27E2A08E7091}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Winget.CommunityRepository.Ef", "src\Winget.CommunityRepository.Ef\Winget.CommunityRepository.Ef.csproj", "{516169B3-872B-443B-8736-27E2A08E7091}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{4D1367F6-2076-481E-A7BD-0BFA6BEFCA3D}"
EndProject
Expand Down
93 changes: 93 additions & 0 deletions src/Svrooij.WinTuner.CmdLets/Commands/DeployWtMsStoreApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using Svrooij.PowerShell.DependencyInjection;
using System;
using System.Management.Automation;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using WingetIntune.Graph;
using GraphModels = Microsoft.Graph.Beta.Models;

namespace Svrooij.WinTuner.CmdLets.Commands;
/// <summary>
/// <para type="synopsis">Create a MsStore app in Intune</para>
/// <para type="description">Use this command to create an Microsoft Store app in Microsoft Intune</para>
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/Deploy-WtMsStoreApp">Documentation</para>
/// </summary>
/// <example>
/// <para type="description">Add Firefox to Intune, using interactive authentication</para>
/// <code>Deploy-WtMsStoreApp -PackageId 9NZVDKPMR9RD -Username [email protected]</code>
/// </example>
[Cmdlet(VerbsLifecycle.Deploy, "WtMsStoreApp", DefaultParameterSetName = nameof(PackageId))]
[OutputType(typeof(GraphModels.WinGetApp))]
public class DeployWtMsStoreApp : BaseIntuneCmdlet
{
/// <summary>
/// <para type="description">The package id to upload to Intune.</para>
/// </summary>
[Parameter(
Mandatory = true,
Position = 0,
ParameterSetName = nameof(PackageId),
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "The package id to upload to Intune.")]
public string? PackageId { get; set; }

/// <summary>
/// <para type="description">Name of the app to look for, first match will be created.</para>
/// </summary>
[Parameter(
Mandatory = true,
Position = 0,
ParameterSetName = nameof(SearchQuery),
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = false,
HelpMessage = "Name of the app to look for, first match will be created.")]
public string? SearchQuery { get; set; }

[ServiceDependency]
private ILogger<DeployWtMsStoreApp>? logger;

[ServiceDependency]
private GraphStoreAppUploader? graphStoreAppUploader;

[ServiceDependency]
private HttpClient? httpClient;

/// <inheritdoc/>
public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
{
ValidateAuthenticationParameters();
if (ParameterSetName == nameof(SearchQuery))
{
ArgumentException.ThrowIfNullOrWhiteSpace(SearchQuery);
logger!.LogInformation("Searching package id for {searchQuery}", SearchQuery);
PackageId = await graphStoreAppUploader!.GetStoreIdForNameAsync(SearchQuery!, cancellationToken);
if (string.IsNullOrEmpty(PackageId))
{
logger!.LogError("No package found for {searchQuery}", SearchQuery);
return;
}
}

// At this moment the package ID should always be filled.
ArgumentException.ThrowIfNullOrWhiteSpace(PackageId);

logger!.LogInformation("Uploading MSStore app {PackageId} to Intune", PackageId);
var graphServiceClient = CreateGraphServiceClient(httpClient!);
try
{
var app = await graphStoreAppUploader!.CreateStoreAppAsync(graphServiceClient, PackageId, cancellationToken);

logger!.LogInformation("Created MSStore app {PackageId} with id {appId}", PackageId, app!.Id);
WriteObject(app);
}
catch (Exception ex)
{
logger!.LogError(ex, "Error creating MSStore app {PackageId}", PackageId);
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public override async Task ProcessRecordAsync(CancellationToken cancellationToke
if (Categories is not null && Categories.Any())
{
logger?.LogInformation("Adding categories to app {appId}", AppId);
await graphServiceClient.AddIntuneCategoriesToApp(AppId!, Categories, cancellationToken);
await graphServiceClient.AddIntuneCategoriesToAppAsync(AppId!, Categories, cancellationToken);
}

if ((AvailableFor is not null && AvailableFor.Any()) ||
Expand Down
5 changes: 0 additions & 5 deletions src/WingetIntune/Graph/GraphAppUploader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
using Microsoft.Graph.Beta;
using Microsoft.Graph.Beta.Models;
using Microsoft.Graph.Beta.Models.ODataErrors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WingetIntune.Intune;
using WingetIntune.Models;

Expand Down
96 changes: 96 additions & 0 deletions src/WingetIntune/Graph/GraphStoreAppUploader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.Extensions.Logging;
using Microsoft.Graph.Beta;
using Microsoft.Graph.Beta.Models;
using Microsoft.Graph.Beta.Models.ODataErrors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WingetIntune.Models;

namespace WingetIntune.Graph;
public class GraphStoreAppUploader
{
private readonly ILogger<GraphStoreAppUploader> logger;
private readonly IFileManager fileManager;
private readonly Internal.MsStore.MicrosoftStoreClient microsoftStoreClient;
private readonly Mapper mapper = new();

public GraphStoreAppUploader(ILogger<GraphStoreAppUploader> logger, IFileManager fileManager, Internal.MsStore.MicrosoftStoreClient microsoftStoreClient)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(fileManager);
ArgumentNullException.ThrowIfNull(microsoftStoreClient);
this.logger = logger;
this.fileManager = fileManager;
this.microsoftStoreClient = microsoftStoreClient;
}

public Task<string?> GetStoreIdForNameAsync(string searchstring, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(searchstring);
return microsoftStoreClient.GetPackageIdForFirstMatchAsync(searchstring, cancellationToken);
}

public async Task<WinGetApp?> CreateStoreAppAsync(GraphServiceClient graphServiceClient, string packageId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(graphServiceClient);
ArgumentException.ThrowIfNullOrEmpty(packageId);
ArgumentNullException.ThrowIfNull(cancellationToken);

var catalog = await microsoftStoreClient.GetDisplayCatalogAsync(packageId!, cancellationToken);
ArgumentNullException.ThrowIfNull(catalog);
if (!(catalog.Products?.Count() > 0))
{
logger.LogError("No products found for {packageId}", packageId);
return null;
}

var app = mapper.ToWinGetApp(catalog!);

try
{
var imagePath = Path.GetTempFileName();
var uriPart = catalog.Products.First()?.LocalizedProperties.FirstOrDefault()?.Images?.FirstOrDefault(i => i.Height == 300 && i.Width == 300)?.Uri; // && i.ImagePurpose.Equals("Tile", StringComparison.OrdinalIgnoreCase)
if (uriPart is null)
{
logger.LogWarning("No image found for {packageId}", packageId);
}
else
{
var imageUrl = $"http:{uriPart}";
await fileManager.DownloadFileAsync(imageUrl, imagePath, overrideFile: true, cancellationToken: cancellationToken);
app.LargeIcon = new MimeContent
{
Type = "image/png",
Value = await fileManager.ReadAllBytesAsync(imagePath, cancellationToken)
};
}

}
catch (Exception ex)
{
logger.LogError(ex, "Error downloading image for {packageId}", packageId);
}

logger.LogInformation("Creating new WinGetApp (MsStore) for {packageId}", packageId);

try
{
var createdApp = await graphServiceClient.DeviceAppManagement.MobileApps.PostAsync(app, cancellationToken);
logger.LogInformation("MsStore app {packageIdentifier} created in Intune {appId}", createdApp?.PackageIdentifier, createdApp?.Id);
return createdApp;
}
catch (ODataError ex)
{
logger.LogError(ex, "Error publishing app {message}", ex.Error?.Message);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Error publishing app");
throw;
}
}
}
2 changes: 1 addition & 1 deletion src/WingetIntune/Graph/GraphWorkflows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace WingetIntune.Graph;

public static class GraphWorkflows
{
public static async Task AddIntuneCategoriesToApp(this GraphServiceClient graphServiceClient, string appId, string[] categories, CancellationToken cancellationToken)
public static async Task AddIntuneCategoriesToAppAsync(this GraphServiceClient graphServiceClient, string appId, string[] categories, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(graphServiceClient);
ArgumentException.ThrowIfNullOrEmpty(appId);
Expand Down
Loading