diff --git a/README.md b/README.md index 0389d58..25a80d2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ dotnet add package Den.Dev.Grunt ```csharp HaloInfiniteClient client = new("", clearanceToken: ""); -var matchStats = await client.Stats.GetMatchStats("21416434-4717-4966-9902-af7097469f74"); +var matchStats = await client.Stats.GetMatchStatsAsync("21416434-4717-4966-9902-af7097469f74"); Console.WriteLine("Match data retrieved!"); ``` diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Config/endpoint-test-config.json b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Config/endpoint-test-config.json index bb15010..1f0f8f3 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Config/endpoint-test-config.json +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Config/endpoint-test-config.json @@ -1,10 +1,15 @@ { - "skipHttpMethods": ["PUT", "POST", "DELETE", "PATCH"], + "skipHttpMethods": [ + "PUT", + "POST", + "DELETE", + "PATCH" + ], "discoveryChain": [ { "step": 1, "endpointId": "Settings_GetClearance", - "method": "Settings.GetClearance", + "method": "Settings.GetClearanceAsync", "args": { "audience": "RETAIL", "sandbox": "UNUSED", @@ -18,7 +23,7 @@ { "step": 2, "endpointId": "Stats_GetMatchHistory", - "method": "Stats.GetMatchHistory", + "method": "Stats.GetMatchHistoryAsync", "args": { "player": "$playerXuid", "start": 0, @@ -34,331 +39,418 @@ "validationTargets": [ { "endpointId": "Academy_GetBotCustomization", - "method": "Academy.GetBotCustomization", - "args": { "flightId": "$flightId" }, + "method": "Academy.GetBotCustomizationAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "BotCustomizationData" }, { "endpointId": "Academy_GetContent", - "method": "Academy.GetContent", + "method": "Academy.GetContentAsync", "args": {}, "expectedModel": "AcademyClientManifest" }, { "endpointId": "Academy_GetContentTest", - "method": "Academy.GetContentTest", - "args": { "clearanceId": "$flightId" }, + "method": "Academy.GetContentTestAsync", + "args": { + "clearanceId": "$flightId" + }, "expectedModel": "TestAcademyClientManifest" }, { "endpointId": "Academy_GetStarDefinitions", - "method": "Academy.GetStarDefinitions", + "method": "Academy.GetStarDefinitionsAsync", "args": {}, "expectedModel": "AcademyStarDefinitions" }, { "endpointId": "BanProcessor_BanSummary", - "method": "BanProcessor.BanSummary", - "args": { "targetlist": ["$playerXuid"] }, + "method": "BanProcessor.GetBanSummaryAsync", + "args": { + "targetlist": [ + "$playerXuid" + ] + }, "expectedModel": "BansSummaryQueryResult" }, { "endpointId": "Configuration_GetApiSettingsContainer", - "method": "Configuration.GetApiSettingsContainer", + "method": "Configuration.GetApiSettingsContainerAsync", "args": {}, "expectedModel": "Configuration" }, { "endpointId": "Economy_AiCoresCustomization", - "method": "Economy.AiCoresCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetAiCoresCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "AiCoreContainer" }, { "endpointId": "Economy_AllOwnedCoresDetails", - "method": "Economy.AllOwnedCoresDetails", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetAllOwnedCoresDetailsAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "PlayerCores" }, { "endpointId": "Economy_ArmorCoresCustomization", - "method": "Economy.ArmorCoresCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetArmorCoresCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "ArmorCoreCollection" }, { "endpointId": "Economy_GetActiveBoosts", - "method": "Economy.GetActiveBoosts", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetActiveBoostsAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "ActiveBoostsContainer" }, { "endpointId": "Economy_GetBoostsStore", - "method": "Economy.GetBoostsStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetBoostsStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetSoftCurrencyStore", - "method": "Economy.GetSoftCurrencyStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetSoftCurrencyStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetGiveawayRewards", - "method": "Economy.GetGiveawayRewards", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetGiveawayRewardsAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "PlayerGiveaways" }, { "endpointId": "Economy_GetHCSStore", - "method": "Economy.GetHCSStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetHCSStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetInventoryItems", - "method": "Economy.GetInventoryItems", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetInventoryItemsAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "PlayerInventory" }, { "endpointId": "Economy_GetMainStore", - "method": "Economy.GetMainStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetMainStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetMultiplePlayersCustomization", - "method": "Economy.GetMultiplePlayersCustomization", - "args": { "playerIds": ["$playerXuid"] }, + "method": "Economy.GetMultiplePlayersCustomizationAsync", + "args": { + "playerIds": [ + "$playerXuid" + ] + }, "expectedModel": "PlayerCustomizationCollection" }, { "endpointId": "Economy_GetOperationRewardLevelsStore", - "method": "Economy.GetOperationRewardLevelsStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetOperationRewardLevelsStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetOperationsStore", - "method": "Economy.GetOperationsStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetOperationsStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetVirtualCurrencyBalances", - "method": "Economy.GetVirtualCurrencyBalances", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetVirtualCurrencyBalancesAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "CurrencySnapshot" }, { "endpointId": "Economy_GetXpGrantsStore", - "method": "Economy.GetXpGrantsStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetXpGrantsStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_PlayerAppearanceCustomization", - "method": "Economy.PlayerAppearanceCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetPlayerAppearanceCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "AppearanceCustomization" }, { "endpointId": "Economy_PlayerCustomization", - "method": "Economy.PlayerCustomization", - "args": { "player": "$playerXuid", "viewType": "public" }, + "method": "Economy.GetPlayerCustomizationAsync", + "args": { + "player": "$playerXuid", + "viewType": "public" + }, "expectedModel": "CustomizationData" }, { "endpointId": "Economy_PlayerOperations", - "method": "Economy.PlayerOperations", - "args": { "player": "$playerXuid", "flightId": "$flightId" }, + "method": "Economy.GetPlayerOperationsAsync", + "args": { + "player": "$playerXuid", + "flightId": "$flightId" + }, "expectedModel": "OperationRewardTrackSnapshot" }, { "endpointId": "Economy_SpartanBodyCustomization", - "method": "Economy.SpartanBodyCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetSpartanBodyCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "SpartanBody" }, { "endpointId": "Economy_VehicleCoresCustomization", - "method": "Economy.VehicleCoresCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetVehicleCoresCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "VehicleCoreCollection" }, { "endpointId": "Economy_WeaponCoresCustomization", - "method": "Economy.WeaponCoresCustomization", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetWeaponCoresCustomizationAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "WeaponCoreCollection" }, { "endpointId": "Economy_GetCustomizationStore", - "method": "Economy.GetCustomizationStore", - "args": { "player": "$playerXuid" }, + "method": "Economy.GetCustomizationStoreAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "StoreItem" }, { "endpointId": "Economy_GetPlayerCareerRank", - "method": "Economy.GetPlayerCareerRank", - "args": { "players": ["$playerXuid"], "careerPathId": "careerRank1" }, + "method": "Economy.GetPlayerCareerRankAsync", + "args": { + "players": [ + "$playerXuid" + ], + "careerPathId": "careerRank1" + }, "expectedModel": "RewardTrackResultContainer" }, { "endpointId": "GameCms_GetPlayNowButtonSettings", - "method": "GameCms.GetPlayNowButtonSettings", + "method": "GameCms.GetPlayNowButtonSettingsAsync", "args": {}, "expectedModel": "FallbackPlaylist" }, { "endpointId": "GameCms_GetAchievements", - "method": "GameCms.GetAchievements", + "method": "GameCms.GetAchievementsAsync", "args": {}, "expectedModel": "AchievementCollection" }, { "endpointId": "GameCms_GetAsyncComputeOverrides", - "method": "GameCms.GetAsyncComputeOverrides", + "method": "GameCms.GetAsyncComputeOverridesAsync", "args": {}, "expectedModel": "AsyncComputeOverrides" }, { "endpointId": "GameCms_GetClawAccess", - "method": "GameCms.GetClawAccess", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetClawAccessAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "ClawAccessSnapshot" }, { "endpointId": "GameCms_GetCpuPresets", - "method": "GameCms.GetCpuPresets", + "method": "GameCms.GetCpuPresetsAsync", "args": {}, "expectedModel": "CPUPresetSnapshot" }, { "endpointId": "GameCms_GetCustomGameDefaults", - "method": "GameCms.GetCustomGameDefaults", + "method": "GameCms.GetCustomGameDefaultsAsync", "args": {}, "expectedModel": "CustomGameDefinition" }, { "endpointId": "GameCms_GetCustomizationCatalog", - "method": "GameCms.GetCustomizationCatalog", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetCustomizationCatalogAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "InventoryDefinition" }, { "endpointId": "GameCms_GetDevicePresetOverrides", - "method": "GameCms.GetDevicePresetOverrides", + "method": "GameCms.GetDevicePresetOverridesAsync", "args": {}, "expectedModel": "DevicePresetOverrides" }, { "endpointId": "GameCms_GetGraphicsSpecControlOverrides", - "method": "GameCms.GetGraphicsSpecControlOverrides", + "method": "GameCms.GetGraphicsSpecControlOverridesAsync", "args": {}, "expectedModel": "OverrideQueryDefinition" }, { "endpointId": "GameCms_GetLobbyErrorMessages", - "method": "GameCms.GetLobbyErrorMessages", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetLobbyErrorMessagesAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "LobbyHopperErrorMessageList" }, { "endpointId": "GameCms_GetMetadata", - "method": "GameCms.GetMetadata", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetMetadataAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "Metadata" }, { "endpointId": "GameCms_GetNetworkConfiguration", - "method": "GameCms.GetNetworkConfiguration", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetNetworkConfigurationAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "NetworkConfiguration" }, { "endpointId": "GameCms_GetNotAllowedInTitleMessage", - "method": "GameCms.GetNotAllowedInTitleMessage", + "method": "GameCms.GetNotAllowedInTitleMessageAsync", "args": {}, "expectedModel": "OEConfiguration" }, { "endpointId": "GameCms_GetRecommendedDrivers", - "method": "GameCms.GetRecommendedDrivers", + "method": "GameCms.GetRecommendedDriversAsync", "args": {}, "expectedModel": "DriverManifest" }, { "endpointId": "GameCms_GetCareerRanks", - "method": "GameCms.GetCareerRanks", - "args": { "careerPathId": "careerRank1" }, + "method": "GameCms.GetCareerRanksAsync", + "args": { + "careerPathId": "careerRank1" + }, "expectedModel": "CareerTrackContainer" }, { "endpointId": "GameCms_GetSeasonCalendar", - "method": "GameCms.GetSeasonCalendar", + "method": "GameCms.GetSeasonCalendarAsync", "args": {}, "expectedModel": "SeasonCalendar" }, { "endpointId": "GameCms_GetCSRCalendar", - "method": "GameCms.GetCSRCalendar", + "method": "GameCms.GetCSRCalendarAsync", "args": {}, "expectedModel": "SeasonCalendar" }, { "endpointId": "GameCms_GetGuideImages", - "method": "GameCms.GetGuideImages", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideImagesAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetGuideMultiplayer", - "method": "GameCms.GetGuideMultiplayer", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideMultiplayerAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetGuideNews", - "method": "GameCms.GetGuideNews", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideNewsAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetGuideProgression", - "method": "GameCms.GetGuideProgression", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideProgressionAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetGuideSpecs", - "method": "GameCms.GetGuideSpecs", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideSpecsAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetGuideTitleAuthorization", - "method": "GameCms.GetGuideTitleAuthorization", - "args": { "flightId": "$flightId" }, + "method": "GameCms.GetGuideTitleAuthorizationAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "GuideContainer" }, { "endpointId": "GameCms_GetMedalMetadata", - "method": "GameCms.GetMedalMetadata", + "method": "GameCms.GetMedalMetadataAsync", "args": {}, "expectedModel": "MedalMetadata" }, { "endpointId": "Settings_GetFlightedFeatureFlags", - "method": "Settings.GetFlightedFeatureFlags", - "args": { "flightId": "$flightId" }, + "method": "Settings.GetFlightedFeatureFlagsAsync", + "args": { + "flightId": "$flightId" + }, "expectedModel": "FlightedFeatureFlags" }, { "endpointId": "Settings_GetClearance", - "method": "Settings.GetClearance", + "method": "Settings.GetClearanceAsync", "args": { "audience": "RETAIL", "sandbox": "UNUSED", @@ -369,122 +461,161 @@ }, { "endpointId": "Skill_GetMatchPlayerResult", - "method": "Skill.GetMatchPlayerResult", - "args": { "matchId": "$matchId", "playerIds": ["$playerXuid"] }, + "method": "Skill.GetMatchPlayerResultAsync", + "args": { + "matchId": "$matchId", + "playerIds": [ + "$playerXuid" + ] + }, "expectedModel": "MatchSkillInfo" }, { "endpointId": "Skill_GetPlaylistCsr", - "method": "Skill.GetPlaylistCsr", - "args": { "playlistId": "edfef3ac-9cbe-4fa2-b949-8f29deafd483", "playerIds": ["$playerXuid"] }, + "method": "Skill.GetPlaylistCsrAsync", + "args": { + "playlistId": "edfef3ac-9cbe-4fa2-b949-8f29deafd483", + "playerIds": [ + "$playerXuid" + ] + }, "expectedModel": "PlaylistCsrResultContainer" }, { "endpointId": "Stats_GetChallengeDecks", - "method": "Stats.GetChallengeDecks", - "args": { "player": "$playerXuid" }, + "method": "Stats.GetChallengeDecksAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "ChallengeDecksResponse" }, { "endpointId": "Stats_GetMatchCount", - "method": "Stats.GetMatchCount", - "args": { "player": "$playerXuid" }, + "method": "Stats.GetMatchCountAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "PlayerMatchCount" }, { "endpointId": "Stats_GetMatchHistory", - "method": "Stats.GetMatchHistory", - "args": { "player": "$playerXuid", "start": 0, "count": 5, "type": "All" }, + "method": "Stats.GetMatchHistoryAsync", + "args": { + "player": "$playerXuid", + "start": 0, + "count": 5, + "type": "All" + }, "expectedModel": "MatchHistoryResponse" }, { "endpointId": "Stats_GetMatchStats", - "method": "Stats.GetMatchStats", - "args": { "matchId": "$matchId" }, + "method": "Stats.GetMatchStatsAsync", + "args": { + "matchId": "$matchId" + }, "expectedModel": "MatchStats" }, { "endpointId": "Stats_GetPlayerMatchProgression", - "method": "Stats.GetPlayerMatchProgression", - "args": { "player": "$playerXuid", "matchId": "$matchId" }, + "method": "Stats.GetPlayerMatchProgressionAsync", + "args": { + "player": "$playerXuid", + "matchId": "$matchId" + }, "expectedModel": "MatchProgression" }, { "endpointId": "Stats_MatchPrivacy", - "method": "Stats.MatchPrivacy", - "args": { "player": "$playerXuid" }, + "method": "Stats.GetMatchPrivacyAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "MatchesPrivacy" }, { "endpointId": "Stats_GetPlayerServiceRecord", - "method": "Stats.GetPlayerServiceRecord", - "args": { "playerId": "$playerXuid", "mode": "Matchmade" }, + "method": "Stats.GetPlayerServiceRecordByXuidAsync", + "args": { + "playerId": "$playerXuid", + "mode": "Matchmade" + }, "expectedModel": "PlayerServiceRecord" }, { "endpointId": "Stats_GetPlayerDailyCustomExperience", - "method": "Stats.GetPlayerDailyCustomExperience", - "args": { "player": "$playerXuid" }, + "method": "Stats.GetPlayerDailyCustomExperienceAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "PlayerDailyCustomExperience" }, { "endpointId": "TextModeration_GetSigningKeys", - "method": "TextModeration.GetSigningKeys", + "method": "TextModeration.GetSigningKeysAsync", "args": {}, "expectedModel": "ModerationProofKeys" }, { "endpointId": "UgcDiscovery_GetForgeTemplates", - "method": "UgcDiscovery.GetForgeTemplates", + "method": "UgcDiscovery.GetForgeTemplatesAsync", "args": {}, "expectedModel": "Project" }, { "endpointId": "UgcDiscovery_GetForgeModeCategories", - "method": "UgcDiscovery.GetForgeModeCategories", + "method": "UgcDiscovery.GetForgeModeCategoriesAsync", "args": {}, "expectedModel": "Project" }, { "endpointId": "UgcDiscovery_GetCommunityTab", - "method": "UgcDiscovery.GetCommunityTab", + "method": "UgcDiscovery.GetCommunityTabAsync", "args": {}, "expectedModel": "Project" }, { "endpointId": "UgcDiscovery_Get343Recommended", - "method": "UgcDiscovery.Get343Recommended", + "method": "UgcDiscovery.Get343RecommendedAsync", "args": {}, "expectedModel": "Project" }, { "endpointId": "UgcDiscovery_GetFilm", - "method": "UgcDiscovery.GetFilm", - "args": { "assetId": "$assetId" }, + "method": "UgcDiscovery.GetFilmAsync", + "args": { + "assetId": "$assetId" + }, "expectedModel": "Film" }, { "endpointId": "UgcDiscovery_GetMapWithoutVersion", - "method": "UgcDiscovery.GetMapWithoutVersion", - "args": { "assetId": "$mapAssetId" }, + "method": "UgcDiscovery.GetMapWithoutVersionAsync", + "args": { + "assetId": "$mapAssetId" + }, "expectedModel": "Map" }, { "endpointId": "UgcDiscovery_GetTagsInfo", - "method": "UgcDiscovery.GetTagsInfo", + "method": "UgcDiscovery.GetTagsInfoAsync", "args": {}, "expectedModel": "TagInfo" }, { "endpointId": "UgcDiscovery_SpectateByMatchId", - "method": "UgcDiscovery.SpectateByMatchId", - "args": { "matchId": "$matchId" }, + "method": "UgcDiscovery.SpectateByMatchIdAsync", + "args": { + "matchId": "$matchId" + }, "expectedModel": "Film" }, { "endpointId": "Ugc_ListPlayerFavoritesAgnostic", - "method": "Ugc.ListPlayerFavoritesAgnostic", - "args": { "player": "$playerXuid" }, + "method": "Ugc.ListPlayerFavoritesAgnosticAsync", + "args": { + "player": "$playerXuid" + }, "expectedModel": "AuthoringFavoritesContainer" } ], diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/AuthenticationManager.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/AuthenticationManager.cs index 4468352..59d0d3e 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/AuthenticationManager.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/AuthenticationManager.cs @@ -132,7 +132,7 @@ await AnsiConsole.Status() return; } - spartanToken = await _haloAuthClient.GetSpartanToken(xstsToken); + spartanToken = await _haloAuthClient.GetSpartanTokenAsync(xstsToken); if (spartanToken == null) { return; @@ -149,7 +149,7 @@ await AnsiConsole.Status() ctx.Status("[blue]Obtaining clearance...[/]"); try { - var clearance = (await Client.Settings.GetClearance("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13")).Result; + var clearance = (await Client.Settings.GetClearanceAsync("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13")).Result; if (clearance != null) { ClearanceToken = clearance.FlightConfigurationId ?? string.Empty; diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/ParameterDiscovery.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/ParameterDiscovery.cs index 65926ab..1156ed8 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/ParameterDiscovery.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Auditor/Services/ParameterDiscovery.cs @@ -109,7 +109,7 @@ private async Task RunBuiltInDiscoveryAsync() AnsiConsole.MarkupLine(" [dim]Discovering match IDs from match history...[/]"); try { - var historyResult = await _client.Stats.GetMatchHistory( + var historyResult = await _client.Stats.GetMatchHistoryAsync( _registry.Parameters.PlayerXuid, 0, 10, @@ -140,7 +140,7 @@ private async Task RunBuiltInDiscoveryAsync() AnsiConsole.MarkupLine(" [dim]Discovering flight ID from clearance...[/]"); try { - var clearanceResult = await _client.Settings.GetClearance("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13"); + var clearanceResult = await _client.Settings.GetClearanceAsync("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13"); if (clearanceResult?.Result?.FlightConfigurationId != null) { @@ -161,7 +161,7 @@ private async Task RunBuiltInDiscoveryAsync() try { var matchId = _registry.Parameters.MatchIds[0]; - var matchResult = await _client.Stats.GetMatchStats(matchId); + var matchResult = await _client.Stats.GetMatchStatsAsync(matchId); if (matchResult?.Result?.MatchInfo != null) { diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Composer/Program.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Composer/Program.cs index c6111cd..7335d7c 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Composer/Program.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Composer/Program.cs @@ -210,7 +210,7 @@ private static void ProcessUncertainAssetData(List? assets, AssetClas { Task.Run(async () => { - var container = await haloInfiniteClient.UgcDiscovery.GetMap(asset.AssetId.ToString(), asset.VersionId.ToString()); + var container = await haloInfiniteClient.UgcDiscovery.GetMapAsync(asset.AssetId.ToString(), asset.VersionId.ToString()); var buildInsertionString = $"INSERT OR REPLACE INTO MapMetadata (ResponseBody, SnapshotTimestamp) VALUES(?, ?)"; domain.Execute(buildInsertionString, new string[] { container.Response.Message, DateTime.Now.ToString("o", CultureInfo.InvariantCulture) }); @@ -220,7 +220,7 @@ private static void ProcessUncertainAssetData(List? assets, AssetClas { Task.Run(async () => { - var container = await haloInfiniteClient.UgcDiscovery.GetEngineGameVariant(asset.AssetId.ToString(), asset.VersionId.ToString()); + var container = await haloInfiniteClient.UgcDiscovery.GetEngineGameVariantAsync(asset.AssetId.ToString(), asset.VersionId.ToString()); var buildInsertionString = $"INSERT OR REPLACE INTO EngineGameVariantMetadata (ResponseBody, SnapshotTimestamp) VALUES(?, ?)"; domain.Execute(buildInsertionString, new string[] { container.Response.Message, DateTime.Now.ToString("o", CultureInfo.InvariantCulture) }); }).GetAwaiter().GetResult(); @@ -229,7 +229,7 @@ private static void ProcessUncertainAssetData(List? assets, AssetClas { Task.Run(async () => { - var container = await haloInfiniteClient.UgcDiscovery.GetUgcGameVariant(asset.AssetId.ToString(), asset.VersionId.ToString()); + var container = await haloInfiniteClient.UgcDiscovery.GetUgcGameVariantAsync(asset.AssetId.ToString(), asset.VersionId.ToString()); var buildInsertionString = $"INSERT OR REPLACE INTO UgcGameVariantMetadata (ResponseBody, SnapshotTimestamp) VALUES(?, ?)"; domain.Execute(buildInsertionString, new string[] { container.Response.Message, DateTime.Now.ToString("o", CultureInfo.InvariantCulture) }); }).GetAwaiter().GetResult(); @@ -246,13 +246,13 @@ private static async Task BuildStatsCommandHandler(string buildId, string var domainDatabase = new SQLiteConnection(domain); - var buildData = await haloInfiniteClient!.UgcDiscovery.GetManifestByBuildGuid(buildId); + var buildData = await haloInfiniteClient!.UgcDiscovery.GetManifestByBuildGuidAsync(buildId); if (buildData.Response!.Code == 401) { // The token is no longer working - need to acquire a new one. WriteTimedLogEntry("Token expired. Refreshing..."); haloInfiniteClient = InstantiateClient(); - buildData = await haloInfiniteClient!.UgcDiscovery.GetManifestByBuildGuid(buildId); + buildData = await haloInfiniteClient!.UgcDiscovery.GetManifestByBuildGuidAsync(buildId); } if (buildData != null && buildData.Result != null) @@ -282,13 +282,13 @@ private static async Task ProjectStatsCommandHandler(string projectId, str var domainDatabase = new SQLiteConnection(domain); - var projectData = await haloInfiniteClient!.UgcDiscovery.GetProjectWithoutVersion(projectId); + var projectData = await haloInfiniteClient!.UgcDiscovery.GetProjectWithoutVersionAsync(projectId); if (projectData.Response!.Code == 401) { // The token is no longer working - need to acquire a new one. WriteTimedLogEntry("Token expired. Refreshing..."); haloInfiniteClient = InstantiateClient(); - projectData = await haloInfiniteClient!.UgcDiscovery.GetProjectWithoutVersion(projectId); + projectData = await haloInfiniteClient!.UgcDiscovery.GetProjectWithoutVersionAsync(projectId); } if (projectData != null && projectData.Result != null) @@ -340,13 +340,13 @@ private static async Task RankSnapshotCommandHandler(string playlistId, bo var domainDatabase = new SQLiteConnection(domain); - var rankData = await haloInfiniteClient!.Skill.GetPlaylistCsr(playlistId, playerXuids.ToList()); + var rankData = await haloInfiniteClient!.Skill.GetPlaylistCsrAsync(playlistId, playerXuids.ToList()); if (rankData.Response!.Code == 401) { // The token is no longer working - need to acquire a new one. WriteTimedLogEntry("Token expired. Refreshing..."); haloInfiniteClient = InstantiateClient(); - rankData = await haloInfiniteClient!.Skill.GetPlaylistCsr(playlistId, playerXuids.ToList()); + rankData = await haloInfiniteClient!.Skill.GetPlaylistCsrAsync(playlistId, playerXuids.ToList()); } if (rankData != null && rankData.Result != null) @@ -399,13 +399,13 @@ private static async Task GetMedalsCommandHandler(string domain) var domainDatabase = new SQLiteConnection(domain); - var medalMetadata = await haloInfiniteClient.GameCms.GetMedalMetadata(); + var medalMetadata = await haloInfiniteClient.GameCms.GetMedalMetadataAsync(); if (medalMetadata.Response!.Code == 401) { // The token is no longer working - need to acquire a new one. WriteTimedLogEntry("Token expired. Refreshing..."); haloInfiniteClient = InstantiateClient(); - medalMetadata = await haloInfiniteClient.GameCms.GetMedalMetadata(); + medalMetadata = await haloInfiniteClient.GameCms.GetMedalMetadataAsync(); } if (medalMetadata != null && medalMetadata.Result != null) @@ -459,13 +459,13 @@ private static async Task GetServiceRecordCommandHandler(bool isXuidFile, foreach (var playerXuid in playerXuids) { - var srData = await haloInfiniteClient.Stats.GetPlayerServiceRecord(playerXuid, LifecycleMode.Matchmade); + var srData = await haloInfiniteClient.Stats.GetPlayerServiceRecordByXuidAsync(playerXuid, LifecycleMode.Matchmade); if (srData.Response!.Code == 401) { // The token is no longer working - need to acquire a new one. WriteTimedLogEntry("Token expired. Refreshing..."); haloInfiniteClient = InstantiateClient(); - srData = await haloInfiniteClient.Stats.GetPlayerServiceRecord(playerXuid, LifecycleMode.Matchmade); + srData = await haloInfiniteClient.Stats.GetPlayerServiceRecordByXuidAsync(playerXuid, LifecycleMode.Matchmade); } if (srData != null && srData.Result != null) @@ -567,7 +567,7 @@ await Task.WhenAll(distinctMatchIds.Select(async matchId => if (!availability.MatchAvailable) { WriteTimedLogEntry($"[{completionProgress:#.00}%] [{matchCounter}/{matchesTotal}] Getting match stats for {matchId}..."); - matchStats = await SafeAPICall(async () => await haloInfiniteClient!.Stats.GetMatchStats(matchId.ToString())); + matchStats = await SafeAPICall(async () => await haloInfiniteClient!.Stats.GetMatchStatsAsync(matchId.ToString())); if (matchStats != null && matchStats.Result != null) { @@ -591,7 +591,7 @@ await Task.WhenAll(distinctMatchIds.Select(async matchId => { if (matchStats == null) { - matchStats = await SafeAPICall(async () => await haloInfiniteClient!.Stats.GetMatchStats(matchId.ToString())); + matchStats = await SafeAPICall(async () => await haloInfiniteClient!.Stats.GetMatchStatsAsync(matchId.ToString())); } if (matchStats != null && matchStats.Result != null && matchStats.Result.Players != null) @@ -604,7 +604,7 @@ await Task.WhenAll(distinctMatchIds.Select(async matchId => WriteTimedLogEntry($"[{completionProgress:#.00}%] [{matchCounter}/{matchesTotal}] Attempting to get player results for players for match {matchId}."); - var playerStatsSnapshot = await SafeAPICall(async () => await haloInfiniteClient.Skill.GetMatchPlayerResult(matchId.ToString(), targetPlayers!)); + var playerStatsSnapshot = await SafeAPICall(async () => await haloInfiniteClient.Skill.GetMatchPlayerResultAsync(matchId.ToString(), targetPlayers!)); if (playerStatsSnapshot != null && playerStatsSnapshot.Result != null && playerStatsSnapshot.Result.Value != null) { @@ -672,7 +672,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result, stri if (!availability.MapAvailable) { - var map = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetMap(result.MatchInfo.MapVariant.AssetId.ToString(), result.MatchInfo.MapVariant.VersionId.ToString())); + var map = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetMapAsync(result.MatchInfo.MapVariant.AssetId.ToString(), result.MatchInfo.MapVariant.VersionId.ToString())); if (map != null && map.Result != null && map.Response.Code == 200) { var record = new MapRecord { ResponseBody = map.Response.Message }; @@ -689,7 +689,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result, stri { if (result.MatchInfo.Playlist != null) { - var playlist = await SafeAPICall(async () => await haloInfiniteClient!.UgcDiscovery.GetPlaylist(result.MatchInfo.Playlist.AssetId.ToString(), result.MatchInfo.Playlist.VersionId.ToString(), haloInfiniteClient.ClearanceToken)); + var playlist = await SafeAPICall(async () => await haloInfiniteClient!.UgcDiscovery.GetPlaylistAsync(result.MatchInfo.Playlist.AssetId.ToString(), result.MatchInfo.Playlist.VersionId.ToString(), haloInfiniteClient.ClearanceToken)); if (playlist != null && playlist.Result != null && playlist.Response.Code == 200) { var record = new PlaylistRecord { ResponseBody = playlist.Response.Message }; @@ -707,7 +707,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result, stri { if (result.MatchInfo.PlaylistMapModePair != null) { - var playlistMmp = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetMapModePair(result.MatchInfo.PlaylistMapModePair.AssetId.ToString(), result.MatchInfo.PlaylistMapModePair.VersionId.ToString(), haloInfiniteClient.ClearanceToken)); + var playlistMmp = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetMapModePairAsync(result.MatchInfo.PlaylistMapModePair.AssetId.ToString(), result.MatchInfo.PlaylistMapModePair.VersionId.ToString(), haloInfiniteClient.ClearanceToken)); if (playlistMmp != null && playlistMmp.Result != null && playlistMmp.Response.Code == 200) { var record = new PlaylistMapModePairRecord { ResponseBody = playlistMmp.Response.Message }; @@ -723,7 +723,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result, stri if (!availability.GameVariantAvailable) { - var gameVariant = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetUgcGameVariant(result.MatchInfo.UgcGameVariant.AssetId.ToString(), result.MatchInfo.UgcGameVariant.VersionId.ToString())); + var gameVariant = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetUgcGameVariantAsync(result.MatchInfo.UgcGameVariant.AssetId.ToString(), result.MatchInfo.UgcGameVariant.VersionId.ToString())); if (gameVariant != null && gameVariant.Result != null && gameVariant.Response.Code == 200) { targetGameVariant = gameVariant.Result; @@ -743,7 +743,7 @@ internal static async Task UpdateMatchAssetRecords(MatchStats result, stri if (!availability.EngineGameVariantAvailable && targetGameVariant != null) { - var engineGameVariant = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetEngineGameVariant(targetGameVariant.EngineGameVariantLink.AssetId.ToString(), targetGameVariant.EngineGameVariantLink.VersionId.ToString())); + var engineGameVariant = await SafeAPICall(async () => await haloInfiniteClient.UgcDiscovery.GetEngineGameVariantAsync(targetGameVariant.EngineGameVariantLink.AssetId.ToString(), targetGameVariant.EngineGameVariantLink.VersionId.ToString())); if (engineGameVariant != null && engineGameVariant.Result != null && engineGameVariant.Response.Code == 200) { @@ -797,7 +797,7 @@ public static async Task> SafeAP private static async Task?> GetPlayerMatchIds(string playerXuid, int start, int count, Den.Dev.Grunt.Models.HaloInfinite.MatchType matchType) { - var matchCountSnapshot = await haloInfiniteClient!.Stats.GetMatchCount(playerXuid); + var matchCountSnapshot = await haloInfiniteClient!.Stats.GetMatchCountAsync(playerXuid); if (matchCountSnapshot.Response!.Code == 401) { @@ -806,7 +806,7 @@ public static async Task> SafeAP haloInfiniteClient = InstantiateClient(); // The counter is not accurate. - matchCountSnapshot = await haloInfiniteClient!.Stats.GetMatchCount(playerXuid); + matchCountSnapshot = await haloInfiniteClient!.Stats.GetMatchCountAsync(playerXuid); } if (matchCountSnapshot != null && matchCountSnapshot.Result != null) @@ -846,7 +846,7 @@ public static async Task> SafeAP do { - var matches = await haloInfiniteClient.Stats.GetMatchHistory(playerXuid, queryStart, queryCount, matchType); + var matches = await haloInfiniteClient.Stats.GetMatchHistoryAsync(playerXuid, queryStart, queryCount, matchType); if (matches != null && matches.Result != null && matches.Result.Results != null && matches.Result.ResultCount > 0) { var matchIdBatch = matches.Result.Results.Select(item => item.MatchId).ToList(); @@ -944,7 +944,7 @@ public static async Task> SafeAP Task.Run(async () => { - haloToken = await haloAuthClient.GetSpartanToken(haloTicket.Token, 4); + haloToken = await haloAuthClient.GetSpartanTokenAsync(haloTicket.Token, 4); WriteTimedLogEntry("Your Halo token:"); WriteTimedLogEntry(haloToken.Token); }).GetAwaiter().GetResult(); diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Librarian/Program.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Librarian/Program.cs index 3e205b6..0dd8e0d 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Librarian/Program.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Librarian/Program.cs @@ -46,7 +46,7 @@ private static async Task Main(string[] args) var client = new HaloInfiniteClient(string.Empty, string.Empty); var configResult = await ui.WithSpinnerAsync("Fetching endpoint configuration...", async () => { - return await client.Configuration.GetApiSettingsContainer(); + return await client.Configuration.GetApiSettingsContainerAsync(); }); if (configResult?.Result?.Endpoints == null) @@ -133,7 +133,7 @@ private static async Task RunGapAnalysis(CommandLineOptions options, Consol var client = new HaloInfiniteClient(string.Empty, string.Empty); var configResult = await ui.WithSpinnerAsync("Fetching endpoint configuration...", async () => { - return await client.Configuration.GetApiSettingsContainer(); + return await client.Configuration.GetApiSettingsContainerAsync(); }); if (configResult?.Result?.Endpoints == null) diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Zeta/Services/AuthenticationService.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Zeta/Services/AuthenticationService.cs index d5e6380..69b436a 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Zeta/Services/AuthenticationService.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt.Zeta/Services/AuthenticationService.cs @@ -101,7 +101,7 @@ await AnsiConsole.Status() return; } - spartanToken = await _haloAuthClient.GetSpartanToken(xstsToken); + spartanToken = await _haloAuthClient.GetSpartanTokenAsync(xstsToken); if (spartanToken == null) { @@ -126,7 +126,7 @@ await AnsiConsole.Status() ctx.Status("[bold blue]Obtaining clearance[/]"); try { - var clearance = (await context.HaloClient.Settings.GetClearance("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13")).Result; + var clearance = (await context.HaloClient.Settings.GetClearanceAsync("RETAIL", "UNUSED", "268411.25.10.26.1801-0", "1.13")).Result; if (clearance != null) { context.ClearanceToken = clearance.FlightConfigurationId ?? string.Empty; diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/HaloAuthenticationClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/HaloAuthenticationClient.cs index 7867886..cda7496 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/HaloAuthenticationClient.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/HaloAuthenticationClient.cs @@ -1,4 +1,4 @@ -// +// // Developed by Den Delimarsky. // Den Delimarsky licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -21,7 +21,7 @@ namespace Den.Dev.Grunt.Authentication /// Halo authentication client, used to provide the key authentication /// data to perform Halo API requests. /// - public class HaloAuthenticationClient + public sealed class HaloAuthenticationClient : IHaloAuthenticationClient { private readonly HttpClient client; @@ -41,8 +41,10 @@ public HaloAuthenticationClient(HttpClient? httpClient = null) /// Version for the Spartan token to be obtained. Halo Infinite uses 4, while Halo 5 uses 3. /// Cancellation token for the operation. /// If successful, returns an instance of representing the authentication token. Otherwise, returns null. - public async Task GetSpartanToken(string xstsToken, int version = 4, CancellationToken cancellationToken = default) + public async Task GetSpartanTokenAsync(string xstsToken, int version = 4, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(xstsToken); + string? data = string.Empty; if (version == 4) @@ -64,7 +66,7 @@ public HaloAuthenticationClient(HttpClient? httpClient = null) data = JsonSerializer.Serialize(tokenRequest); } - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(HaloCoreEndpoints.SpartanTokenEndpoint), Method = version == 4 ? HttpMethod.Post : HttpMethod.Get, @@ -79,11 +81,17 @@ public HaloAuthenticationClient(HttpClient? httpClient = null) request.Headers.Add("X-343-Authorization-XBL3", $"XBL3.0 x=*;{xstsToken}"); } - var response = await this.client.SendAsync(request, cancellationToken); + var response = await this.client.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + response.Dispose(); + return JsonSerializer.Deserialize(responseContent); + } - return response.IsSuccessStatusCode - ? JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(cancellationToken)) - : null; + response.Dispose(); + return null; } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/IHaloAuthenticationClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/IHaloAuthenticationClient.cs new file mode 100644 index 0000000..137276a --- /dev/null +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Authentication/IHaloAuthenticationClient.cs @@ -0,0 +1,28 @@ +// +// Developed by Den Delimarsky. +// Den Delimarsky licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. +// + +using System.Threading; +using System.Threading.Tasks; +using Den.Dev.Grunt.Models.Security; + +namespace Den.Dev.Grunt.Authentication +{ + /// + /// Interface for the Halo authentication client. + /// + public interface IHaloAuthenticationClient + { + /// + /// Gets the Spartan V4 token. + /// + /// XSTS token from the Xbox Live authentication flow. + /// Version for the Spartan token to be obtained. Halo Infinite uses 4, while Halo 5 uses 3. + /// Cancellation token for the operation. + /// If successful, returns an instance of representing the authentication token. Otherwise, returns null. + Task GetSpartanTokenAsync(string xstsToken, int version = 4, CancellationToken cancellationToken = default); + } +} diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Foundation/ClientBase.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Foundation/ClientBase.cs index 76aee8c..bfe771c 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Foundation/ClientBase.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Foundation/ClientBase.cs @@ -12,6 +12,7 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Converters; using Den.Dev.Grunt.Models; @@ -101,7 +102,7 @@ protected ClientBase(HttpClient httpClient, MemoryCache memoryCache) /// /// Gets or sets the instance of the HTTP client that handles processing of API requests and responses. /// - public HttpClient Client { get; set; } + public HttpClient Client { get; protected set; } /// /// Gets or sets the Spartan token used to authenticate against the Halo Infinite API. @@ -109,7 +110,7 @@ protected ClientBase(HttpClient httpClient, MemoryCache memoryCache) public string SpartanToken { get; set; } = string.Empty; /// - /// Gets or sets the player identifier in the format "xuid(XUID_VALUE)". + /// Gets or sets the player identifier in the format "xuid(XUID_VALUE)". /// public string Xuid { get; set; } = string.Empty; @@ -143,9 +144,10 @@ protected ClientBase(HttpClient httpClient, MemoryCache memoryCache) /// Determines whether a raw response will be returned with the result. Disabled by default. /// A list of custom headers to append to the request. /// Determines whether to try and serialize the response data even if the request returns an error code (that is - not HTTP 200 OK). Default is set to true. + /// Cancellation token for the operation. /// Data type to return with the response metadata. /// Response string in case of a successful request. Null if request failed. - public async Task> ExecuteAPIRequest( + public async Task> ExecuteAPIRequestAsync( string endpoint, HttpMethod method, bool useSpartanToken, @@ -155,7 +157,8 @@ public async Task> ExecuteAPIReq APIContentType contentType = APIContentType.Json, bool includeRawResponse = false, List>? customHeaders = null, - bool enforceSuccess = true) + bool enforceSuccess = true, + CancellationToken cancellationToken = default) { HaloApiResultContainer resultContainer = new(default!, new RawResponseContainer()); @@ -169,59 +172,73 @@ public async Task> ExecuteAPIReq useClearance, customHeaders); - // Capture request details for diagnostics when includeRawResponse is enabled - var captureRawResponse = includeRawResponse || this.IncludeRawResponses; - if (captureRawResponse) - { - resultContainer.Response!.RequestUrl = endpoint; - resultContainer.Response.RequestMethod = method.Method; - resultContainer.Response.RequestHeaders = CaptureHeaders(request.Headers, request.Content?.Headers); - resultContainer.Response.RequestBody = textContent; - } - - HttpResponseMessage? response = null; - byte[]? responseData = null; - try { - (response, responseData) = await this.SendWithCacheAsync(request, endpoint, enforceSuccess); - } - catch (HttpRequestException ex) - { - resultContainer.Response!.Message = ex.Message; - - if (ex.InnerException is WebException webException) + // Capture request details for diagnostics when includeRawResponse is enabled + var captureRawResponse = includeRawResponse || this.IncludeRawResponses; + if (captureRawResponse) { - if (webException.Response is HttpWebResponse httpWebResponse) - { - resultContainer.Response!.Code = (int)httpWebResponse.StatusCode; - } + resultContainer.Response!.RequestUrl = endpoint; + resultContainer.Response.RequestMethod = method.Method; + resultContainer.Response.RequestHeaders = CaptureHeaders(request.Headers, request.Content?.Headers); + resultContainer.Response.RequestBody = textContent; } - } - if (response != null) - { - resultContainer.Response!.Code = Convert.ToInt32(response!.StatusCode); + HttpResponseMessage? response = null; + byte[]? responseData = null; - if (captureRawResponse) + try { - resultContainer.Response.ResponseHeaders = CaptureHeaders(response.Headers, response.Content?.Headers); + (response, responseData) = await this.SendWithCacheAsync(request, endpoint, enforceSuccess, cancellationToken).ConfigureAwait(false); } - - if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotModified || enforceSuccess) + catch (HttpRequestException ex) { - resultContainer.Result = this.DeserializeResponse( - responseData!, - response, - resultContainer.Response, - captureRawResponse); + resultContainer.Response!.Message = ex.Message; + + if (ex.InnerException is WebException webException) + { + if (webException.Response is HttpWebResponse httpWebResponse) + { + resultContainer.Response!.Code = (int)httpWebResponse.StatusCode; + } + } } - if (response.Content != null) + if (response != null) { - resultContainer.Response.Message = await response.Content.ReadAsStringAsync(); + try + { + resultContainer.Response!.Code = Convert.ToInt32(response!.StatusCode); + + if (captureRawResponse) + { + resultContainer.Response.ResponseHeaders = CaptureHeaders(response.Headers, response.Content?.Headers); + } + + if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotModified || enforceSuccess) + { + resultContainer.Result = this.DeserializeResponse( + responseData!, + response, + resultContainer.Response, + captureRawResponse); + } + + if (response.Content != null) + { + resultContainer.Response.Message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + } + finally + { + response.Dispose(); + } } } + finally + { + request.Dispose(); + } return resultContainer; } @@ -242,7 +259,7 @@ private static bool IsTransientError(HttpResponseMessage? response, Exception? e return code >= 500 || code == 408 || code == 429; } - private static async Task CloneHttpRequestMessageAsync(HttpRequestMessage request) + private static async Task CloneHttpRequestMessageAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var clone = new HttpRequestMessage(request.Method, request.RequestUri); @@ -255,7 +272,7 @@ private static async Task CloneHttpRequestMessageAsync(HttpR // Copy content if present if (request.Content != null) { - var contentBytes = await request.Content.ReadAsByteArrayAsync(); + var contentBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); clone.Content = new ByteArrayContent(contentBytes); // Copy content headers @@ -287,10 +304,10 @@ private static Dictionary CaptureHeaders(HttpHeaders headers, Ht return result; } - private async Task UpdateCacheAsync(string cacheKey, HttpResponseMessage response) + private async Task UpdateCacheAsync(string cacheKey, HttpResponseMessage response, CancellationToken cancellationToken) { var eTag = response.Headers.ETag?.ToString(); - var content = await response.Content.ReadAsByteArrayAsync(); + var content = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); var cacheEntryOptions = new MemoryCacheEntryOptions { @@ -300,7 +317,7 @@ private async Task UpdateCacheAsync(string cacheKey, HttpResponseMessage respons this.cache.Set(cacheKey, new CachedAPIResponse { ETag = eTag, Content = content }, cacheEntryOptions); } - private async Task SendWithRetryAsync(HttpRequestMessage request) + private async Task SendWithRetryAsync(HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage? response = null; Exception? lastException = null; @@ -317,10 +334,10 @@ private async Task SendWithRetryAsync(HttpRequestMessage re } else { - requestToSend = await CloneHttpRequestMessageAsync(request); + requestToSend = await CloneHttpRequestMessageAsync(request, cancellationToken).ConfigureAwait(false); } - response = await this.Client.SendAsync(requestToSend); + response = await this.Client.SendAsync(requestToSend, cancellationToken).ConfigureAwait(false); if (!IsTransientError(response, null)) { @@ -328,6 +345,13 @@ private async Task SendWithRetryAsync(HttpRequestMessage re } // Transient error, will retry if attempts remain + // On the last attempt, keep the response so the caller can inspect the error code + if (attempt < MaxRetries) + { + response?.Dispose(); + response = null; + } + lastException = null; } catch (HttpRequestException ex) @@ -350,7 +374,7 @@ private async Task SendWithRetryAsync(HttpRequestMessage re // Don't delay after the last attempt if (attempt < MaxRetries) { - await Task.Delay(RetryDelays[attempt]); + await Task.Delay(RetryDelays[attempt], cancellationToken).ConfigureAwait(false); } } @@ -440,7 +464,8 @@ private HttpRequestMessage BuildRequest( private async Task<(HttpResponseMessage? Response, byte[]? Data)> SendWithCacheAsync( HttpRequestMessage request, string cacheKey, - bool enforceSuccess) + bool enforceSuccess, + CancellationToken cancellationToken) { HttpResponseMessage? response = null; byte[]? responseData = null; @@ -451,7 +476,7 @@ private HttpRequestMessage BuildRequest( { var eTagHeader = cachedResponse.ETag; request.Headers.Add("If-None-Match", eTagHeader); - response = await this.SendWithRetryAsync(request); + response = await this.SendWithRetryAsync(request, cancellationToken).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.NotModified) { @@ -461,23 +486,23 @@ private HttpRequestMessage BuildRequest( { if (response.IsSuccessStatusCode || enforceSuccess) { - await this.UpdateCacheAsync(cacheKey, response); + await this.UpdateCacheAsync(cacheKey, response, cancellationToken).ConfigureAwait(false); } - responseData = await response.Content.ReadAsByteArrayAsync(); + responseData = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); } } } else { - response = await this.SendWithRetryAsync(request); + response = await this.SendWithRetryAsync(request, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotModified || enforceSuccess) { - await this.UpdateCacheAsync(cacheKey, response); + await this.UpdateCacheAsync(cacheKey, response, cancellationToken).ConfigureAwait(false); } - responseData = await response.Content.ReadAsByteArrayAsync(); + responseData = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); } return (response, responseData); @@ -508,22 +533,37 @@ private HttpRequestMessage BuildRequest( if (Attribute.GetCustomAttribute(typeof(T), typeof(IsAutomaticallySerializableAttribute)) != null || typeof(T).IsGenericType) { - var responseString = Encoding.UTF8.GetString(responseData); - if (!string.IsNullOrWhiteSpace(responseString)) + if (captureRaw) { - if (captureRaw) + var responseString = Encoding.UTF8.GetString(responseData); + if (!string.IsNullOrWhiteSpace(responseString)) { rawResponse.Message = responseString; - } - try - { - return JsonSerializer.Deserialize(responseString, this.serializerOptions); + try + { + return JsonSerializer.Deserialize(responseString, this.serializerOptions); + } + catch (JsonException) + { + // Deserialization failed, but HTTP details are preserved in Response + return default; + } } - catch (JsonException) + } + else + { + if (responseData.Length > 0) { - // Deserialization failed, but HTTP details are preserved in Response - return default; + try + { + return JsonSerializer.Deserialize(responseData, this.serializerOptions); + } + catch (JsonException) + { + // Deserialization failed, but HTTP details are preserved in Response + return default; + } } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/HaloInfiniteClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/HaloInfiniteClient.cs index 9c1523b..1621df9 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/HaloInfiniteClient.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/HaloInfiniteClient.cs @@ -25,17 +25,17 @@ namespace Den.Dev.Grunt.Core /// var client = new HaloInfiniteClient(spartanToken, xuid, clearanceToken); /// /// // Access economy APIs - /// var inventory = await client.Economy.GetInventoryItems(player); + /// var inventory = await client.Economy.GetInventoryItemsAsync(player); /// /// // Access stats APIs - /// var matchHistory = await client.Stats.GetMatchHistory(player, 0, 25, MatchType.All); + /// var matchHistory = await client.Stats.GetMatchHistoryAsync(player, 0, 25, MatchType.All); /// /// // Access GameCMS APIs - /// var medals = await client.GameCms.GetMedalMetadata(); + /// var medals = await client.GameCms.GetMedalMetadataAsync(); /// /// /// - public class HaloInfiniteClient : ClientBase + public sealed class HaloInfiniteClient : ClientBase, IHaloInfiniteClient { /// /// Initializes a new instance of the class. diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IHaloInfiniteClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IHaloInfiniteClient.cs new file mode 100644 index 0000000..70971df --- /dev/null +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IHaloInfiniteClient.cs @@ -0,0 +1,77 @@ +// +// Developed by Den Delimarsky. +// Den Delimarsky licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. +// + +using Den.Dev.Grunt.Core.Modules.HaloInfinite; + +namespace Den.Dev.Grunt.Core +{ + /// + /// Interface for the Halo Infinite API client. + /// + public interface IHaloInfiniteClient + { + /// + /// Gets the Academy module for bot customization and drill-related APIs. + /// + AcademyModule Academy { get; } + + /// + /// Gets the Ban Processor module for ban-related APIs. + /// + BanProcessorModule BanProcessor { get; } + + /// + /// Gets the Configuration module for endpoint discovery APIs. + /// + ConfigurationModule Configuration { get; } + + /// + /// Gets the Economy module for player customization, stores, and inventory APIs. + /// + EconomyModule Economy { get; } + + /// + /// Gets the Game CMS module for content management APIs including achievements, metadata, and files. + /// + GameCmsModule GameCms { get; } + + /// + /// Gets the Lobby module for lobby and presence APIs. + /// + LobbyModule Lobby { get; } + + /// + /// Gets the Settings module for clearance and flight configuration APIs. + /// + SettingsModule Settings { get; } + + /// + /// Gets the Skill module for CSR and match skill APIs. + /// + SkillModule Skill { get; } + + /// + /// Gets the Stats module for match history and service record APIs. + /// + StatsModule Stats { get; } + + /// + /// Gets the Text Moderation module for moderation key APIs. + /// + TextModerationModule TextModeration { get; } + + /// + /// Gets the UGC module for user-generated content authoring APIs. + /// + UgcModule Ugc { get; } + + /// + /// Gets the UGC Discovery module for UGC search and discovery APIs. + /// + UgcDiscoveryModule UgcDiscovery { get; } + } +} diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IWaypointClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IWaypointClient.cs new file mode 100644 index 0000000..997d83d --- /dev/null +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/IWaypointClient.cs @@ -0,0 +1,37 @@ +// +// Developed by Den Delimarsky. +// Den Delimarsky licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. +// + +using Den.Dev.Grunt.Core.Modules.Waypoint; + +namespace Den.Dev.Grunt.Core +{ + /// + /// Interface for the Halo Waypoint API client. + /// + public interface IWaypointClient + { + /// + /// Gets the Profile module for user settings and profile APIs. + /// + ProfileModule Profile { get; } + + /// + /// Gets the Redemption module for code redemption APIs. + /// + RedemptionModule Redemption { get; } + + /// + /// Gets the Content module for article and content APIs. + /// + ContentModule Content { get; } + + /// + /// Gets the Comms module for communication and notification APIs. + /// + CommsModule Comms { get; } + } +} diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/AcademyModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/AcademyModule.cs index 8ac4399..35a8d35 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/AcademyModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/AcademyModule.cs @@ -5,6 +5,8 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for Academy-related API operations including bot customization and drills. /// - public class AcademyModule : ModuleBase + public sealed class AcademyModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -28,28 +30,34 @@ internal AcademyModule(ClientBase client) } /// - /// Get bot customization information. + /// Gets bot customization information. /// /// /// ID of the flight/clearance associated with the request. + /// Cancellation token for the operation. /// If successful, returns an instance of BotCustomizationData that contains bot customization information. Otherwise, returns null. - public async Task> GetBotCustomization(string flightId) + public Task> GetBotCustomizationAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/multiplayer/file/Academy/BotCustomizationData.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the client manifest for the Academy. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of AcademyClientManifest that contains the definition of drills available in the Academy. Otherwise, returns null. - public async Task> GetContent() + public Task> GetContentAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/multiplayer/file/Academy/AcademyClientManifest.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -57,23 +65,29 @@ public async Task /// /// ID of the flight/clearance associated with the request. + /// Cancellation token for the operation. /// If successful, returns an instance of TestAcademyClientManifest that contains the definition of drills available in the Academy. Otherwise, returns null. - public async Task> GetContentTest(string clearanceId) + public Task> GetContentTestAsync(string clearanceId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/multiplayer/file/Academy/AcademyClientManifest_Test.json?flight={clearanceId}"); + ArgumentException.ThrowIfNullOrEmpty(clearanceId); + + return this.GetAsync( + $"/hi/multiplayer/file/Academy/AcademyClientManifest_Test.json?flight={clearanceId}", + cancellationToken: cancellationToken); } /// /// Gets definitions for stars awarded in the Academy. This call breaks if a user agent is specified. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of AcademyStarDefinitions that contains definitions for stars awarded in the Academy. Otherwise, returns null. - public async Task> GetStarDefinitions() + public Task> GetStarDefinitionsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/multiplayer/file/Academy/AcademyStarGUIDDefinitions.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/BanProcessorModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/BanProcessorModule.cs index dafd41c..f316766 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/BanProcessorModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/BanProcessorModule.cs @@ -5,7 +5,9 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -17,7 +19,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for ban processor related API operations. /// - public class BanProcessorModule : ModuleBase + public sealed class BanProcessorModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -36,13 +38,17 @@ internal BanProcessorModule(ClientBase client) /// In some quick tests, it seems that including Authenticated(Device) in the request results in 401 Unauthorized if called outside the game. Additional work might be required to understand how to validate the device. /// /// - /// A list of targets that need to be checked. Authenticated devices can be included as "Authenticated(Device)". Individual players can be specified as "xuid(XUID_VALUE)". + /// A list of targets that need to be checked. Authenticated devices can be included as "Authenticated(Device)". Individual players can be specified as "xuid(XUID_VALUE)". + /// Cancellation token for the operation. /// An instance of BanSummary containing applicable ban information if request was successful. Return value is null otherwise. - public async Task> BanSummary(List targetlist) + public Task> GetBanSummaryAsync(List targetList, CancellationToken cancellationToken = default) { - var formattedTargetList = string.Join(",", targetlist); - return await this.GetAsync( - $"/hi/bansummary?auth=st&targets={formattedTargetList}"); + ArgumentNullException.ThrowIfNull(targetList); + + var formattedTargetList = string.Join(",", targetList); + return this.GetAsync( + $"/hi/bansummary?auth=st&targets={formattedTargetList}", + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/ConfigurationModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/ConfigurationModule.cs index a2bc456..89f533d 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/ConfigurationModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/ConfigurationModule.cs @@ -5,6 +5,7 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +17,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for configuration and endpoint discovery APIs. /// - public class ConfigurationModule : ModuleBase + public sealed class ConfigurationModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -30,13 +31,15 @@ internal ConfigurationModule(ClientBase client) /// /// Gets the API settings container, which has the full list of available endpoints. /// + /// Cancellation token for the operation. /// If successful, returns an instance of Configuration that contains the full list of available endpoints. Otherwise, returns null. - public async Task> GetApiSettingsContainer() + public Task> GetApiSettingsContainerAsync(CancellationToken cancellationToken = default) { - return await this.GetAsyncFullUrl( + return this.GetAsyncFullUrl( HaloCoreEndpoints.HaloInfiniteEndpointsEndpoint, useClearance: false, - useSpartanToken: true); + useSpartanToken: true, + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/EconomyModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/EconomyModule.cs index 8df2dda..25d867c 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/EconomyModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/EconomyModule.cs @@ -5,8 +5,10 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -18,7 +20,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for economy-related API operations including player customization, stores, and inventory. /// - public class EconomyModule : ModuleBase + public sealed class EconomyModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -35,38 +37,51 @@ internal EconomyModule(ClientBase client) /// /// The player's numeric XUID. /// Unique AI Core ID. Example ID is "304-100-ai-core-debb20e3". + /// Cancellation token for the operation. /// If successful, returns an instance of Core containing AI core customization metadata if request was successful. Otherwise, returns null. - public async Task> AiCoreCustomization(string player, string coreId) + public Task> GetAiCoreCustomizationAsync(string player, string coreId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(coreId); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/ais/{coreId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Get AI core customization for a player. + /// Gets AI core customization for a player. /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of AiCores containing AI core customization metadata if request was successful. Return value is null otherwise. - public async Task> AiCoresCustomization(string player) + public Task> GetAiCoresCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/ais", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Get details about all owned cores for a player. + /// Gets details about all owned cores for a player. /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of PlayerCores containing player core customization metadata if request was successful. Return value is null otherwise. - public async Task> AllOwnedCoresDetails(string player) + public Task> GetAllOwnedCoresDetailsAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/cores", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -75,12 +90,17 @@ public async Task> All /// /// The player's numeric XUID. /// The unique identifier for an armor core. An example value is "017-001-eag-c13d0b38". + /// Cancellation token for the operation. /// If successful, returns an instance of ArmorCore containing customization information. Otherwise, returns null. - public async Task> ArmorCoreCustomization(string player, string coreId) + public Task> GetArmorCoreCustomizationAsync(string player, string coreId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(coreId); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/armors/{coreId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -88,12 +108,16 @@ public async Task> Armor /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of ArmorCoreCollection that contains the list of armor cores. Otherwise, returns null. - public async Task> ArmorCoresCustomization(string player) + public Task> GetArmorCoresCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/armors", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -101,12 +125,16 @@ public async Task /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of ActiveBoostsContainer that contains the list of active boosts. Otherwise, returns null. - public async Task> GetActiveBoosts(string player) + public Task> GetActiveBoostsAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/boosts", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -115,12 +143,17 @@ public async Task /// The player's numeric XUID. /// The unique ID for the reward given to a player. Example value is "Challenges-35a86ae3-017c-4b5a-b633-b2802a770e0a". + /// Cancellation token for the operation. /// If successful, returns an instance of RewardSnapshot that contains the list of awarded rewards. Otherwise, returns null. - public async Task> GetAwardedRewards(string player, string rewardId) + public Task> GetAwardedRewardsAsync(string player, string rewardId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(rewardId); + + return this.GetAsync( $"/hi/players/xuid({player})/rewards/{rewardId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -128,12 +161,16 @@ public async Task> /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem containing boost information. Otherwise, returns null. - public async Task> GetBoostsStore(string player) + public Task> GetBoostsStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/boosts", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -141,12 +178,16 @@ public async Task> GetBo /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem containing The Exchange information. Otherwise, returns null. - public async Task> GetSoftCurrencyStore(string player) + public Task> GetSoftCurrencyStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/softcurrencyoffers", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -159,14 +200,17 @@ public async Task> GetSo /// /// The player's numeric XUID. /// The sub-store index (0-5). Maps to creditsubstorefront00 through creditsubstorefront05. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem containing store offerings. Otherwise, returns null. - public async Task> GetCreditSubStore(string player, int storeIndex) + public Task> GetCreditSubStoreAsync(string player, int storeIndex, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(player); ValidateRange(storeIndex, 0, 5, nameof(storeIndex)); - return await this.GetAsync( + return this.GetAsync( $"/hi/players/xuid({player})/stores/creditsubstorefront{storeIndex:D2}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -180,14 +224,17 @@ public async Task> GetCr /// /// The player's numeric XUID. /// The sub-store index (0-15). Maps to softcurrencysubstorefront00 through softcurrencysubstorefront15. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem containing store offerings. Otherwise, returns null. - public async Task> GetSoftCurrencySubStore(string player, int storeIndex) + public Task> GetSoftCurrencySubStoreAsync(string player, int storeIndex, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(player); ValidateRange(storeIndex, 0, 15, nameof(storeIndex)); - return await this.GetAsync( + return this.GetAsync( $"/hi/players/xuid({player})/stores/softcurrencysubstorefront{storeIndex:D2}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -195,12 +242,16 @@ public async Task> GetSo /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of PlayerGiveaways containing available giveaways. Otherwise, returns null. - public async Task> GetGiveawayRewards(string player) + public Task> GetGiveawayRewardsAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/giveaways", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -208,12 +259,16 @@ public async Task> /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, an instance of StoreItem containing store offerings. Otherwise, returns null. - public async Task> GetHCSStore(string player) + public Task> GetHCSStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/hcs", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -221,12 +276,16 @@ public async Task> GetHC /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of PlayerInventory that contains a list of items in the player's inventory. Otherwise, returns null. - public async Task> GetInventoryItems(string player) + public Task> GetInventoryItemsAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/inventory", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -234,12 +293,16 @@ public async Task> /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem that contains information about items available in the main store. Otherwise, returns null. - public async Task> GetMainStore(string player) + public Task> GetMainStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/main", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -247,13 +310,17 @@ public async Task> GetMa /// /// /// List of numeric XUIDs for the players. + /// Cancellation token for the operation. /// If successful, returns an instance of PlayerCustomizationCollection that contains player customizations. Otherwise, returns null. - public async Task> GetMultiplePlayersCustomization(List playerIds) + public Task> GetMultiplePlayersCustomizationAsync(List playerIds, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(playerIds); + var formattedPlayerList = string.Join(",", playerIds.Select(id => $"xuid({id})")); - return await this.GetAsync( + return this.GetAsync( $"/hi/customization?players={formattedPlayerList}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -261,12 +328,16 @@ public async Task /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem that contains information about items available in the operations reward levels store. Otherwise, returns null. - public async Task> GetOperationRewardLevelsStore(string player) + public Task> GetOperationRewardLevelsStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/operationrewardlevels", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -274,12 +345,16 @@ public async Task> GetOp /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem that contains information about items available in the operations store. Otherwise, returns null. - public async Task> GetOperationsStore(string player) + public Task> GetOperationsStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/operations", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -289,12 +364,18 @@ public async Task> GetOp /// The player's numeric XUID. /// Type of reward track. For seasons, this is usually "operation". This parameter is a singular noun, and is pluralized automatically in the function (the "s" character is appended). /// Unique identifier for the reward track. An example value is "battlepass-noblesacrifice.json". + /// Cancellation token for the operation. /// If successful, returns an instance of RewardTrack containing information for reward track tiers. Otherwise, returns null. - public async Task> GetRewardTrack(string player, string rewardTrackType, string trackId) + public Task> GetRewardTrackAsync(string player, string rewardTrackType, string trackId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(rewardTrackType); + ArgumentException.ThrowIfNullOrEmpty(trackId); + + return this.GetAsync( $"/hi/players/xuid({player})/rewardtracks/{rewardTrackType}s/{trackId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -302,12 +383,16 @@ public async Task> Get /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of CurrencySnapshot that contains the balances. Otherwise, returns null. - public async Task> GetVirtualCurrencyBalances(string player) + public Task> GetVirtualCurrencyBalancesAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/currencies", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -315,12 +400,16 @@ public async Task /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem that contains information about items in the store. Otherwise, returns null. - public async Task> GetXpGrantsStore(string player) + public Task> GetXpGrantsStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/xpgrants", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -329,12 +418,17 @@ public async Task> GetXp /// /// The player's numeric XUID. /// The unique core ID. An example is "017-001-eag-c13d0b38". + /// Cancellation token for the operation. /// If successful, returns an instance of Core containing core information. Otherwise, returns null. - public async Task> OwnedCoreDetails(string player, string coreId) + public Task> GetOwnedCoreDetailsAsync(string player, string coreId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(coreId); + + return this.GetAsync( $"/hi/players/xuid({player})/cores/{coreId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -342,12 +436,16 @@ public async Task> Own /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of AppearanceCustomization containing customization information. Otherwise, returns null. - public async Task> PlayerAppearanceCustomization(string player) + public Task> GetPlayerAppearanceCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/appearance", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -356,12 +454,17 @@ public async Task /// The player's numeric XUID. /// Determines which view into customizations is shown. Available values are "public" and "private". The private view enables showing all available cores, while the public view only shows equipped cores. + /// Cancellation token for the operation. /// If successful, returns an instance of CustomizationData containing player customizations. Otherwise, returns null. - public async Task> PlayerCustomization(string player, string viewType) + public Task> GetPlayerCustomizationAsync(string player, string viewType, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(viewType); + + return this.GetAsync( $"/hi/players/xuid({player})/customization?view={viewType}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -370,29 +473,40 @@ public async Task /// The player's numeric XUID. /// The unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of OperationRewardTrackSnapshot containing battle pass information. Otherwise, returns null. - public async Task> PlayerOperations(string player, string flightId) + public Task> GetPlayerOperationsAsync(string player, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/players/xuid({player})/rewardtracks/operations?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets information about transactions that the player executed. /// /// - /// This function is likely used as a POST as well (hence the name - right now we're only using GET). Once we discover how this API works, we can extend the functionality further. + /// The endpoint is named PostCurrencyTransaction in the API definition, but the current implementation uses a GET request. + /// The method name matches the endpoint definition for consistency. /// /// /// The player's numeric XUID. /// The unique identifier for the currency. Valid values include "cr", "rerollcurrency", "xpboost", and "xpgrant". + /// Cancellation token for the operation. /// If successful, returns an instance of TransactionSnapshot listing all existing transactions. Otherwise, returns null. - public async Task> PostCurrencyTransaction(string player, string currencyId) + public Task> PostCurrencyTransactionAsync(string player, string currencyId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(currencyId); + + return this.GetAsync( $"/hi/players/xuid({player})/currencies/{currencyId}/transactions", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -401,12 +515,17 @@ public async Task /// The player's numeric XUID. /// The unique store identifier. An example value is "hcs". + /// Cancellation token for the operation. /// If successful, returns an instance of StoreItem containing offerings. Otherwise, returns null. - public async Task> ScheduledStorefrontOfferings(string player, string storeId) + public Task> GetScheduledStorefrontOfferingsAsync(string player, string storeId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(storeId); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/{storeId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -414,12 +533,16 @@ public async Task> Sched /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of SpartanBody containing the customization information. Otherwise, returns null. - public async Task> SpartanBodyCustomization(string player) + public Task> GetSpartanBodyCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/spartanbody", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -428,12 +551,17 @@ public async Task> Spa /// /// The player's numeric XUID. /// Unique vehicle core ID. Example value is "409-304-olympus-e8b8a8b3". + /// Cancellation token for the operation. /// If successful, returns an instance of VehicleCore. Otherwise, returns null. - public async Task> VehicleCoreCustomization(string player, string coreId) + public Task> GetVehicleCoreCustomizationAsync(string player, string coreId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(coreId); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/vehicles/{coreId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -441,12 +569,16 @@ public async Task> Veh /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of VehicleCoreCollection containing a list of available vehicle cores. Otherwise, returns null. - public async Task> VehicleCoresCustomization(string player) + public Task> GetVehicleCoresCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/vehicles", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -455,12 +587,17 @@ public async Task /// The player's numeric XUID. /// The unique ID of the weapon core. + /// Cancellation token for the operation. /// If successful, returns an instance of WeaponCore containing information about the weapon core. Otherwise, returns null. - public async Task> WeaponCoreCustomization(string player, string coreId) + public Task> GetWeaponCoreCustomizationAsync(string player, string coreId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(coreId); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/weapons/{coreId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -468,25 +605,33 @@ public async Task> Weap /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of WeaponCoreCollection. Otherwise, returns null. - public async Task> WeaponCoresCustomization(string player) + public Task> GetWeaponCoresCustomizationAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/customization/weapons", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Gets or sets the store customization offers available for a player. + /// Gets the store customization offers available for a player. /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null. - public async Task> GetCustomizationStore(string player) + public Task> GetCustomizationStoreAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/stores/customizationoffers", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -495,18 +640,24 @@ public async Task> GetCu /// /// List of numeric XUIDs for the players. /// Unique identifier for the career path. Example value is "careerRank1". + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null with associated error details in within the result. - public async Task> GetPlayerCareerRank(List players, string careerPathId) + public Task> GetPlayerCareerRankAsync(List players, string careerPathId, CancellationToken cancellationToken = default) { - var formattedPlayerList = string.Empty; - if (players != null && players.Count > 0) + ArgumentNullException.ThrowIfNull(players); + ArgumentException.ThrowIfNullOrEmpty(careerPathId); + + if (players.Count == 0) { - formattedPlayerList = string.Join(",", players.Select(id => $"xuid({id})")); + throw new ArgumentException("Players list must not be empty.", nameof(players)); } - return await this.GetAsync( + var formattedPlayerList = string.Join(",", players.Select(id => $"xuid({id})")); + + return this.GetAsync( $"/hi/careerranks/{careerPathId}?players={formattedPlayerList}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/GameCmsModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/GameCmsModule.cs index 352eb65..dfbf1f8 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/GameCmsModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/GameCmsModule.cs @@ -5,7 +5,9 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -17,7 +19,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for Game CMS related API operations including achievements, metadata, and content files. /// - public class GameCmsModule : ModuleBase + public sealed class GameCmsModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -33,24 +35,30 @@ internal GameCmsModule(ClientBase client) /// /// /// Path to a store offering, for example 'StoreContent/Display/Offerings/20240410-01.json'. + /// Cancellation token for the operation. /// If successful, returns an instance of containing offering details. Otherwise, returns null with a description of the error. - public async Task> GetStoreOffering(string offeringPath) + public Task> GetStoreOfferingAsync(string offeringPath, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(offeringPath); + + return this.GetAsync( $"/hi/Progression/file/{offeringPath}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the fallback playlist for the Play Now button. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null with a description of the error. - public async Task> GetPlayNowButtonSettings() + public Task> GetPlayNowButtonSettingsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Multiplayer/file/playlists/playNowButton/settings.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -60,24 +68,28 @@ public async Task /// Keep in mind that this is not a list of achievements that the player has unlocked - it's just an aggregation of all available achievements in Halo Infinite. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of AchievementCollection that contains the list of available achievements. Otherwise, returns null. - public async Task> GetAchievements() + public Task> GetAchievementsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Multiplayer/file/Live/Achievements.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Gets information about active async compute overrides. Unknown what the concrete purpose of this API is yet. + /// Gets information about active async compute overrides. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of AsyncComputeOverrides containing override metadata. Otherwise, returns null. - public async Task> GetAsyncComputeOverrides() + public Task> GetAsyncComputeOverridesAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/graphics/AsyncComputeOverrides.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -86,12 +98,17 @@ public async Task /// Path to the challenge file. Example is "ChallengeContent/ClientChallengeDefinitions/S1RotationalSet1Challenges/Normal/NTeamSlayerPlay.json". /// The unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of Challenge containing challenge information. Otherwise, returns null. - public async Task> GetChallenge(string challengePath, string flightId) + public Task> GetChallengeAsync(string challengePath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(challengePath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{challengePath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -100,12 +117,17 @@ public async Task> GetCh /// /// Path to the challenge deck. An example value is "ChallengeContent/ClientChallengeDeckDefinitions/S2EntrenchedWeeklyDeck2.json". /// Unique identifier for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of ChallengeDeckDefinition containing challenge deck metadata. Otherwise, returns null. - public async Task> GetChallengeDeck(string challengeDeckPath, string flightId) + public Task> GetChallengeDeckAsync(string challengeDeckPath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(challengeDeckPath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{challengeDeckPath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -114,12 +136,17 @@ public async Task /// Path to the currency. An example is "currency/currencies/cr.json". /// Unique identifier for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of CurrencyDefinition containing information about the specified currency. Otherwise, returns null. - public async Task> GetCurrency(string currencyPath, string flightId) + public Task> GetCurrencyAsync(string currencyPath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(currencyPath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{currencyPath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -131,36 +158,44 @@ public async Task /// /// Unique identifier for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of ClawAccessSnapshot containing relevant XUID lists. Otherwise, returns null. - public async Task> GetClawAccess(string flightId) + public Task> GetClawAccessAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/TitleAuthorization/file/claw/access.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the pre-defined CPU presets for different game performance configurations. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of CPUPresetSnapshot containing preset information. Otherwise, returns null. - public async Task> GetCpuPresets() + public Task> GetCpuPresetsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/cpu/presets.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Returns the parameters for new custom games started in Halo Infinite. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of CustomGameDefinition containing game parameters. Otherwise, returns null. - public async Task> GetCustomGameDefaults() + public Task> GetCustomGameDefaultsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Multiplayer/file/NonMatchmaking/customgame.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -168,12 +203,16 @@ public async Task /// /// Unique identifier for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of InventoryDefinition containing the full list of available items. Otherwise, returns null. - public async Task> GetCustomizationCatalog(string flightId) + public Task> GetCustomizationCatalogAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/inventory/catalog/inventory_catalog.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -183,12 +222,14 @@ public async Task /// + /// Cancellation token for the operation. /// If successful, an instance of DevicePresetOverrides. Otherwise, returns null. - public async Task> GetDevicePresetOverrides() + public Task> GetDevicePresetOverridesAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/graphics/DevicePresetOverrides.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -197,50 +238,63 @@ public async Task /// The path to the event file. An example value is "RewardTracks/Events/Rituals/ritualEagleStrike.json". /// Unique identifier for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of RewardTrackMetadata is returned. Otherwise, returns null. - public async Task> GetEvent(string eventPath, string flightId) + public Task> GetEventAsync(string eventPath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(eventPath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{eventPath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the queries used to obtain override values for graphic device specifications. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of OverrideQueryDefinition containing query definitions. Otherwise, returns null. - public async Task> GetGraphicsSpecControlOverrides() + public Task> GetGraphicsSpecControlOverridesAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/graphics/GraphicsSpecControlOverrides.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Unknown what this API specifically returns, but the assumption is that it's configuration for graphic setting overrides. + /// Gets configuration for graphic setting overrides. Returns a raw response string. /// /// - /// TODO: Need to figure out what the API response here is. Haven't seen this actually activated in-game. For the time being, the API call will return a raw response. + /// The exact structure of the API response has not been fully mapped. The API call returns a raw response for the time being. /// + /// Cancellation token for the operation. /// Returns a string containing the response. - public async Task> GetGraphicSpecs() + public Task> GetGraphicSpecsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/graphics/overrides.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets an image for an associated game CMS asset. Example path is "progression/inventory/armor/gloves/003-001-olympus-8e7c9dff-sm.png". /// /// Path to the CMS image. + /// Cancellation token for the operation. /// If successful, returns the byte array for the requested image. Otherwise, returns null. - public async Task> GetImage(string filePath) + public Task> GetImageAsync(string filePath, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(filePath); + + return this.GetAsync( $"/hi/images/file/{filePath}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -252,12 +306,17 @@ public async Task> GetImage /// /// Path to the item to be obtained. Example is "/inventory/armor/emblems/013-001-363f4a25.json". /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of InGameItem. Otherwise, null. - public async Task> GetItem(string itemPath, string flightId) + public Task> GetItemAsync(string itemPath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(itemPath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{itemPath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -265,12 +324,16 @@ public async Task> GetI /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of LobbyHopperErrorMessageList that contains possible errors. Otherwise, returns null. - public async Task> GetLobbyErrorMessages(string flightId) + public Task> GetLobbyErrorMessagesAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Multiplayer/file/gameStartErrorMessages/LobbyHoppperErrorMessageList.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -278,12 +341,16 @@ public async Task /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of Metadata containing the information about in-game manufacturers and currencies. Otherwise, null. - public async Task> GetMetadata(string flightId) + public Task> GetMetadataAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/metadata/metadata.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -291,12 +358,16 @@ public async Task> GetMet /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of NetworkConfiguration. Otherwise, returns null. - public async Task> GetNetworkConfiguration(string flightId) + public Task> GetNetworkConfigurationAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Multiplayer/file/network/config.json?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -304,25 +375,31 @@ public async Task /// /// Path to the news collection. Example is "/articles/articles.json". + /// Cancellation token for the operation. /// If successful, returns a News instance containing the currently active news. Otherwise, returns null. - public async Task> GetNews(string filePath) + public Task> GetNewsAsync(string filePath, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(filePath); + + return this.GetAsync( $"/hi/news/file/{filePath}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Returns information about a message that is displayed when, I assume, authentication fails. + /// Returns information about a message that is displayed when authentication fails. /// /// It's unclear where this is actually used because the sample response is a test one, without any relevant context. /// + /// Cancellation token for the operation. /// If successful, an instance of OEConfiguration containing the message. Otherwise, null. - public async Task> GetNotAllowedInTitleMessage() + public Task> GetNotAllowedInTitleMessageAsync(CancellationToken cancellationToken = default) { - return await this.GetAsyncFullUrl( + return this.GetAsyncFullUrl( $"https://{HaloCoreEndpoints.GameCmsOrigin}.{HaloCoreEndpoints.ServiceDomain}/branches/hi/OEConfiguration/data/authfail/Default.json", - useSpartanToken: false); + useSpartanToken: false, + cancellationToken: cancellationToken); } /// @@ -330,24 +407,30 @@ public async Task> /// /// Type of progression file to be obtained. /// Path to the progression file. + /// Cancellation token for the operation. /// If successful, returns an instance of T, where T is the type of the progression file. Otherwise, returns null. - public async Task> GetProgressionFile(string filePath) + public Task> GetProgressionFileAsync(string filePath, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(filePath); + + return this.GetAsync( $"/hi/Progression/file/{filePath}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Get recommended drivers for the current version of Halo Infinite. + /// Gets recommended drivers for the current version of Halo Infinite. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of DriverManifest that contains details on supported drivers. Otherwise, returns null. - public async Task> GetRecommendedDrivers() + public Task> GetRecommendedDriversAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Specs/file/graphics/RecommendedDrivers.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -359,12 +442,17 @@ public async Task> /// /// The path to the season. Typical example is "Seasons/Season7.json" for the Lone Wolves season. /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of SeasonRewardTrack containing season information. Otherwise, returns null. - public async Task> GetSeasonRewardTrack(string seasonPath, string flightId) + public Task> GetSeasonRewardTrackAsync(string seasonPath, string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(seasonPath); + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/file/{seasonPath}?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -372,36 +460,44 @@ public async Task /// /// Unique identifier for the career path. Example value is "careerRank1". + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null with associated error details in within the result. - public async Task> GetCareerRanks(string careerPathId) + public Task> GetCareerRanksAsync(string careerPathId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(careerPathId); + + return this.GetAsync( $"/hi/Progression/file/RewardTracks/CareerRanks/{careerPathId}.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the currently available season calendar. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of that contains pointers to season details. Otherwise, returns null with associated error details in within the result. - public async Task> GetSeasonCalendar() + public Task> GetSeasonCalendarAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Progression/file/Calendars/Seasons/SeasonCalendar.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the currently available CSR season calendar. This is applicable for ranked games and usually delineates when the rank reset will happen. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of that contains pointers to season details. Otherwise, returns null with associated error details in within the result. - public async Task> GetCSRCalendar() + public Task> GetCSRCalendarAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/Progression/file/Csr/Calendars/CsrSeasonCalendar.json", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -409,12 +505,16 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideImages(string flightId) + public Task> GetGuideImagesAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/images/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -422,12 +522,16 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideMultiplayer(string flightId) + public Task> GetGuideMultiplayerAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Multiplayer/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -435,12 +539,16 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideNews(string flightId) + public Task> GetGuideNewsAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/News/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -448,12 +556,16 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideProgression(string flightId) + public Task> GetGuideProgressionAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Progression/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -461,12 +573,16 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideSpecs(string flightId) + public Task> GetGuideSpecsAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/Specs/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -474,22 +590,27 @@ public async Task> /// /// /// Unique ID for the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of GuideContainer containing file information. Otherwise, returns null. - public async Task> GetGuideTitleAuthorization(string flightId) + public Task> GetGuideTitleAuthorizationAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/hi/TitleAuthorization/guide/xo?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets a list of all available medals and their metadata. /// /// + /// Cancellation token for the operation. /// If successful, an instance of containing medal information. Otherwise, returns null and error details. - public async Task> GetMedalMetadata() + public Task> GetMedalMetadataAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync("/hi/Waypoint/file/medals/metadata.json"); + return this.GetAsync("/hi/Waypoint/file/medals/metadata.json", cancellationToken: cancellationToken); } /// @@ -497,32 +618,41 @@ public async Task> G /// /// /// JSON file associated with a playlist. Example is "a446725e-b281-414c-a21e-31b8700e95a1.json". + /// Cancellation token for the operation. /// If successful, an instance of containing playlist configuration. Otherwise, returns null and error details. - public async Task> GetMultiplayerPlaylistConfiguration(string playlistFile) + public Task> GetMultiplayerPlaylistConfigurationAsync(string playlistFile, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/Multiplayer/file/playlists/assets/{playlistFile}"); + ArgumentException.ThrowIfNullOrEmpty(playlistFile); + + return this.GetAsync( + $"/hi/Multiplayer/file/playlists/assets/{playlistFile}", + cancellationToken: cancellationToken); } /// /// Gets emblem mapping configuration. /// /// + /// Cancellation token for the operation. /// If successful, an instance of with emblem mapping. Otherwise, returns null and error details. - public async Task>, RawResponseContainer>> GetEmblemMapping() + public Task>, RawResponseContainer>> GetEmblemMappingAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync>>( - "/hi/Waypoint/file/images/emblems/mapping.json"); + return this.GetAsync>>( + "/hi/Waypoint/file/images/emblems/mapping.json", + cancellationToken: cancellationToken); } /// /// Gets a file from the Halo Waypoint service. /// /// Path to the file to be retrieved. + /// Cancellation token for the operation. /// If successful, a byte array containing the file contents. Otherwise, returns null and error details. - public async Task> GetGenericWaypointFile(string filePath) + public Task> GetGenericWaypointFileAsync(string filePath, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/Waypoint/file/{filePath}"); + ArgumentException.ThrowIfNullOrEmpty(filePath); + + return this.GetAsync($"/hi/Waypoint/file/{filePath}", cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/LobbyModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/LobbyModule.cs index ffbb797..c36eb5e 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/LobbyModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/LobbyModule.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -18,7 +19,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for lobby-related API operations including QoS servers and presence. /// - public class LobbyModule : ModuleBase + public sealed class LobbyModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -33,10 +34,11 @@ internal LobbyModule(ClientBase client) /// Gets a list of available lobby servers. /// /// + /// Cancellation token for the operation. /// A list of Server instances if the request is successful. Otherwise, returns null. - public async Task, RawResponseContainer>> GetQosServers() + public Task, RawResponseContainer>> GetQosServersAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync>("/titles/hi/qosservers"); + return this.GetAsync>("/titles/hi/qosservers", cancellationToken: cancellationToken); } /// @@ -44,12 +46,13 @@ public async Task, RawResponseContainer>> Ge /// /// /// Presence request, containing a list of Xuids representing Xbox Live players. + /// Cancellation token for the operation. /// If successful, an instance of representing the lobby details. Otherwise, null. - public async Task> Presence(LobbyPresenceRequestContainer presenceRequest) + public Task> GetPresenceAsync(LobbyPresenceRequestContainer presenceRequest, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(presenceRequest); - return await this.GetAsync("/hi/presence"); + return this.PostJsonAsync("/hi/presence", presenceRequest, cancellationToken: cancellationToken); } /// @@ -60,13 +63,20 @@ public async TaskThe player's numeric XUID. /// Audience for the join handle. Example value is "Friends". /// Platform for the join handle. Example value is "Discord". + /// Cancellation token for the operation. /// An instance of LobbyJoinHandle if the request is successful. Otherwise, returns null. /// It seems that this request requires a more "broad access" Spartan token that is generated by the game, and is not open to third-party apps. Additional investigation is required. - public async Task> ThirdPartyJoinHandle(string lobbyId, string player, string handleAudience, string handlePlatform) + public Task> GetThirdPartyJoinHandleAsync(string lobbyId, string player, string handleAudience, string handlePlatform, CancellationToken cancellationToken = default) { - return await this.GetAsyncFullUrl( + ArgumentException.ThrowIfNullOrEmpty(lobbyId); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(handleAudience); + ArgumentException.ThrowIfNullOrEmpty(handlePlatform); + + return this.GetAsyncFullUrl( $"https://{HaloCoreEndpoints.HaloInfiniteLobbyOrigin}.{HaloCoreEndpoints.ServiceDomain}/hi/lobbies/{lobbyId}/players/xuid({player})/thirdPartyJoinHandle?audience={handleAudience}&platform={handlePlatform}", - customHeaders: new List>() { new KeyValuePair("Accept-Language", "en-us") }); + customHeaders: new List>() { new KeyValuePair("Accept-Language", "en-us") }, + cancellationToken: cancellationToken); } /// @@ -77,10 +87,16 @@ public async Task> /// The player's numeric XUID. /// Authentication to be used. "st" represents Spartan token. /// Binary payload (Bond-encoded) that contains the lobby bootstrap logic. + /// Cancellation token for the operation. /// An instance of in the response container if successful. Otherwise, a null. - public async Task> JoinLobby(string lobbyId, string player, string auth, byte[] lobbyBootstrapPayload) + public Task> JoinLobbyAsync(string lobbyId, string player, string auth, byte[] lobbyBootstrapPayload, CancellationToken cancellationToken = default) { - return await this.Client.ExecuteAPIRequest( + ArgumentException.ThrowIfNullOrEmpty(lobbyId); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(auth); + ArgumentNullException.ThrowIfNull(lobbyBootstrapPayload); + + return this.Client.ExecuteAPIRequestAsync( $"https://{HaloCoreEndpoints.HaloInfiniteLobbyOrigin}.{HaloCoreEndpoints.ServiceDomain}/hi/lobbies/{lobbyId}/players/xuid({player})?auth={auth}", System.Net.Http.HttpMethod.Put, true, @@ -88,7 +104,8 @@ public async Task +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for settings-related API operations including clearances and flights. /// - public class SettingsModule : ModuleBase + public sealed class SettingsModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -28,15 +30,19 @@ internal SettingsModule(ClientBase client) } /// - /// Get a list of features enabled for a given flight. + /// Gets a list of features enabled for a given flight. /// /// Clearance ID/flight that is being used. + /// Cancellation token for the operation. /// An instance of FlightedFeatureFlags containing a list of enabled and disabled features if the request is successful. Otherwise, returns null. - public async Task> GetFlightedFeatureFlags(string flightId) + public Task> GetFlightedFeatureFlagsAsync(string flightId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(flightId); + + return this.GetAsync( $"/featureflags/hi?flight={flightId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -44,12 +50,16 @@ public async Task /// /// Release identifier. Examples seen are 1.4, 1.5, and 1.6. + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null. - public async Task> ActiveClearance(string release) + public Task> GetActiveClearanceAsync(string release, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(release); + + return this.GetAsync( $"/hi/clearances/active?release={release}", - useSpartanToken: false); + useSpartanToken: false, + cancellationToken: cancellationToken); } /// @@ -59,11 +69,17 @@ public async Task> /// Identifier associated with the sandbox. Typical value is UNUSED. /// Number of the game build the data is requested for. Example value is 211755.22.01.23.0549-0. /// Release identifier. Examples seen are 1.4 and 1.5. + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns null. - public async Task> ActiveFlight(string sandbox, string buildNumber, string release) + public Task> GetActiveFlightAsync(string sandbox, string buildNumber, string release, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/oban/flight-configurations/titles/hi/audiences/RETAIL/active?sandbox={sandbox}&build={buildNumber}&release={release}"); + ArgumentException.ThrowIfNullOrEmpty(sandbox); + ArgumentException.ThrowIfNullOrEmpty(buildNumber); + ArgumentException.ThrowIfNullOrEmpty(release); + + return this.GetAsync( + $"/oban/flight-configurations/titles/hi/audiences/RETAIL/active?sandbox={sandbox}&build={buildNumber}&release={release}", + cancellationToken: cancellationToken); } /// @@ -74,11 +90,18 @@ public async Task> /// Identifier associated with the sandbox. Typical value is UNUSED. /// Number of the game build the data is requested for. Example value is 211755.22.01.23.0549-0. /// Release identifier. Examples seen are 1.4 and 1.5. + /// Cancellation token for the operation. /// An instance of PlayerClearance if the request is successful. Otherwise, returns null. - public async Task> GetClearance(string audience, string sandbox, string buildNumber, string release) + public Task> GetClearanceAsync(string audience, string sandbox, string buildNumber, string release, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/oban/flight-configurations/titles/hi/audiences/{audience}/active?sandbox={sandbox}&build={buildNumber}&release={release}"); + ArgumentException.ThrowIfNullOrEmpty(audience); + ArgumentException.ThrowIfNullOrEmpty(sandbox); + ArgumentException.ThrowIfNullOrEmpty(buildNumber); + ArgumentException.ThrowIfNullOrEmpty(release); + + return this.GetAsync( + $"/oban/flight-configurations/titles/hi/audiences/{audience}/active?sandbox={sandbox}&build={buildNumber}&release={release}", + cancellationToken: cancellationToken); } /// @@ -90,27 +113,42 @@ public async Task> /// Identifier associated with the sandbox. Typical value is UNUSED. /// Number of the game build the data is requested for. Example value is 211755.22.01.23.0549-0. /// Release identifier. Examples seen are 1.4 and 1.5. + /// Cancellation token for the operation. /// An instance of PlayerClearance if the request is successful. Otherwise, returns null. - public async Task> GetPlayerClearance(string audience, string player, string sandbox, string buildNumber, string release) + public Task> GetPlayerClearanceAsync(string audience, string player, string sandbox, string buildNumber, string release, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(audience); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(sandbox); + ArgumentException.ThrowIfNullOrEmpty(buildNumber); + ArgumentException.ThrowIfNullOrEmpty(release); + + return this.GetAsync( $"/oban/flight-configurations/titles/hi/audiences/{audience}/players/xuid({player})/active?sandbox={sandbox}&build={buildNumber}&release={release}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// - /// Gets the player clearance/flight ID for RETAIL audience. + /// Gets the player clearance/flight ID for the RETAIL audience. /// /// /// The player's numeric XUID. /// Identifier associated with the sandbox. Typical value is UNUSED. /// Number of the game build the data is requested for. Example value is 211755.22.01.23.0549-0. /// Release identifier. Examples seen are 1.4 and 1.5. + /// Cancellation token for the operation. /// An instance of PlayerClearance if the request is successful. Otherwise, returns null. - public async Task> PlayerClearance(string player, string sandbox, string buildNumber, string release) + public Task> GetRetailPlayerClearanceAsync(string player, string sandbox, string buildNumber, string release, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/oban/flight-configurations/titles/hi/audiences/RETAIL/players/xuid({player})/active?sandbox={sandbox}&build={buildNumber}&release={release}"); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(sandbox); + ArgumentException.ThrowIfNullOrEmpty(buildNumber); + ArgumentException.ThrowIfNullOrEmpty(release); + + return this.GetAsync( + $"/oban/flight-configurations/titles/hi/audiences/RETAIL/players/xuid({player})/active?sandbox={sandbox}&build={buildNumber}&release={release}", + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/SkillModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/SkillModule.cs index c3e0be6..d23253d 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/SkillModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/SkillModule.cs @@ -5,8 +5,10 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -18,7 +20,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for skill-related API operations including CSR and match skill information. /// - public class SkillModule : ModuleBase + public sealed class SkillModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -38,13 +40,18 @@ internal SkillModule(ClientBase client) /// /// The unique match ID. /// List of numeric XUIDs for the players. + /// Cancellation token for the operation. /// An instance of representing player skills if the request was successful. Otherwise, returns null. - public async Task> GetMatchPlayerResult(string matchId, List playerIds) + public Task> GetMatchPlayerResultAsync(string matchId, List playerIds, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(matchId); + ArgumentNullException.ThrowIfNull(playerIds); + var formattedPlayerList = string.Join(",", playerIds.Select(id => $"xuid({id})")); - return await this.GetAsync( + return this.GetAsync( $"/hi/matches/{matchId}/skill?players={formattedPlayerList}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -54,13 +61,18 @@ public async Task> /// Unique ID for the playlist. /// List of numeric XUIDs for the players. /// Season identifier. Example value is "CsrSeason2-3". + /// Cancellation token for the operation. /// If successful, an instance of representing player CSRs. Otherwise, returns null. - public async Task> GetPlaylistCsr(string playlistId, List playerIds, string seasonId = "") + public Task> GetPlaylistCsrAsync(string playlistId, List playerIds, string seasonId = "", CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(playlistId); + ArgumentNullException.ThrowIfNull(playerIds); + var formattedPlayerList = string.Join(",", playerIds.Select(id => $"xuid({id})")); - return await this.GetAsync( + return this.GetAsync( $"/hi/playlist/{playlistId}/csrs?players={formattedPlayerList}&season={seasonId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/StatsModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/StatsModule.cs index dd126eb..a509d95 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/StatsModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/StatsModule.cs @@ -5,6 +5,8 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for stats-related API operations including match history and service records. /// - public class StatsModule : ModuleBase + public sealed class StatsModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -32,10 +34,13 @@ internal StatsModule(ClientBase client) /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of ChallengeDecksResponse containing deck information if request was successful. Return value is null otherwise. - public async Task> GetChallengeDecks(string player) + public Task> GetChallengeDecksAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/players/xuid({player})/decks"); + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync($"/hi/players/xuid({player})/decks", cancellationToken: cancellationToken); } /// @@ -43,10 +48,13 @@ public async Task /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of PlayerMatchCount containing match counts if request was successful. Return value is null otherwise. - public async Task> GetMatchCount(string player) + public Task> GetMatchCountAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/players/xuid({player})/matches/count"); + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync($"/hi/players/xuid({player})/matches/count", cancellationToken: cancellationToken); } /// @@ -57,11 +65,16 @@ public async Task /// Start value for the counter, from which data should be returned. /// Number of matches to return. Maximum is 25. Going beyond 25 will result in only 25 values being returned. /// Type of matches to query. + /// Cancellation token for the operation. /// An instance of containing match metadata if request was successful. Return value is null otherwise. - public async Task> GetMatchHistory(string player, int start, int count, MatchType type) + public Task> GetMatchHistoryAsync(string player, int start, int count, MatchType type, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/players/xuid({player})/matches?start={start}&count={count}&type={type}"); + ArgumentException.ThrowIfNullOrEmpty(player); + ValidateRange(count, 1, 25, nameof(count)); + + return this.GetAsync( + $"/hi/players/xuid({player})/matches?start={start}&count={count}&type={type}", + cancellationToken: cancellationToken); } /// @@ -69,23 +82,31 @@ public async Task /// /// Match ID in GUID format. + /// Cancellation token for the operation. /// An instance of MatchStats containing match metadata if request was successful. Return value is null otherwise. - public async Task> GetMatchStats(string matchId) + public Task> GetMatchStatsAsync(string matchId, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/matches/{matchId}/stats"); + ArgumentException.ThrowIfNullOrEmpty(matchId); + + return this.GetAsync($"/hi/matches/{matchId}/stats", cancellationToken: cancellationToken); } /// - /// Get challenge progression associated with a given match. + /// Gets challenge progression associated with a given match. /// /// /// The player's numeric XUID. /// Match ID in GUID format. + /// Cancellation token for the operation. /// An instance of MatchProgression containing match challenge progression metadata if request was successful. Return value is null otherwise. - public async Task> GetPlayerMatchProgression(string player, string matchId) + public Task> GetPlayerMatchProgressionAsync(string player, string matchId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/players/xuid({player})/matches/{matchId}/progression"); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(matchId); + + return this.GetAsync( + $"/hi/players/xuid({player})/matches/{matchId}/progression", + cancellationToken: cancellationToken); } /// @@ -93,10 +114,13 @@ public async Task /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of MatchesPrivacy containing match privacy metadata if request was successful. Return value is null otherwise. - public async Task?> MatchPrivacy(string player) + public Task> GetMatchPrivacyAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/players/xuid({player})/matches-privacy"); + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync($"/hi/players/xuid({player})/matches-privacy", cancellationToken: cancellationToken); } /// @@ -107,13 +131,17 @@ public async Task /// The player's numeric XUID. /// Type of games for which to get the service record. /// The ID of the season for which additional stats are pulled. Example value is "Seasons/Season7.json". + /// Cancellation token for the operation. /// If successful, an instance of containing service record information. Otherwise, returns null with additional details about the error. - public async Task?> GetPlayerServiceRecordByXuid(string xuid, LifecycleMode mode, string seasonId = "") + public Task> GetPlayerServiceRecordByXuidAsync(string xuid, LifecycleMode mode, string seasonId = "", CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(xuid); + var seasonMarker = !string.IsNullOrWhiteSpace(seasonId) ? $"?seasonId={seasonId}" : string.Empty; - return await this.GetAsync( - $"/hi/players/xuid({xuid})/{mode}/servicerecord{seasonMarker}"); + return this.GetAsync( + $"/hi/players/xuid({xuid})/{mode}/servicerecord{seasonMarker}", + cancellationToken: cancellationToken); } /// @@ -124,13 +152,17 @@ public async Task /// The player's gamertag. Example value is "BreadKrtek". /// Type of games for which to get the service record. /// The ID of the season for which additional stats are pulled. Example value is "Seasons/Season7.json". + /// Cancellation token for the operation. /// If successful, an instance of containing service record information. Otherwise, returns null with additional details about the error. - public async Task?> GetPlayerServiceRecordByGamertag(string gamertag, LifecycleMode mode, string seasonId = "") + public Task> GetPlayerServiceRecordByGamertagAsync(string gamertag, LifecycleMode mode, string seasonId = "", CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(gamertag); + var seasonMarker = !string.IsNullOrWhiteSpace(seasonId) ? $"?seasonId={seasonId}" : string.Empty; - return await this.GetAsync( - $"/hi/players/{gamertag}/{mode}/servicerecord{seasonMarker}"); + return this.GetAsync( + $"/hi/players/{gamertag}/{mode}/servicerecord{seasonMarker}", + cancellationToken: cancellationToken); } /// @@ -138,10 +170,13 @@ public async Task /// /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// An instance of containing daily experience if request was successful. Return value is null otherwise. - public async Task> GetPlayerDailyCustomExperience(string player) + public Task> GetPlayerDailyCustomExperienceAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/hi/players/xuid({player})/customexperience"); + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync($"/hi/players/xuid({player})/customexperience", cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/TextModerationModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/TextModerationModule.cs index 8bde1b5..12d6268 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/TextModerationModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/TextModerationModule.cs @@ -5,6 +5,8 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for text moderation related API operations. /// - public class TextModerationModule : ModuleBase + public sealed class TextModerationModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -32,24 +34,30 @@ internal TextModerationModule(ClientBase client) /// /// /// Key ID. Full list can be obtained by a call to GetSigningKeys. + /// Cancellation token for the operation. /// An instance of Key containing a single signing key data if request was successful. Return value is null otherwise. - public async Task> GetSigningKey(string keyId) + public Task> GetSigningKeyAsync(string keyId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(keyId); + + return this.GetAsync( $"/hi/moderation-proof-keys/{keyId}", - useSpartanToken: false); + useSpartanToken: false, + cancellationToken: cancellationToken); } /// /// Gets a list of available moderation proof signing keys. /// /// + /// Cancellation token for the operation. /// An instance of ModerationProofKeys containing signing key data if request was successful. Return value is null otherwise. - public async Task> GetSigningKeys() + public Task> GetSigningKeysAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/moderation-proof-keys", - useSpartanToken: false); + useSpartanToken: false, + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcDiscoveryModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcDiscoveryModule.cs index 99adf7d..4d24aa2 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcDiscoveryModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcDiscoveryModule.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -19,7 +20,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for UGC Discovery API operations including searching, manifests, maps, playlists, and game variants. /// - public class UgcDiscoveryModule : ModuleBase + public sealed class UgcDiscoveryModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -35,35 +36,43 @@ internal UgcDiscoveryModule(ClientBase client) /// /// /// Build GUID. Example value is "5df1784f-72a9-4207-a529-2f91eb37fc1f". + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns a null object along with error details. - public async Task> GetManifestByBuildGuid(string buildGuid) + public Task> GetManifestByBuildGuidAsync(string buildGuid, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/manifests/guids/{buildGuid}/game"); + ArgumentException.ThrowIfNullOrEmpty(buildGuid); + + return this.GetAsync( + $"/hi/manifests/guids/{buildGuid}/game", + cancellationToken: cancellationToken); } /// /// Gets the collection of Forge templates (canvases) such as Arid, Seafloor, Mires, Void, Argyle, and more. These are suggested maps from which to start when making a new map in Forge. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing the templates. Otherwise, returns a null object along with error details. - public async Task> GetForgeTemplates() + public Task> GetForgeTemplatesAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/projects/bf0e9bab-6fed-47a4-8bf7-bfd4422ee552", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Gets the Forge Mode Creator Variants, used for mode creator system inside Forge. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing the variants. Otherwise, returns a null object along with error details. - public async Task> GetForgeModeCategories() + public Task> GetForgeModeCategoriesAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/projects/aff73c44-0771-468f-b9cf-5c52eee7ab4c", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -71,12 +80,14 @@ public async Task> GetForg /// /// Important to note that the API currently does not return a viable result while being listed in the endpoint configuration. /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing the list of assets in the community tab. Otherwise, returns a null object along with error details. - public async Task> GetCommunityTab() + public Task> GetCommunityTabAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/projects/90f9e508-99ce-411c-bf88-7bf12b5e9f52", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -84,11 +95,15 @@ public async Task> GetComm /// /// /// Film asset ID. This is not the same as the match ID, but can be retrieved from match details. + /// Cancellation token for the operation. /// If successful, returns an instance of containing film metadata. Otherwise, returns a null object along with error details. - public async Task> GetFilm(string assetId) + public Task> GetFilmAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/films/{assetId}"); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( + $"/hi/films/{assetId}", + cancellationToken: cancellationToken); } /// @@ -98,12 +113,14 @@ public async Task> GetFilm(st /// This endpoint is used within the content browser in Halo Infinite. /// /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing the list of recommended assets. Otherwise, returns a null object along with error details. - public async Task> Get343Recommended() + public Task> Get343RecommendedAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/projects/712add52-f989-48e1-b3bb-ac7cd8a1c17a", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -112,12 +129,17 @@ public async Task> Get343R /// /// Unique asset ID for the engine game variant. /// Unique ID for the asset version for the engine game variant. + /// Cancellation token for the operation. /// If successful, returns an instance of EngineGameVariant containing appropriate metadata. Otherwise, returns null. - public async Task> GetEngineGameVariant(string assetId, string versionId) + public Task> GetEngineGameVariantAsync(string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( $"/hi/engineGameVariants/{assetId}/versions/{versionId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -125,12 +147,16 @@ public async Task /// /// Unique asset ID for the engine game variant. + /// Cancellation token for the operation. /// If successful, returns an instance of EngineGameVariant containing appropriate metadata. Otherwise, returns null. - public async Task> GetEngineGameVariantWithoutVersion(string assetId) + public Task> GetEngineGameVariantWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/engineGameVariants/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -140,12 +166,18 @@ public async TaskUnique asset ID for the manifest. Example value is "6369c3a6-390e-496c-ab71-93c326347327". /// Unique version ID for the manifest. Example value is "9a348b5b-08aa-41c2-8b3a-681870c78a76". /// ID of the currently active flight. + /// Cancellation token for the operation. /// If successful, an instance of representing the asset details. Otherwise, returns null. - public async Task> GetManifest(string assetId, string versionId, string clearanceId) + public Task> GetManifestAsync(string assetId, string versionId, string clearanceId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + ArgumentException.ThrowIfNullOrEmpty(clearanceId); + + return this.GetAsync( $"/hi/manifests/{assetId}/versions/{versionId}?clearanceId={clearanceId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -153,11 +185,15 @@ public async Task> GetMan /// /// /// Build for which the manifest needs to be obtained. Maps to official Halo builds, such as 6.10022.10499. + /// Cancellation token for the operation. /// An instance of Manifest containing game manifest information if request is successful. Otherwise, returns null. - public async Task> GetManifestByBuild(string buildNumber) + public Task> GetManifestByBuildAsync(string buildNumber, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/manifests/builds/{buildNumber}/game"); + ArgumentException.ThrowIfNullOrEmpty(buildNumber); + + return this.GetAsync( + $"/hi/manifests/builds/{buildNumber}/game", + cancellationToken: cancellationToken); } /// @@ -166,12 +202,17 @@ public async Task> GetMan /// /// Unique map ID. For example, the ID for the Recharge map is "8420410b-044d-44d7-80b6-98a766c8c39f". /// Unique version ID for a map. For example, for the Recharge map a version is "068c0974-f748-41ba-b457-b8fed603576e". + /// Cancellation token for the operation. /// An instance of Map containing map metadata if request is successful. Otherwise, returns null. - public async Task> GetMap(string assetId, string versionId) + public Task> GetMapAsync(string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( $"/hi/maps/{assetId}/versions/{versionId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -185,12 +226,18 @@ public async Task> GetMap(stri /// Unique ID for the map and mode combination. /// Unique version ID for the map and mode combination. /// ID of the currently active flight. + /// Cancellation token for the operation. /// An instance of MapModePair containing map metadata if request is successful. Otherwise, returns null. - public async Task> GetMapModePair(string assetId, string versionId, string clearanceId) + public Task> GetMapModePairAsync(string assetId, string versionId, string clearanceId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + ArgumentException.ThrowIfNullOrEmpty(clearanceId); + + return this.GetAsync( $"/hi/mapModePairs/{assetId}/versions/{versionId}?clearanceId={clearanceId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -198,12 +245,16 @@ public async Task> Get /// /// /// Unique ID for the map and mode combination. Example value is "b6aca0c7-8ba7-4066-bf91-693571374c3c" for "sgh_interlock". + /// Cancellation token for the operation. /// If successful, returns an instance of representing the map and mode combination. Otherwise, returns null. - public async Task> GetMapModePairWithoutVersion(string assetId) + public Task> GetMapModePairWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/mapModePairs/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -211,12 +262,16 @@ public async Task> Get /// /// /// Unique map ID. For example, the ID for the Recharge map is "8420410b-044d-44d7-80b6-98a766c8c39f". + /// Cancellation token for the operation. /// An instance of Map containing map metadata if request is successful. Otherwise, returns null. - public async Task> GetMapWithoutVersion(string assetId) + public Task> GetMapWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/maps/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -226,12 +281,18 @@ public async Task> GetMapWitho /// Unique asset ID for the playlist. /// Unique version ID for the playlist. /// ID of the currently active flight. + /// Cancellation token for the operation. /// If successful, returns an instance of Playlist containing playlist information. Otherwise, returns null. - public async Task> GetPlaylist(string assetId, string versionId, string clearanceId) + public Task> GetPlaylistAsync(string assetId, string versionId, string clearanceId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + ArgumentException.ThrowIfNullOrEmpty(clearanceId); + + return this.GetAsync( $"/hi/playlists/{assetId}/versions/{versionId}?clearanceId={clearanceId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -239,12 +300,16 @@ public async Task> GetPla /// /// /// Unique asset ID for the playlist. + /// Cancellation token for the operation. /// If successful, returns an instance of representing the targeted playlist. Otherwise, returns null. - public async Task> GetPlaylistWithoutVersion(string assetId) + public Task> GetPlaylistWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/playlists/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -253,12 +318,17 @@ public async Task> GetPla /// /// Unique asset ID for the prefab. /// Unique version ID for the prefab. + /// Cancellation token for the operation. /// If successful, returns a instance representing the specific prefab. Otherwise, returns null. - public async Task> GetPrefab(string assetId, string versionId) + public Task> GetPrefabAsync(string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( $"/hi/prefabs/{assetId}/versions/{versionId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -266,12 +336,16 @@ public async Task> GetPrefa /// /// /// Unique asset ID for the prefab. + /// Cancellation token for the operation. /// If successful, returns a instance representing the specific prefab. Otherwise, returns null. - public async Task> GetPrefabWithoutVersion(string assetId) + public Task> GetPrefabWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/prefabs/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -280,12 +354,17 @@ public async Task> GetPrefa /// /// Unique asset ID representing the project. Example asset ID currently active is the custom game manifest ID: "a9dc0785-2a99-4fec-ba6e-0216feaaf041". /// Version ID for the project. As an example, a version of a production manifest is "a4e68648-f994-44bb-853e-d09ee224d799". + /// Cancellation token for the operation. /// An instance of Project containing current game project information if request is successful. Otherwise, returns null. - public async Task> GetProject(string assetId, string versionId) + public Task> GetProjectAsync(string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( $"/hi/projects/{assetId}/versions/{versionId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -293,24 +372,30 @@ public async Task> GetProj /// /// /// Unique asset ID representing the project. Example asset ID currently active is the custom game manifest ID: "a9dc0785-2a99-4fec-ba6e-0216feaaf041". + /// Cancellation token for the operation. /// An instance of Project containing current game project information if request is successful. Otherwise, returns null. - public async Task> GetProjectWithoutVersion(string assetId) + public Task> GetProjectWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/projects/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// /// Returns information about available tags that can be associated with game assets. /// /// + /// Cancellation token for the operation. /// An instance of TagInfo containing a list of tags if the request is successful. Otherwise, returns null. - public async Task> GetTagsInfo() + public Task> GetTagsInfoAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync( + return this.GetAsync( "/hi/info/tags", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -319,12 +404,17 @@ public async Task> GetTags /// /// Unique ID for the game asset. For example, for "Fiesta - Slayer" game mode, the asset ID is "aca7bbf8-7a18-4aae-8785-1bd3f58275fd". /// Version for the asset to obtain. Example value is "3685f6b2-2860-4e98-9d13-513087edb465". + /// Cancellation token for the operation. /// An instance of UGCGameVariant containing game variant metadata if the request is successful. Otherwise, returns null. - public async Task> GetUgcGameVariant(string assetId, string versionId) + public Task> GetUgcGameVariantAsync(string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( $"/hi/ugcGameVariants/{assetId}/versions/{versionId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -332,12 +422,16 @@ public async Task> /// /// /// Unique ID for the game asset. For example, for "Fiesta - Slayer" game mode, the asset ID is "aca7bbf8-7a18-4aae-8785-1bd3f58275fd". + /// Cancellation token for the operation. /// An instance of UGCGameVariant containing asset metadata if the request is successful. Otherwise, returns null. - public async Task> GetUgcGameVariantWithoutVersion(string assetId) + public Task> GetUgcGameVariantWithoutVersionAsync(string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/hi/ugcGameVariants/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -360,10 +454,11 @@ public async Task> /// Maximum date modified. Optional. /// Minimum date published. Optional. /// Maximum date published. Optional. + /// Cancellation token for the operation. /// If successful, returns an instance of SearchResultsContainer containing assets. Otherwise, returns null. - public async Task> Search(int start = 0, int count = 12, bool includeTimes = true, string sort = "DatePublishedUtc", ResultOrder order = ResultOrder.Desc, List? assetKinds = null, string? author = null, string? term = null, List? tags = null, decimal? averageRatingMin = null, DateTime? fromDateCreatedUtc = null, DateTime? toDateCreatedUtc = null, DateTime? fromDateModifiedUtc = null, DateTime? toDateModifiedUtc = null, DateTime? fromDatePublishedUtc = null, DateTime? toDatePublishedUtc = null) + public Task> SearchAsync(int start = 0, int count = 12, bool includeTimes = true, string sort = "DatePublishedUtc", ResultOrder order = ResultOrder.Desc, List? assetKinds = null, string? author = null, string? term = null, List? tags = null, decimal? averageRatingMin = null, DateTime? fromDateCreatedUtc = null, DateTime? toDateCreatedUtc = null, DateTime? fromDateModifiedUtc = null, DateTime? toDateModifiedUtc = null, DateTime? fromDatePublishedUtc = null, DateTime? toDatePublishedUtc = null, CancellationToken cancellationToken = default) { - var baseSearchString = $"/hi/search?start={start}&count={count}&include-times={includeTimes}&sort={sort}&order={order}&"; + var baseSearchString = $"/hi/search?start={start}&count={count}&include-times={includeTimes}&sort={sort}&order={order}"; if (!string.IsNullOrEmpty(author)) { @@ -424,9 +519,10 @@ public async Task( + return this.GetAsync( baseSearchString, - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -435,11 +531,15 @@ public async TaskDespite the name of this request, the data captured here is not actually a movie but rather a full re-creation of the match, using in-game assets and player positions. /// /// Unique ID for the match. + /// Cancellation token for the operation. /// An instance of Film containing film metadata if the request is successful. Otherwise, returns null. - public async Task> SpectateByMatchId(string matchId) + public Task> SpectateByMatchIdAsync(string matchId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/films/matches/{matchId}/spectate"); + ArgumentException.ThrowIfNullOrEmpty(matchId); + + return this.GetAsync( + $"/hi/films/matches/{matchId}/spectate", + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcModule.cs index 8aa56e6..890c3fc 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/HaloInfinite/UgcModule.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -20,7 +21,7 @@ namespace Den.Dev.Grunt.Core.Modules.HaloInfinite /// /// Module for UGC (User Generated Content) authoring API operations including asset management, favorites, and ratings. /// - public class UgcModule : ModuleBase + public sealed class UgcModule : ModuleBase { /// /// Initializes a new instance of the class. @@ -40,13 +41,21 @@ internal UgcModule(ClientBase client) /// Unique asset ID. Example value is "3895f3d4-2493-4b84-ae18-876ad3ab344d" for a UGC game variant. /// The player's numeric XUID. /// A object with the AuthoringRole set to the desired permission level. + /// Cancellation token for the operation. /// If successful, returns an instance of with permission details. Otherwise, returns a null result object with attached error details. - public async Task> GrantOrRevokePermissions(string title, string assetType, string assetId, string player, Permission permission) + public Task> GrantOrRevokePermissionsAsync(string title, string assetType, string assetId, string player, Permission permission, CancellationToken cancellationToken = default) { - return await this.PatchJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentNullException.ThrowIfNull(permission); + + return this.PatchJsonAsync( $"/{title}/{assetType}/{assetId}/permissions/xuid({player})", permission, - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -57,11 +66,18 @@ public async Task> Gran /// The player's numeric XUID. /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "373f3d27-cb4c-4d7b-b6c9-7757de3c1133" for "Arena:King of the Hill". + /// Cancellation token for the operation. /// If successful, returns an instance of FavoriteAsset containing asset information. Otherwise, returns null. - public async Task> CheckAssetPlayerBookmark(string title, string player, string assetType, string assetId) + public Task> CheckAssetPlayerBookmarkAsync(string title, string player, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/{title}/players/xuid({player})/favorites/{assetType}/{assetId}"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( + $"/{title}/players/xuid({player})/favorites/{assetType}/{assetId}", + cancellationToken: cancellationToken); } /// @@ -72,13 +88,20 @@ public async Task> C /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Container for the session descriptor that starts the new version. + /// Cancellation token for the operation. /// If version creation is successful, returns an instance of AuthoringAssetVersion. Otherwise, returns null. - public async Task> CreateAssetVersionAgnostic(string title, string assetType, string assetId, AuthoringSessionSourceStarter starter) + public Task> CreateAssetVersionAgnosticAsync(string title, string assetType, string assetId, AuthoringSessionSourceStarter starter, CancellationToken cancellationToken = default) { - return await this.PostJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentNullException.ThrowIfNull(starter); + + return this.PostJsonAsync( $"/{title}/{assetType}/{assetId}/versions", starter, - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -87,12 +110,18 @@ public async TaskTitle which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If deletion is successful, returns true. Otherwise, returns false. - public async Task> DeleteAllVersions(string title, string assetType, string assetId) + public Task> DeleteAllVersionsAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.DeleteAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.DeleteAsync( $"/{title}/{assetType}/{assetId}/versions", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -101,12 +130,18 @@ public async Task> DeleteAllV /// Title which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If deletion is successful, returns true. Otherwise, returns false. - public async Task> DeleteAsset(string title, string assetType, string assetId) + public Task> DeleteAssetAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.DeleteAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.DeleteAsync( $"/{title}/{assetType}/{assetId}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -116,11 +151,18 @@ public async Task> DeleteAsse /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Unique ID for the version of the asset. + /// Cancellation token for the operation. /// If deletion is successful, returns true. Otherwise, returns false. - public async Task> DeleteVersion(string title, string assetType, string assetId, string versionId) + public Task> DeleteVersionAsync(string title, string assetType, string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.DeleteAsync( - $"/{title}/{assetType}/{assetId}/versions/{versionId}"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.DeleteAsync( + $"/{title}/{assetType}/{assetId}/versions/{versionId}", + cancellationToken: cancellationToken); } /// @@ -129,29 +171,41 @@ public async Task> DeleteVers /// Title which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If session termination is successful, return true. Otherwise, returns false. - public async Task> EndSession(string title, string assetType, string assetId) + public Task> EndSessionAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.DeleteAsync( - $"/{title}/{assetType}/{assetId}/sessions/active"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.DeleteAsync( + $"/{title}/{assetType}/{assetId}/sessions/active", + cancellationToken: cancellationToken); } /// /// Favorites an asset for the player. /// /// - /// This method expects a JSON body, but I don't yet know what the underlying data structure is. + /// This method expects a JSON body. The underlying data structure for the request body has not been fully determined. /// /// /// The player's numeric XUID. /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of FavoriteAsset confirming the addition of the asset to favorites. Otherwise, returns null. - public async Task> FavoriteAnAsset(string player, string assetType, string assetId) + public Task> FavoriteAnAssetAsync(string player, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.PutAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.PutAsync( $"/hi/players/xuid({player})/favorites/{assetType}/{assetId}", - "{}"); + "{}", + cancellationToken: cancellationToken); } /// @@ -161,23 +215,33 @@ public async Task> F /// Title which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAsset containing authoring metadata. Otherwise, returns null. - public async Task> GetAsset(string title, string assetType, string assetId) + public Task> GetAssetAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/{title}/{assetType}/{assetId}"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( + $"/{title}/{assetType}/{assetId}", + cancellationToken: cancellationToken); } /// /// Returns a binary blob using its path as a reference. /// /// Path to the blob to be obtained. + /// Cancellation token for the operation. /// If successful, returns a binary blob containing file data. Otherwise, returns null. - public async Task> GetBlob(string blobPath) + public Task> GetBlobAsync(string blobPath, CancellationToken cancellationToken = default) { - return await this.GetAsyncFullUrl( + ArgumentException.ThrowIfNullOrEmpty(blobPath); + + return this.GetAsyncFullUrl( $"https://blobs-infiniteugc.{HaloCoreEndpoints.ServiceDomain}/{blobPath}", - useSpartanToken: false); + useSpartanToken: false, + cancellationToken: cancellationToken); } /// @@ -189,12 +253,17 @@ public async Task> GetBlob( /// /// Title which contains the asset. An example value here is "hi". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersion containing film data in the CustomData property. Otherwise, returns null. - public async Task> GetLatestAssetVersionFilm(string title, string assetId) + public Task> GetLatestAssetVersionFilmAsync(string title, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/{title}/films/{assetId}/versions/latest", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -207,12 +276,18 @@ public async TaskTitle which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersion containing version metadata for an asset. Otherwise, returns null. - public async Task> GetLatestAssetVersionAgnostic(string title, string assetType, string assetId) + public Task> GetLatestAssetVersionAgnosticAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/{title}/{assetType}/{assetId}/versions/latest", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -222,12 +297,18 @@ public async TaskTitle which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersion containing version metadata for a published asset. Otherwise, returns null. - public async Task> GetPublishedVersion(string title, string assetType, string assetId) + public Task> GetPublishedVersionAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( $"/{title}/{assetType}/{assetId}/versions/published", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -238,11 +319,18 @@ public async TaskType of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Unique ID for the version of the asset. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersion that contains asset version information. Otherwise, returns null. - public async Task> GetSpecificAssetVersion(string title, string assetType, string assetId, string versionId) + public Task> GetSpecificAssetVersionAsync(string title, string assetType, string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/{title}/{assetType}/{assetId}/versions/{versionId}"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.GetAsync( + $"/{title}/{assetType}/{assetId}/versions/{versionId}", + cancellationToken: cancellationToken); } /// @@ -255,11 +343,17 @@ public async TaskTitle which contains the asset. An example value here is "hi". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersionContainer that contains information about all available versions for an asset. Otherwise, returns null. - public async Task> ListAllVersions(string title, string assetType, string assetId) + public Task> ListAllVersionsAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/{title}/{assetType}/{assetId}/versions"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( + $"/{title}/{assetType}/{assetId}/versions", + cancellationToken: cancellationToken); } /// @@ -278,18 +372,24 @@ public async TaskDetermines whether results are ordered in descending or ascending order. /// List of keywords by which to filter. /// Type of asset to return. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetContainer containing information about assets a player owns. Otherwise, returns null. - public async Task> ListPlayerAssets(string title, string player, int start, int count, bool includeTimes, string sort, ResultOrder order, List keywords, AssetKind kind) + public Task> ListPlayerAssetsAsync(string title, string player, int start, int count, bool includeTimes, string sort, ResultOrder order, List keywords, AssetKind kind, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(sort); + var formattedKeywordList = string.Empty; if (keywords != null && keywords.Count > 0) { formattedKeywordList = string.Join(",", keywords); } - return await this.GetAsync( + return this.GetAsync( $"/{title}/players/xuid({player})/assets?start={start}&count={count}&include-times={includeTimes}&sort={sort}&order={order}&keywords={formattedKeywordList}&kind={kind}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -301,12 +401,17 @@ public async Task /// The player's numeric XUID. /// Type of asset to check. Example value is "UgcGameVariants". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringFavoritesContainer containing the list of favorites. Otherwise, returns null. - public async Task> ListPlayerFavorites(string player, string assetType) + public Task> ListPlayerFavoritesAsync(string player, string assetType, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + + return this.GetAsync( $"/hi/players/xuid({player})/favorites/{assetType}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -317,12 +422,16 @@ public async Task /// /// The player's numeric XUID. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringFavoritesContainer containing the list of favorites. Otherwise, returns null. - public async Task> ListPlayerFavoritesAgnostic(string player) + public Task> ListPlayerFavoritesAgnosticAsync(string player, CancellationToken cancellationToken = default) { - return await this.GetAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + + return this.GetAsync( $"/hi/players/xuid({player})/favorites", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -334,13 +443,21 @@ public async TaskUnique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Unique ID for the asset version to be published. /// Updated asset version with custom configuration. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetVersion containing the changes. Otherwise, returns null. - public async Task> PatchAssetVersion(string title, string assetType, string assetId, string versionId, AuthoringAssetVersion patchedAsset) + public Task> PatchAssetVersionAsync(string title, string assetType, string assetId, string versionId, AuthoringAssetVersion patchedAsset, CancellationToken cancellationToken = default) { - return await this.PatchJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + ArgumentNullException.ThrowIfNull(patchedAsset); + + return this.PatchJsonAsync( $"/{title}/{assetType}/{assetId}/versions/{versionId}", patchedAsset, - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -353,13 +470,20 @@ public async TaskUnique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Unique ID for the asset version to be published. /// ID of the currently active flight. + /// Cancellation token for the operation. /// If the publishing process is successful, returns true. Otherwise, returns false. - public async Task> PublishAssetVersion(string assetType, string assetId, string versionId, string clearanceId) + public Task> PublishAssetVersionAsync(string assetType, string assetId, string versionId, string clearanceId, CancellationToken cancellationToken = default) { - return await this.PostAsync( + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + ArgumentException.ThrowIfNullOrEmpty(clearanceId); + + return this.PostAsync( $"/hi/{assetType}/{assetId}/publish/{versionId}?clearanceId={clearanceId}", "{}", - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -372,11 +496,17 @@ public async Task> PublishAss /// The player's numeric XUID. /// Type of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetRating containing rating information. Otherwise, returns null. - public async Task> GetAssetRatings(string player, string assetType, string assetId) + public Task> GetAssetRatingsAsync(string player, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.GetAsync( - $"/hi/players/xuid({player})/ratings/{assetType}/{assetId}"); + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.GetAsync( + $"/hi/players/xuid({player})/ratings/{assetType}/{assetId}", + cancellationToken: cancellationToken); } /// @@ -387,12 +517,19 @@ public async TaskType of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// An object containing asset rating information. Rating should be set in CustomData. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAssetRating containing the updated rating. Otherwise, returns null. - public async Task> RateAnAsset(string player, string assetType, string assetId, AuthoringAssetRating rating) + public Task> RateAnAssetAsync(string player, string assetType, string assetId, AuthoringAssetRating rating, CancellationToken cancellationToken = default) { - return await this.PutJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentNullException.ThrowIfNull(rating); + + return this.PutJsonAsync( $"/hi/players/xuid({player})/ratings/{assetType}/{assetId}", - rating); + rating, + cancellationToken: cancellationToken); } /// @@ -403,37 +540,47 @@ public async TaskType of asset to check. Example value is "UgcGameVariants". /// Unique ID for the asset. Example value is "f96f57e2-9f15-45c5-83ac-5775a48d2ba8" for "Attrition-Default-UGC". /// Instance of containing the report for the asset. + /// Cancellation token for the operation. /// If successful, returns an instance of AssetReport containing the report information. Otherwise, returns null. - public async Task> ReportAnAsset(string player, string assetType, string assetId, AssetReport report) + public Task> ReportAnAssetAsync(string player, string assetType, string assetId, AssetReport report, CancellationToken cancellationToken = default) { - return await this.PutJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(player); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentNullException.ThrowIfNull(report); + + return this.PutJsonAsync( $"/hi/players/xuid({player})/reports/{assetType}/{assetId}", - report); + report, + cancellationToken: cancellationToken); } /// /// API for creating new assets. /// /// - /// This API is used to create new assets in the user's file browser. The game generally uses Bond-encoded requests, so it's - /// still up to discovery to figure out what the values for the POST request are. - /// TODO: Need to figure out what the actual data model is for the POST request. + /// This API is used to create new assets in the user's file browser. The game generally uses Bond-encoded requests. + /// The exact data model for the POST request body has not been fully determined. /// /// /// Title for the game for which the authoring session needs to be spawned. Example variant is "hi" for "Halo Infinite". /// Type of asset to check. Example value is "UgcGameVariants", "Maps", or "Prefabs". /// Asset definition, containing information about the asset to be created. /// Content type to be used for the request. Default value uses the Bond encoding. + /// Cancellation token for the operation. /// If successful, returns an instance of AuthoringAsset containing asset information. Otherwise, returns null. - public async Task> SpawnAsset(string title, string assetType, object? asset = null, APIContentType contentType = APIContentType.BondCompactBinary) + public Task> SpawnAssetAsync(string title, string assetType, object asset, APIContentType contentType = APIContentType.BondCompactBinary, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); ArgumentNullException.ThrowIfNull(asset); - return await this.PostJsonAsync( + return this.PostJsonAsync( $"/{title}/{assetType}", - asset!, + asset, useClearance: true, - contentType: contentType); + contentType: contentType, + cancellationToken: cancellationToken); } /// @@ -448,13 +595,20 @@ public async Task> /// Unique asset ID for the asset type specified earlier. /// Determines whether to include the container SAS in the response or not. Setting this value to "true" will result in a 403 Forbidden error. /// Starter object that describes who is starting the session and the previous version of the asset. + /// Cancellation token for the operation. /// If successful, returns an instance of AssetAuthoringSession with details about the created session. Otherwise, returns null. - public async Task> StartSessionAgnostic(string title, string assetType, string assetId, bool includeContainerSas, AuthoringSessionStarter starter) + public Task> StartSessionAgnosticAsync(string title, string assetType, string assetId, bool includeContainerSas, AuthoringSessionStarter starter, CancellationToken cancellationToken = default) { - return await this.PostJsonAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentNullException.ThrowIfNull(starter); + + return this.PostJsonAsync( $"/{title}/{assetType}/{assetId}/sessions?include-container-sas={includeContainerSas}", starter, - useClearance: true); + useClearance: true, + cancellationToken: cancellationToken); } /// @@ -469,12 +623,18 @@ public async TaskType of asset to check. Example value is "UgcGameVariants". /// Unique asset ID for the asset type specified earlier. /// Determines whether to include the container SAS in the response or not. Setting this value to "true" will result in a 403 Forbidden error. + /// Cancellation token for the operation. /// If successful, returns an instance of AssetAuthoringSession with details about the created session. Otherwise, returns null. - public async Task> ExtendSessionAgnostic(string title, string assetType, string assetId, bool includeContainerSas) + public Task> ExtendSessionAgnosticAsync(string title, string assetType, string assetId, bool includeContainerSas, CancellationToken cancellationToken = default) { - return await this.PatchAsync( + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.PatchAsync( $"/{title}/{assetType}/{assetId}/sessions?include-container-sas={includeContainerSas}", - "{}"); + "{}", + cancellationToken: cancellationToken); } /// @@ -483,11 +643,17 @@ public async TaskTitle for the game for which the authoring session needs to be spawned. Example variant is "hi" for "Halo Infinite". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique asset ID for the asset type specified earlier. + /// Cancellation token for the operation. /// If the request to delete the session is successful, returns true. Otherwise, returns false. - public async Task> DeleteSessionAgnostic(string title, string assetType, string assetId) + public Task> DeleteSessionAgnosticAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.DeleteAsync( - $"/{title}/{assetType}/{assetId}/sessions"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.DeleteAsync( + $"/{title}/{assetType}/{assetId}/sessions", + cancellationToken: cancellationToken); } /// @@ -500,11 +666,17 @@ public async Task> DeleteSess /// Title for the game for which the authoring session needs to be spawned. Example variant is "hi" for "Halo Infinite". /// Type of asset to check. Example value is "UgcGameVariants". /// Unique asset ID for the asset type specified earlier. + /// Cancellation token for the operation. /// If the request to undelete an asset was successful, returns true. Otherwise, returns false. - public async Task> UndeleteAsset(string title, string assetType, string assetId) + public Task> UndeleteAssetAsync(string title, string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.PostAsync( - $"/{title}/{assetType}/{assetId}/recover"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.PostAsync( + $"/{title}/{assetType}/{assetId}/recover", + cancellationToken: cancellationToken); } /// @@ -514,11 +686,18 @@ public async Task> UndeleteAs /// Type of asset to unpublish. Example value is "UgcGameVariants". /// Unique asset ID for the asset type specified earlier. /// Unique ID for the asset version to be undeleted. + /// Cancellation token for the operation. /// If successful, returns true. Otherwise, returns false. - public async Task> UndeleteVersion(string title, string assetType, string assetId, string versionId) + public Task> UndeleteVersionAsync(string title, string assetType, string assetId, string versionId, CancellationToken cancellationToken = default) { - return await this.PostAsync( - $"/{title}/{assetType}/{assetId}/versions/{versionId}/recover"); + ArgumentException.ThrowIfNullOrEmpty(title); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + ArgumentException.ThrowIfNullOrEmpty(versionId); + + return this.PostAsync( + $"/{title}/{assetType}/{assetId}/versions/{versionId}/recover", + cancellationToken: cancellationToken); } /// @@ -526,11 +705,16 @@ public async Task> UndeleteVe /// /// Type of asset to unpublish. Example value is "UgcGameVariants". /// Unique asset ID for the asset type specified earlier. + /// Cancellation token for the operation. /// If successful, returns true. Otherwise, returns false. - public async Task> UnpublishAsset(string assetType, string assetId) + public Task> UnpublishAssetAsync(string assetType, string assetId, CancellationToken cancellationToken = default) { - return await this.PostAsync( - $"/hi/{assetType}/{assetId}/unpublish"); + ArgumentException.ThrowIfNullOrEmpty(assetType); + ArgumentException.ThrowIfNullOrEmpty(assetId); + + return this.PostAsync( + $"/hi/{assetType}/{assetId}/unpublish", + cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/ModuleBase.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/ModuleBase.cs index 7532661..c518c50 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/ModuleBase.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/ModuleBase.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -79,7 +80,7 @@ protected static void ValidateRange(int value, int min, int max, string paramNam /// /// The relative path (should start with /). /// The fully qualified URL. - protected string BuildUrl(string path) => + protected virtual string BuildUrl(string path) => $"https://{this.Origin}.{HaloCoreEndpoints.ServiceDomain}{path}"; /// @@ -89,17 +90,20 @@ protected string BuildUrl(string path) => /// The relative API path. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> GetAsync( string path, bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Get, useSpartanToken, useClearance, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a GET request against a fully specified URL. @@ -110,21 +114,24 @@ protected Task> GetAsync( /// Whether to include the Spartan token. Defaults to true. /// Optional custom headers to include. /// Whether to enforce success response codes. + /// Cancellation token for the operation. /// The API response container. protected Task> GetAsyncFullUrl( string fullUrl, bool useClearance = false, bool useSpartanToken = true, List>? customHeaders = null, - bool enforceSuccess = true) => - this.Client.ExecuteAPIRequest( + bool enforceSuccess = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( fullUrl, HttpMethod.Get, useSpartanToken, useClearance, includeRawResponse: this.Client.IncludeRawResponses, customHeaders: customHeaders, - enforceSuccess: enforceSuccess); + enforceSuccess: enforceSuccess, + cancellationToken: cancellationToken); /// /// Executes a POST request against the API. @@ -134,19 +141,22 @@ protected Task> GetAsyncFullUrl< /// The JSON content to send. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> PostAsync( string path, string content = "", bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Post, useSpartanToken, useClearance, content, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a POST request with JSON serialized body. @@ -158,21 +168,24 @@ protected Task> PostAsync( /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. /// The content type for the request. + /// Cancellation token for the operation. /// The API response container. protected Task> PostJsonAsync( string path, TBody body, bool useClearance = false, bool useSpartanToken = true, - APIContentType contentType = APIContentType.Json) => - this.Client.ExecuteAPIRequest( + APIContentType contentType = APIContentType.Json, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Post, useSpartanToken, useClearance, JsonSerializer.Serialize(body), contentType: contentType, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a PUT request against the API. @@ -182,19 +195,22 @@ protected Task> PostJsonAsyncThe JSON content to send. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> PutAsync( string path, string content = "", bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Put, useSpartanToken, useClearance, content, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a PUT request with JSON serialized body. @@ -205,19 +221,22 @@ protected Task> PutAsync( /// The object to serialize as JSON. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> PutJsonAsync( string path, TBody body, bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Put, useSpartanToken, useClearance, JsonSerializer.Serialize(body), - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a PATCH request against the API. @@ -227,19 +246,22 @@ protected Task> PutJsonAsyncThe JSON content to send. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> PatchAsync( string path, string content = "", bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Patch, useSpartanToken, useClearance, content, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a PATCH request with JSON serialized body. @@ -250,19 +272,22 @@ protected Task> PatchAsync( /// The object to serialize as JSON. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> PatchJsonAsync( string path, TBody body, bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Patch, useSpartanToken, useClearance, JsonSerializer.Serialize(body), - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); /// /// Executes a DELETE request against the API. @@ -271,16 +296,19 @@ protected Task> PatchJsonAsyncThe relative API path. /// Whether to include the clearance token. /// Whether to include the Spartan token. Defaults to true. + /// Cancellation token for the operation. /// The API response container. protected Task> DeleteAsync( string path, bool useClearance = false, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( + bool useSpartanToken = true, + CancellationToken cancellationToken = default) => + this.Client.ExecuteAPIRequestAsync( this.BuildUrl(path), HttpMethod.Delete, useSpartanToken, useClearance, - includeRawResponse: this.Client.IncludeRawResponses); + includeRawResponse: this.Client.IncludeRawResponses, + cancellationToken: cancellationToken); } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/CommsModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/CommsModule.cs index b66b4d8..da33a3e 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/CommsModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/CommsModule.cs @@ -6,6 +6,7 @@ // using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -17,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.Waypoint /// /// Module for Halo Waypoint communication and notification APIs. /// - public class CommsModule : WaypointModuleBase + public sealed class CommsModule : WaypointModuleBase { /// /// Initializes a new instance of the class. @@ -31,10 +32,11 @@ internal CommsModule(ClientBase client) /// /// Marks the user's notifications as read on Halo Waypoint. /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing the XUID and the date when notifications were marked as read. Otherwise, returns a null object and the error details. - public async Task> MarkNotificationsAsRead() + public Task> MarkNotificationsAsReadAsync(CancellationToken cancellationToken = default) { - return await this.PostAsync("/users/me/read-notifications", useSpartanToken: true); + return this.PostAsync("/users/me/read-notifications", useSpartanToken: true, cancellationToken: cancellationToken); } /// @@ -42,10 +44,11 @@ public async Task /// The number of notifications to skip. Defaults to 0. /// The maximum number of notifications to return. Defaults to 20. + /// Cancellation token for the operation. /// If successful, returns a list of objects. Otherwise, returns a null object and the error details. - public async Task, RawResponseContainer>> GetNotifications(int offset = 0, int limit = 20) + public Task, RawResponseContainer>> GetNotificationsAsync(int offset = 0, int limit = 20, CancellationToken cancellationToken = default) { - return await this.GetAsync>($"/users/me/notifications?offset={offset}&limit={limit}", useSpartanToken: true); + return this.GetAsync>($"/users/me/notifications?offset={offset}&limit={limit}", useSpartanToken: true, cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ContentModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ContentModule.cs index fc150c8..86aeee0 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ContentModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ContentModule.cs @@ -5,7 +5,9 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -17,7 +19,7 @@ namespace Den.Dev.Grunt.Core.Modules.Waypoint /// /// Module for Halo Waypoint article and content APIs. /// - public class ContentModule : WaypointModuleBase + public sealed class ContentModule : WaypointModuleBase { /// /// Initializes a new instance of the class. @@ -36,13 +38,15 @@ internal ContentModule(ClientBase client) /// Number of articles to retrieve. /// Order in which articles are returned. Example values are "asc" or "desc". /// List of categories for which to return the articles. + /// Cancellation token for the operation. /// If successful, returns the list of articles, each represented as . Otherwise, returns the details about the error. - public async Task, RawResponseContainer>> GetArticles( + public Task, RawResponseContainer>> GetArticlesAsync( string language = "", int offset = -1, int count = -1, string order = "", - List? categories = null) + List? categories = null, + CancellationToken cancellationToken = default) { string urlBase = "/articles?"; @@ -71,25 +75,29 @@ public async Task, RawResponseContainer>> G urlBase += $"categories={string.Join(",", categories)}&"; } - return await this.GetAsync>(urlBase, useSpartanToken: false); + return this.GetAsync>(urlBase.TrimEnd('?', '&'), useSpartanToken: false, cancellationToken: cancellationToken); } /// /// Gets a single article published on Halo Waypoint. /// /// Slug associated with the article. Example value is "halo-waypoint-content-browser". + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns a null object and error details. - public async Task> GetArticle(string slug) + public Task> GetArticleAsync(string slug, CancellationToken cancellationToken = default) { - return await this.GetAsync
($"/articles/{slug}", useSpartanToken: false); + ArgumentException.ThrowIfNullOrEmpty(slug); + + return this.GetAsync
($"/articles/{slug}", useSpartanToken: false, cancellationToken: cancellationToken); } /// /// Gets a list of article categories that are available on Halo Waypoint. /// /// Language in which the categories should be displayed. Example value is "en". + /// Cancellation token for the operation. /// If successful, returns a list of containing publishing categories. Otherwise, returns a null object and the error details. - public async Task, RawResponseContainer>> GetArticleCategories(string language = "") + public Task, RawResponseContainer>> GetArticleCategoriesAsync(string language = "", CancellationToken cancellationToken = default) { string path = "/taxonomy/article_category"; if (!string.IsNullOrEmpty(language)) @@ -97,7 +105,7 @@ public async Task, RawResponseConta path += $"?lang={language}"; } - return await this.GetAsync>(path, useSpartanToken: false); + return this.GetAsync>(path, useSpartanToken: false, cancellationToken: cancellationToken); } /// @@ -108,8 +116,9 @@ public async Task, RawResponseConta /// /// ID of the category. Must be an integer. /// Language in which the category should be displayed. Example value is "en". + /// Cancellation token for the operation. /// If successful, returns an instance of containing category information. Otherwise, returns a null object and the error details. - public async Task> GetArticleCategory(int id, string language = "") + public Task> GetArticleCategoryAsync(int id, string language = "", CancellationToken cancellationToken = default) { string path = $"/taxonomy/article_category/{id}"; if (!string.IsNullOrEmpty(language)) @@ -117,17 +126,20 @@ public async Task> path += $"?lang={language}"; } - return await this.GetAsync(path, useSpartanToken: false); + return this.GetAsync(path, useSpartanToken: false, cancellationToken: cancellationToken); } /// /// Gets Halo Waypoint service award details. /// /// Service award slug. + /// Cancellation token for the operation. /// If successful, returns an instance of . Otherwise, returns a null object and the error details. - public async Task> GetServiceAward(string slug) + public Task> GetServiceAwardAsync(string slug, CancellationToken cancellationToken = default) { - return await this.GetAsync($"/service-awards/{slug}", useSpartanToken: false); + ArgumentException.ThrowIfNullOrEmpty(slug); + + return this.GetAsync($"/service-awards/{slug}", useSpartanToken: false, cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ProfileModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ProfileModule.cs index 07d002c..7b0d053 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ProfileModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/ProfileModule.cs @@ -5,6 +5,8 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.Waypoint /// /// Module for Halo Waypoint profile and user settings APIs. /// - public class ProfileModule : WaypointModuleBase + public sealed class ProfileModule : WaypointModuleBase { /// /// Initializes a new instance of the class. @@ -33,10 +35,11 @@ internal ProfileModule(ClientBase client) /// /// Settings are obtained for the user associated with the Spartan token passed to the request. /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing user configuration information. Otherwise, returns a null object and error details. - public async Task> GetUserSettings() + public Task> GetUserSettingsAsync(CancellationToken cancellationToken = default) { - return await this.PostAsync("/users/me/settings", useSpartanToken: true); + return this.PostAsync("/users/me/settings", useSpartanToken: true, cancellationToken: cancellationToken); } /// @@ -45,10 +48,11 @@ public async Task> Ge /// /// Profile is obtained for the user associated with the Spartan token passed to the request. /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing profile information. Otherwise, returns a null object and error details. - public async Task> GetMyProfile() + public Task> GetMyProfileAsync(CancellationToken cancellationToken = default) { - return await this.PostAsync("/users/me", useSpartanToken: true); + return this.PostAsync("/users/me", useSpartanToken: true, cancellationToken: cancellationToken); } /// @@ -56,20 +60,24 @@ public async Task> Get /// /// User identifier. Can be a XUID or Gamertag. If XUID is used, then should be set to true. /// Determines whether the user ID specified in is a XUID or not. + /// Cancellation token for the operation. /// If successful, returns an instance of containing profile information. Otherwise, returns a null object and error details. - public async Task> GetUserProfile(string userId, bool isXuid) + public Task> GetUserProfileAsync(string userId, bool isXuid, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(userId); + string composedId = isXuid ? $"xuid({userId})" : $"gt({userId})"; - return await this.PostAsync($"/users/me/{composedId}", useSpartanToken: true); + return this.PostAsync($"/users/me/{composedId}", useSpartanToken: true, cancellationToken: cancellationToken); } /// /// Gets the list of a player's service awards associated with Halo Waypoint. /// + /// Cancellation token for the operation. /// If successful, returns an instance of containing service award information. Otherwise, returns a null object and the error details. - public async Task> GetServiceAwards() + public Task> GetServiceAwardsAsync(CancellationToken cancellationToken = default) { - return await this.GetAsync("/users/me/service-awards", useSpartanToken: true); + return this.GetAsync("/users/me/service-awards", useSpartanToken: true, cancellationToken: cancellationToken); } /// @@ -79,10 +87,13 @@ public async Task ensure that only the property is set. Setting other properties will result in a HTTP 400 Bad Request response. /// /// Instance of containing the list of service awards to feature. + /// Cancellation token for the operation. /// If successful, returns an instance of confirming the setting. Otherwise, returns a null object and the error details. - public async Task> PutFeaturedServiceAwards(ServiceAwardSnapshot awards) + public Task> PutFeaturedServiceAwardsAsync(ServiceAwardSnapshot awards, CancellationToken cancellationToken = default) { - return await this.PutJsonAsync("/users/me/service-awards/featured-awards", awards, useSpartanToken: true); + ArgumentNullException.ThrowIfNull(awards); + + return this.PutJsonAsync("/users/me/service-awards/featured-awards", awards, useSpartanToken: true, cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/RedemptionModule.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/RedemptionModule.cs index 4338188..489d3e4 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/RedemptionModule.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/RedemptionModule.cs @@ -5,6 +5,8 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System; +using System.Threading; using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; @@ -16,7 +18,7 @@ namespace Den.Dev.Grunt.Core.Modules.Waypoint /// /// Module for Halo Waypoint code redemption APIs. /// - public class RedemptionModule : WaypointModuleBase + public sealed class RedemptionModule : WaypointModuleBase { /// /// Initializes a new instance of the class. @@ -34,15 +36,18 @@ internal RedemptionModule(ClientBase client) /// The codes redeemable here can be those that are obtained through Xbox Game Pass perks, but can also be outside the scope of that particular program. /// /// Code to be redeemed. + /// Cancellation token for the operation. /// If call is successful, returns an instance of that contains information about the redeemed code. Otherwise, returns a null object and error details. - public async Task> RedeemCode(string code) + public Task> RedeemCodeAsync(string code, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(code); + RedeemableCode container = new() { Code = code, }; - return await this.PostJsonAsync("/users/me/codes", container, useSpartanToken: true); + return this.PostJsonAsync("/users/me/codes", container, useSpartanToken: true, cancellationToken: cancellationToken); } } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/WaypointModuleBase.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/WaypointModuleBase.cs index cff884c..e1c8fa0 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/WaypointModuleBase.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/Modules/Waypoint/WaypointModuleBase.cs @@ -5,21 +5,17 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // -using System; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Endpoints; -using Den.Dev.Grunt.Models; -using Den.Dev.Grunt.Util; namespace Den.Dev.Grunt.Core.Modules.Waypoint { /// - /// Base class for all Waypoint API modules providing shared functionality for making HTTP requests. + /// Base class for all Waypoint API modules. Inherits shared HTTP helper methods + /// from and overrides URL construction to use the + /// Waypoint service domain. /// - public abstract class WaypointModuleBase + public abstract class WaypointModuleBase : ModuleBase { /// /// Initializes a new instance of the class. @@ -27,124 +23,12 @@ public abstract class WaypointModuleBase /// The client instance to use for API requests. /// The origin/subdomain for this module's endpoints. protected WaypointModuleBase(ClientBase client, string origin) + : base(client, origin) { - this.Client = client ?? throw new ArgumentNullException(nameof(client)); - this.Origin = origin ?? throw new ArgumentNullException(nameof(origin)); } - /// - /// Gets the client instance used for executing API requests. - /// - protected ClientBase Client { get; } - - /// - /// Gets the origin/subdomain for this module's API endpoints. - /// - protected string Origin { get; } - - /// - /// Builds a full URL from a relative path using this module's origin. - /// - /// The relative path (should start with /). - /// The fully qualified URL. - protected string BuildUrl(string path) => + /// + protected override string BuildUrl(string path) => $"https://{this.Origin}.{WaypointEndpoints.ServiceDomain}{path}"; - - /// - /// Executes a GET request against the API. - /// - /// The expected response type. - /// The relative API path. - /// Whether to include the Spartan token. Defaults to false. - /// The API response container. - protected Task> GetAsync( - string path, - bool useSpartanToken = false) => - this.Client.ExecuteAPIRequest( - this.BuildUrl(path), - HttpMethod.Get, - useSpartanToken, - useClearance: false, - textContent: GlobalConstants.WEB_USER_AGENT, - includeRawResponse: this.Client.IncludeRawResponses); - - /// - /// Executes a GET request against a fully specified URL. - /// - /// The expected response type. - /// The full URL to request. - /// Whether to include the Spartan token. Defaults to false. - /// The API response container. - protected Task> GetAsyncFullUrl( - string fullUrl, - bool useSpartanToken = false) => - this.Client.ExecuteAPIRequest( - fullUrl, - HttpMethod.Get, - useSpartanToken, - useClearance: false, - textContent: GlobalConstants.WEB_USER_AGENT, - includeRawResponse: this.Client.IncludeRawResponses); - - /// - /// Executes a POST request against the API with WEB_USER_AGENT content. - /// - /// The expected response type. - /// The relative API path. - /// Whether to include the Spartan token. Defaults to true. - /// The API response container. - protected Task> PostAsync( - string path, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( - this.BuildUrl(path), - HttpMethod.Post, - useSpartanToken, - useClearance: false, - textContent: GlobalConstants.WEB_USER_AGENT, - includeRawResponse: this.Client.IncludeRawResponses); - - /// - /// Executes a POST request with JSON serialized body. - /// - /// The expected response type. - /// The type of the request body. - /// The relative API path. - /// The object to serialize as JSON. - /// Whether to include the Spartan token. Defaults to true. - /// The API response container. - protected Task> PostJsonAsync( - string path, - TBody body, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( - this.BuildUrl(path), - HttpMethod.Post, - useSpartanToken, - useClearance: false, - textContent: JsonSerializer.Serialize(body), - includeRawResponse: this.Client.IncludeRawResponses); - - /// - /// Executes a PUT request with JSON serialized body. - /// - /// The expected response type. - /// The type of the request body. - /// The relative API path. - /// The object to serialize as JSON. - /// Whether to include the Spartan token. Defaults to true. - /// The API response container. - protected Task> PutJsonAsync( - string path, - TBody body, - bool useSpartanToken = true) => - this.Client.ExecuteAPIRequest( - this.BuildUrl(path), - HttpMethod.Put, - useSpartanToken, - useClearance: false, - textContent: JsonSerializer.Serialize(body), - contentType: APIContentType.Json, - includeRawResponse: this.Client.IncludeRawResponses); } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/WaypointClient.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/WaypointClient.cs index 3fc57cd..373d775 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/WaypointClient.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Core/WaypointClient.cs @@ -5,6 +5,7 @@ // The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. // +using System.Net.Http; using Den.Dev.Grunt.Core.Foundation; using Den.Dev.Grunt.Core.Modules.Waypoint; @@ -13,7 +14,7 @@ namespace Den.Dev.Grunt.Core /// /// Client for interacting with the Halo Waypoint APIs. /// - public class WaypointClient : ClientBase + public sealed class WaypointClient : ClientBase, IWaypointClient { /// /// Initializes a new instance of the class, used to access the Halo Waypoint API. @@ -32,6 +33,25 @@ public WaypointClient(string spartanToken, string xuid = "", string clearanceTok this.InitializeModules(); } + /// + /// Initializes a new instance of the class with a custom HttpClient. + /// + /// The HttpClient instance to use for API requests. + /// The Spartan token used to authenticate against the Halo Infinite API. + /// The player identifier in the format "xuid(XUID_VALUE)". + /// ID of the flight/clearance currently active for the player. + /// Optional User-Agent header value for outbound requests. + public WaypointClient(HttpClient httpClient, string spartanToken, string xuid = "", string clearanceToken = "", string userAgent = "") + : base(httpClient) + { + this.SpartanToken = spartanToken; + this.Xuid = xuid; + this.ClearanceToken = clearanceToken; + this.UserAgent = userAgent; + + this.InitializeModules(); + } + /// /// Initializes a new instance of the class, used to access the Halo Waypoint API. /// diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/EmblemLocation.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/EmblemLocation.cs index 9a7c313..ceecb5d 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/EmblemLocation.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/EmblemLocation.cs @@ -21,7 +21,7 @@ public class EmblemLocation public int? LocationId { get; set; } /// - /// gets or sets the default emblem option. + /// Gets or sets the default emblem option. /// public Emblem? DefaultOption { get; set; } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResult.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResult.cs new file mode 100644 index 0000000..7db8fc6 --- /dev/null +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResult.cs @@ -0,0 +1,27 @@ +// +// Developed by Den Delimarsky. +// Den Delimarsky licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// The underlying API powering Den.Dev.Grunt is managed by Halo Studios and Microsoft. This wrapper is not endorsed by Halo Studios or Microsoft. +// + +namespace Den.Dev.Grunt.Models +{ + /// + /// Simplified result container for Halo API responses. + /// This is a convenience alias for . + /// + /// The type of result to fetch. + public class HaloApiResult : HaloApiResultContainer + { + /// + /// Initializes a new instance of the class. + /// + /// Result from the Halo API request. + /// Raw response information for the Halo API request. + public HaloApiResult(T result, RawResponseContainer container) + : base(result, container) + { + } + } +} diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResultContainer.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResultContainer.cs index 89fc21c..ba35119 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResultContainer.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloApiResultContainer.cs @@ -15,7 +15,7 @@ namespace Den.Dev.Grunt.Models public class HaloApiResultContainer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Result from the Halo API request. /// Error information for the Halo API request. @@ -34,5 +34,10 @@ public HaloApiResultContainer(T result, TRawResponseContainer container) /// Gets or sets the Halo API request error information. /// public TRawResponseContainer? Response { get; set; } + + /// + /// Gets a value indicating whether the API request was successful (HTTP 2xx status code). + /// + public bool IsSuccess => this.Response is RawResponseContainer raw && raw.Code >= 200 && raw.Code < 300; } } diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/Articleimage.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/ArticleImage.cs similarity index 93% rename from src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/Articleimage.cs rename to src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/ArticleImage.cs index 3493274..d19e9a1 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/Articleimage.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/ArticleImage.cs @@ -1,4 +1,4 @@ -// +// // Developed by Den Delimarsky. // Den Delimarsky licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/GameVariantCategory.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/GameVariantCategory.cs index 5a038ac..15d34f6 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/GameVariantCategory.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/GameVariantCategory.cs @@ -78,7 +78,7 @@ public enum GameVariantCategory MultiplayerStrongholds = 11, /// - /// King of the Hill + /// Bastion mode. /// MultiplayerBastion = 12, @@ -218,7 +218,7 @@ public enum GameVariantCategory MultiplayerLandGrab = 39, /// - /// Minigames,such as Survive The Undead + /// Minigames, such as Survive The Undead /// MultiplayerMinigame = 41, diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/MatchType.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/MatchType.cs index 0d7afcd..47d2117 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/MatchType.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/HaloInfinite/MatchType.cs @@ -8,7 +8,7 @@ namespace Den.Dev.Grunt.Models.HaloInfinite { /// - /// Types of matches that a user can query with . + /// Types of matches that a user can query with . /// public enum MatchType { diff --git a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/RawResponseContainer.cs b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/RawResponseContainer.cs index 75dd5fc..2600db5 100644 --- a/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/RawResponseContainer.cs +++ b/src/dotnet/Den.Dev.Grunt/Den.Dev.Grunt/Models/RawResponseContainer.cs @@ -12,7 +12,7 @@ namespace Den.Dev.Grunt.Models /// /// Container class used to encapsulate any API errors. /// - public class RawResponseContainer + public sealed class RawResponseContainer { /// /// Gets or sets the HTTP error code produced by the API. @@ -48,5 +48,10 @@ public class RawResponseContainer /// Gets or sets the HTTP headers received in the response. /// public Dictionary? ResponseHeaders { get; set; } + + /// + /// Gets a value indicating whether the HTTP response indicates success (2xx status code). + /// + public bool IsSuccess => this.Code >= 200 && this.Code < 300; } } diff --git a/src/dotnet/Den.Dev.Grunt/README.md b/src/dotnet/Den.Dev.Grunt/README.md index f0153b7..4612a0c 100644 --- a/src/dotnet/Den.Dev.Grunt/README.md +++ b/src/dotnet/Den.Dev.Grunt/README.md @@ -1,277 +1,215 @@ -# Den.Dev.Grunt - .NET Library +# Den.Dev.Grunt - Halo Infinite API for .NET -The core .NET library for interacting with Halo Infinite APIs. +[![NuGet](https://img.shields.io/nuget/v/Den.Dev.Grunt)](https://www.nuget.org/packages/Den.Dev.Grunt) +[![NuGet Downloads](https://img.shields.io/nuget/dt/Den.Dev.Grunt)](https://www.nuget.org/packages/Den.Dev.Grunt) -## Installation +A .NET library for the Halo Infinite API. Get player stats, match history, inventory, ranks, and more with strongly-typed responses. + +## Install ```bash dotnet add package Den.Dev.Grunt ``` -Or via the NuGet Package Manager: - -``` -Install-Package Den.Dev.Grunt -``` - -## Components - -| Component | Description | -|:--------------------------|:------------| -| `Den.Dev.Grunt` | The core library that wraps the Halo Infinite web APIs. | -| `Den.Dev.Grunt.Zeta` | Experimental ground for testing wrapped APIs in real scenarios. | -| `Den.Dev.Grunt.Librarian` | Code generator that produces production-quality API client modules from endpoint definitions. | -| `Den.Dev.Grunt.Composer` | Data composition and transformation utilities. | -| `Den.Dev.Grunt.Auditor` | Validates models against live API responses to detect discrepancies. | +## Quick Start (2 minutes) -## Quick Start +All you need is a **Spartan token**. Get one by inspecting network traffic on [halowaypoint.com](https://halowaypoint.com): -### Bring Your Own Token +1. Sign in to Halo Waypoint +2. Open browser DevTools (F12) → Network tab +3. Find any API call returning JSON +4. Copy the `x-343-authorization-spartan` header value (that's your Spartan token) +5. Optionally copy the `343-clearance` header value (needed for some endpoints) -If you have a Spartan token (obtained from Halo Waypoint), you can use it directly: +Then use it: ```csharp -HaloInfiniteClient client = new("", clearanceToken: ""); +using Den.Dev.Grunt.Core; -// Get match stats -var example = await client.Stats.GetMatchStats("21416434-4717-4966-9902-af7097469f74"); -Console.WriteLine("You have data."); -``` +var client = new HaloInfiniteClient("", clearanceToken: ""); -### Full Authentication Flow - -For automatic token generation, first [register an Azure Active Directory application](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app), then create a `client.json` file in your project: - -```json +// Get match stats +var result = await client.Stats.GetMatchStatsAsync("match-guid-here"); +if (result.IsSuccess) { - "client_id": "", - "client_secret": "", - "redirect_url": "" + Console.WriteLine($"Map: {result.Result.MatchInfo.MapVariant.AssetId}"); } -``` -Set the file's `Build Action` to `None` and `Copy to Output Directory` to `Copy if newer`. +// Get player service record +var record = await client.Stats.GetPlayerServiceRecordByGamertagAsync("BreadKrtek", LifecycleMode.Matchmade); -Then use the authentication flow: +// Get player inventory +var inventory = await client.Economy.GetInventoryItemsAsync("player-xuid"); -```csharp -ConfigurationReader clientConfigReader = new(); -var clientConfig = clientConfigReader.ReadConfiguration("client.json"); +// Get medal metadata +var medals = await client.GameCms.GetMedalMetadataAsync(); +``` -XboxAuthenticationClient manager = new(); -var url = manager.GenerateAuthUrl(clientConfig.ClientId, clientConfig.RedirectUrl); +## Available Modules -HaloAuthenticationClient haloAuthClient = new(); +Access API domains through the client's module properties: -OAuthToken currentOAuthToken = null; +```csharp +var client = new HaloInfiniteClient(spartanToken, clearanceToken: clearanceToken); +``` -var ticket = new XboxTicket(); -var haloTicket = new XboxTicket(); -var extendedTicket = new XboxTicket(); +| Module | What You Can Do | Example | +|:-------|:----------------|:--------| +| `client.Stats` | Match history, service records, stats | `GetMatchHistoryAsync(xuid, 0, 25, MatchType.All)` | +| `client.Economy` | Inventory, stores, customization, currency | `GetInventoryItemsAsync(xuid)` | +| `client.GameCms` | Medals, challenges, seasons, items | `GetMedalMetadataAsync()` | +| `client.Skill` | Competitive Skill Rank (CSR) | `GetPlaylistCsrAsync(playlistId, playerIds)` | +| `client.Ugc` | Create/edit maps, modes, prefabs | `SpawnAssetAsync(title, assetType, asset)` | +| `client.UgcDiscovery` | Search/browse community content | `SearchAsync(start: 0, count: 25)` | +| `client.Academy` | Bot customization, drills | `GetBotCustomizationAsync(flightId)` | +| `client.Lobby` | QoS servers, presence | `GetQosServersAsync()` | +| `client.Settings` | Clearance, feature flags | `GetActiveClearanceAsync(flightId)` | +| `client.Configuration` | API endpoint discovery | `GetApiSettingsContainerAsync()` | +| `client.BanProcessor` | Ban status checks | `GetBanSummaryAsync(targetList)` | +| `client.TextModeration` | Text moderation keys | `GetSigningKeysAsync()` | + +## Handling Responses + +Every method returns `HaloApiResultContainer`: -var xblToken = string.Empty; -var haloToken = new SpartanToken(); +```csharp +var result = await client.Stats.GetMatchStatsAsync("match-guid"); -if (System.IO.File.Exists("tokens.json")) +// Check if the request succeeded +if (result.IsSuccess) { - Console.WriteLine("Trying to use local tokens..."); - currentOAuthToken = clientConfigReader.ReadConfiguration("tokens.json"); + var matchData = result.Result; // Strongly-typed response } else { - currentOAuthToken = RequestNewToken(url, manager, clientConfig); + Console.WriteLine($"Error {result.Response.Code}: {result.Response.Message}"); } +``` -Task.Run(async () => -{ - ticket = await manager.RequestUserToken(currentOAuthToken.AccessToken); - if (ticket == null) - { - currentOAuthToken = await manager.RefreshOAuthToken( - clientConfig.ClientId, - currentOAuthToken.RefreshToken, - clientConfig.RedirectUrl, - clientConfig.ClientSecret); - if (currentOAuthToken == null) - { - Console.WriteLine("Could not get the token even with the refresh token."); - currentOAuthToken = RequestNewToken(url, manager, clientConfig); - } - ticket = await manager.RequestUserToken(currentOAuthToken.AccessToken); - } -}).GetAwaiter().GetResult(); - -Task.Run(async () => -{ - haloTicket = await manager.RequestXstsToken(ticket.Token); -}).GetAwaiter().GetResult(); - -Task.Run(async () => -{ - extendedTicket = await manager.RequestXstsToken(ticket.Token, false); -}).GetAwaiter().GetResult(); - -if (ticket != null) -{ - xblToken = manager.GetXboxLiveV3Token(haloTicket.DisplayClaims.Xui[0].Uhs, haloTicket.Token); -} +### Raw Response Inspection -Task.Run(async () => -{ - haloToken = await haloAuthClient.GetSpartanToken(haloTicket.Token); - Console.WriteLine("Your Halo token:"); - Console.WriteLine(haloToken.Token); -}).GetAwaiter().GetResult(); +Enable raw responses to see full HTTP details (useful for debugging): -HaloInfiniteClient client = new(haloToken.Token, extendedTicket.DisplayClaims.Xui[0].Xid); +```csharp +var client = new HaloInfiniteClient(spartanToken, includeRawResponses: true); -// Get clearance for API access -string localClearance = string.Empty; -Task.Run(async () => -{ - var clearance = (await client.Settings.ActiveClearance("1.6")).Result; - if (clearance != null) - { - localClearance = clearance.FlightConfigurationId; - client.ClearanceToken = localClearance; - Console.WriteLine($"Your clearance is {localClearance} and it's set in the client."); - } -}).GetAwaiter().GetResult(); - -// Now you can make API calls -var stats = await client.Stats.GetMatchStats("21416434-4717-4966-9902-af7097469f74"); +var result = await client.Stats.GetMatchStatsAsync("match-guid"); +Console.WriteLine(result.Response.RequestUrl); +Console.WriteLine(result.Response.RequestMethod); +Console.WriteLine(result.Response.Message); // Raw JSON response body ``` -> **Note:** The clearance (`343-clearance` header) needs to be activated at least once with the game before API access is granted. Launch Halo Infinite at least once on your account before querying the API. If you get `403 Forbidden` errors, this is likely the cause. +## Cancellation Support -## Librarian - API Code Generator +All async methods accept a `CancellationToken`: -The Librarian automatically generates production-quality API client code from Halo Infinite endpoint definitions. +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); -### Features +try +{ + var result = await client.Stats.GetMatchStatsAsync("match-guid", cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Request timed out."); +} +``` -- **Automatic endpoint discovery** - Fetches all 177+ endpoints from the live Halo API -- **Strongly-typed responses** - Maps endpoints to specific response types via `response-types.json` -- **HTTP method inference** - Intelligently detects GET, POST, PUT, DELETE based on method names -- **XML documentation** - Generates proper ``, ``, and `` tags -- **Module grouping** - Organizes endpoints into logical modules (Economy, Stats, GameCms, etc.) -- **Scriban templates** - Clean, maintainable template syntax for code generation +## Authentication -### Usage +### Option 1: Manual Token (Quickest) -```bash -# Navigate to the Librarian project -cd Den.Dev.Grunt.Librarian +Grab the Spartan token from Halo Waypoint (see Quick Start above). Tokens expire frequently — get a new one if you see `401 Unauthorized`. + +### Option 2: Programmatic Token -# Generate to default output directory (./Output/Generated) -dotnet run +Use `HaloAuthenticationClient` to exchange an Xbox Live XSTS token for a Spartan token: -# Generate with response type mappings -dotnet run -- --response-types response-types.json +```csharp +using Den.Dev.Grunt.Authentication; -# Preview without writing files -dotnet run -- --dry-run +var authClient = new HaloAuthenticationClient(); +var spartanToken = await authClient.GetSpartanTokenAsync(xstsToken); -# Generate to a custom directory -dotnet run -- --output C:\MyGeneratedCode +var client = new HaloInfiniteClient(spartanToken.Token, xuid: playerXuid); ``` -### Command Line Options - -| Option | Short | Description | -|:-------|:------|:------------| -| `--output` | `-o` | Output directory for generated files (default: `./Output/Generated`) | -| `--response-types` | `-r` | Path to `response-types.json` mapping file | -| `--dry-run` | `-d` | Preview output without writing files | -| `--help` | `-h` | Show help message | +To get an XSTS token, you need an [Azure AD app registration](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) and the Xbox Live authentication flow via the [Den.Dev.Conch](https://www.nuget.org/packages/Den.Dev.Conch) package. -### Response Type Mappings +### Getting Clearance -The `response-types.json` file maps endpoint IDs to their response types: +Some endpoints require a clearance token. Get one after authenticating: -```json +```csharp +var clearance = await client.Settings.GetActiveClearanceAsync("1.6"); +if (clearance.IsSuccess) { - "Economy_GetActiveBoosts": "ActiveBoostsContainer", - "Economy_AiCoreCustomization": "AiCore", - "Stats_GetMatchHistory": "MatchHistoryResponse" + client.ClearanceToken = clearance.Result.FlightConfigurationId; } ``` -Endpoints without explicit mappings default to `object` with a TODO comment for manual review. +> **Note:** You must launch Halo Infinite at least once on your account before the clearance API will work. If you get `403 Forbidden`, this is the likely cause. -### Generated Output +## Testability -The Librarian generates partial class files that can be integrated into the main library: +The library provides interfaces for dependency injection and mocking: -``` -Output/Generated/ -├── EconomyModule.Generated.cs -├── GameCmsModule.Generated.cs -├── StatsModule.Generated.cs -├── UgcModule.Generated.cs -├── UgcDiscoveryModule.Generated.cs -└── ... +```csharp +// Register in DI container +services.AddSingleton( + new HaloInfiniteClient(spartanToken, clearanceToken: clearanceToken)); + +// Inject in your services +public class MyService +{ + private readonly IHaloInfiniteClient _client; + + public MyService(IHaloInfiniteClient client) => _client = client; +} + +// Mock in tests +var mock = new Mock(); +mock.Setup(c => c.Stats).Returns(mockStatsModule); ``` -### Example Generated Code +You can also inject a custom `HttpClient` for full control over the HTTP pipeline: ```csharp -/// -/// Calls the Economy_GetActiveBoosts endpoint. -/// -/// The player's numeric XUID. -/// An instance of HaloApiResultContainer containing the response. -public async Task> GetActiveBoosts(string player) -{ - return await this.GetAsync( - $"/hi/players/xuid({player})/boosts", - useClearance: true); -} +var httpClient = new HttpClient(new MyLoggingHandler(new HttpClientHandler())); +var client = new HaloInfiniteClient(httpClient, spartanToken); ``` -## API Modules - -The `HaloInfiniteClient` provides access to various API modules: - -| Module | Description | -|:-------|:------------| -| `Stats` | Match history, service records, match statistics | -| `Skill` | CSR (Competitive Skill Rank) queries | -| `Economy` | Inventory, stores, customization, currency | -| `GameCms` | Item definitions, challenges, medals, career ranks | -| `Ugc` | User-generated content authoring | -| `UgcDiscovery` | Search and browse user content | -| `Academy` | Bot customization, training drills | -| `Lobby` | QoS servers, lobby presence | -| `Settings` | Clearance levels, feature flags | -| `Configuration` | API endpoint discovery | -| `BanProcessor` | Ban status queries | -| `TextModeration` | Text moderation keys | +## Halo Waypoint APIs -## Building from Source +For Halo Waypoint content (articles, profiles, service awards): + +```csharp +using Den.Dev.Grunt.Core; + +var waypointClient = new WaypointClient(); -### Prerequisites +// Get articles (no authentication required) +var articles = await waypointClient.Content.GetArticlesAsync(language: "en", count: 10); -- .NET 10.0 SDK or later -- Visual Studio 2022 (optional) +// Get player profile (requires Spartan token) +var wpClient = new WaypointClient(spartanToken, xuid: playerXuid); +var profile = await wpClient.Profile.GetMyProfileAsync(); +``` -### Build +## Building from Source ```bash cd src/dotnet/Den.Dev.Grunt dotnet build ``` -### Run Tests - -```bash -dotnet test -``` +Requires .NET 10.0 SDK or later. -## Documentation +## API Reference -Full API documentation is available at [docs.gruntapi.com](https://docs.gruntapi.com). +Full documentation: [docs.gruntapi.com](https://docs.gruntapi.com) ## License -MIT License - see [LICENSE](../../../LICENSE) for details. +MIT - see [LICENSE](../../../LICENSE).