diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7296baad..0b3044a4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,6 +96,10 @@ jobs: with: repository: vrchatapi/vrchatapi-csharp + # The artifact is overlaid onto the checkout rather than replacing it, so anything the + # generator stops emitting would otherwise survive in src/ forever. + - run: rm -rf src + - uses: actions/download-artifact@v8 with: name: csharp-generated diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index 593af131..a97986c4 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -22,6 +22,16 @@ # Then explicitly reverse the ignore rule for a single file: #!docs/README.md +# Files the generator emits that this repository does not ship. Listing them here stops +# them being written at all, rather than deleting them afterwards in generate.sh. README.md VRChat.API.sln -src/VRChat.API.Test/ \ No newline at end of file +src/VRChat.API.Test/** +docs/** +api/** +appveyor.yml +git_push.sh +build.sh +build.bat +mono_nunit_test.sh +nuget.exe \ No newline at end of file diff --git a/VRChat.API.sln b/VRChat.API.sln index 8dabd6cf..ad51a884 100644 --- a/VRChat.API.sln +++ b/VRChat.API.sln @@ -4,8 +4,6 @@ VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VRChat.API", "src\VRChat.API\VRChat.API.csproj", "{CFFD58F6-C881-46CB-BD52-445A8A8A8710}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VRChat.API.Wrapper", "wrapper\VRChat.API\VRChat.API.Wrapper.csproj", "{A38F1B7D-A1FF-B23E-08C3-81561FBF1C30}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VRChat.API.Extensions.Hosting", "wrapper\VRChat.API.Extensions.Hosting\VRChat.API.Extensions.Hosting.csproj", "{D9043377-EF21-E16A-7F4A-58D093467DC1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VRChat.API.Examples.AspNetCore", "examples\VRChat.API.Examples.AspNetCore\VRChat.API.Examples.AspNetCore.csproj", "{9F172233-1AF6-700F-5F54-423D12F46099}" @@ -26,10 +24,6 @@ Global {CFFD58F6-C881-46CB-BD52-445A8A8A8710}.Debug|Any CPU.Build.0 = Debug|Any CPU {CFFD58F6-C881-46CB-BD52-445A8A8A8710}.Release|Any CPU.ActiveCfg = Release|Any CPU {CFFD58F6-C881-46CB-BD52-445A8A8A8710}.Release|Any CPU.Build.0 = Release|Any CPU - {A38F1B7D-A1FF-B23E-08C3-81561FBF1C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A38F1B7D-A1FF-B23E-08C3-81561FBF1C30}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A38F1B7D-A1FF-B23E-08C3-81561FBF1C30}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A38F1B7D-A1FF-B23E-08C3-81561FBF1C30}.Release|Any CPU.Build.0 = Release|Any CPU {D9043377-EF21-E16A-7F4A-58D093467DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9043377-EF21-E16A-7F4A-58D093467DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9043377-EF21-E16A-7F4A-58D093467DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/generate.sh b/generate.sh index a6f4ae72..b9044a72 100755 --- a/generate.sh +++ b/generate.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail if [ ${#} -le 1 ] then @@ -6,87 +7,53 @@ then exit 1 fi -rm src docs *.nupkg *.snupkg -rf +spec="${1}" +version="${2}" +# Applies a sed expression to a file, but only after confirming the text it targets is still +# there. sed exits 0 when it matches nothing, so without this guard a patch that stopped +# applying after a generator upgrade would fail silently. +patch_generated() { + local file="${1}" probe="${2}" expression="${3}" + + if ! grep -qF "${probe}" "${file}" + then + echo "generate.sh: '${probe}' is no longer present in ${file}; the patch below is stale" >&2 + exit 1 + fi + + sed -i "${expression}" "${file}" +} + +rm -rf src docs ./*.nupkg ./*.snupkg + +# Output this repository does not ship is listed in .openapi-generator-ignore, so it is never +# written. Everything VRChat-specific about the generated code lives in ./templates, and the +# csproj customisation in ./templates/netcore_project.additions.mustache. openapi-generator generate \ -g csharp \ -t ./templates \ --library httpclient \ ---additional-properties=packageGuid=1c420561-97f1-4810-ad2d-cd344d27170a,packageName=VRChat.API,packageTags=vrchat,packageVersion="${2}",targetFramework=net8.0,licenseId=MIT,equatable=true \ +--additional-properties=packageGuid=1c420561-97f1-4810-ad2d-cd344d27170a,packageName=VRChat.API,packageVersion="${version}",targetFramework=net8.0,licenseId=MIT,equatable=true,packageAuthors="VRChat API Docs Community",packageCompany="VRChat API Docs Community",packageTitle="VRChat API Library for .NET",packageDescription="VRChat API Library for .NET",packageCopyright="Copyright © 2021 Owners of GitHub organisation \"vrchatapi\" and individual contributors." \ --git-user-id=vrchatapi \ --git-repo-id=vrchatapi-csharp \ -o . \ --i "${1}" \ +-i "${spec}" \ --http-user-agent="vrchatapi-csharp" -rm build.sh -rm build.bat -rm git_push.sh -rm mono_nunit_test.sh -rm nuget.exe -rm appveyor.yml - -rm -rf docs/ -rm -rf api/ -rm -rf src/VRChat.API.Test/ - -# Move wrapper code to src/VRChat.API/Client/ -cp -r wrapper/VRChat.API/Client/* src/VRChat.API/Client/ - cp wrapper/VRChat.API.Extensions.Hosting/vrc_cat.ico src/VRChat.API/vrc_cat.ico cp wrapper/VRChat.API.Extensions.Hosting/vrc_cat.png src/VRChat.API/vrc_cat.png -# Fix cookie handling -for file in $(find ./src/VRChat.API -name '*.cs'); do - sed -i 's/new Cookie("auth", this.Configuration.GetApiKeyWithPrefix("auth"))/new Cookie("auth", this.Configuration.GetApiKeyWithPrefix("auth"), "\/", "api.vrchat.cloud")/g' $file - sed -i 's/new Cookie("twoFactorAuth", this.Configuration.GetApiKeyWithPrefix("twoFactorAuth"))/new Cookie("twoFactorAuth", this.Configuration.GetApiKeyWithPrefix("twoFactorAuth"), "\/", "api.vrchat.cloud")/g' $file - sed -i 's/new Cookie(cookie.Name, cookie.Value)/new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain)/g' $file -done - -# Add ITwoFactorCode to two-factor authentication types -sed -i 's/\(class.*IValidatableObject\)/\1, ITwoFactorCode/' src/VRChat.API/Model/TwoFactorAuthCode.cs -sed -i 's/\(class.*IValidatableObject\)/\1, ITwoFactorCode/' src/VRChat.API/Model/TwoFactorEmailCode.cs - -# Add icons and readme to package -sed -i ':a;N;$!ba;s|\(.*\)|\1\t \n\t \n\t True\n\t \\\n\t \n\t \n\t True\n\t \\\n\t \n |' src/VRChat.API/VRChat.API.csproj -sed -i '/vrchat<\/PackageTags>/a\ vrc_cat.ico' src/VRChat.API/VRChat.API.csproj - -# Adjust package tags -sed -i 's/vrchat<\/PackageTags>/vrchat,vrcapi,vrc-api,vrc<\/PackageTags>/' src/VRChat.API/VRChat.API.csproj - -# Fix username and password encoding -sed -i 's/VRChat.API.Client.ClientUtils.Base64Encode(this.Configuration.Username + \":\" + this.Configuration.Password)/VRChat.API.Client.ClientUtils.Base64Encode(System.Web.HttpUtility.UrlEncode(this.Configuration.Username) + ":" + System.Web.HttpUtility.UrlEncode(this.Configuration.Password))/g' src/VRChat.API/Api/AuthenticationApi.cs - -# Fix fields in csproj -sed -i 's/OpenAPI Library/VRChat API Library for .NET/' src/VRChat.API/VRChat.API.csproj -sed -i 's/A library generated from a OpenAPI doc/VRChat API Library for .NET/' src/VRChat.API/VRChat.API.csproj -sed -i 's/No Copyright/Copyright © 2021 Owners of GitHub organisation "vrchatapi" and individual contributors./' src/VRChat.API/VRChat.API.csproj -sed -i 's/OpenAPI/VRChat API Docs Community/' src/VRChat.API/VRChat.API.csproj -sed -i 's/Minor update/Automated deployment/' src/VRChat.API/VRChat.API.csproj - -# Fix failure to compile -sed -i 's/false<\/GenerateAssemblyInfo>/true<\/GenerateAssemblyInfo>/' src/VRChat.API/VRChat.API.csproj - -# Update VRChat.API.Extensions.Hosting version -sed -i "s|[^<]*|${2}|g" wrapper/VRChat.API.Extensions.Hosting/VRChat.API.Extensions.Hosting.csproj - -# Add README.md to fields -sed -i '/PackageTags/a \ README.md<\/PackageReadmeFile>' src/VRChat.API/VRChat.API.csproj -sed -i '/System.ComponentModel.Annotations/a \ ' src/VRChat.API/VRChat.API.csproj - -# Add Otp.NET package to project -sed -i '/JsonSubTypes/a \ ' src/VRChat.API/VRChat.API.csproj - -# Make CurrentUser fields optional for 2FA response compatibility -sed -i 's/IsRequired = true/IsRequired = false/g' src/VRChat.API/Model/CurrentUser.cs - -# Add RequiresTwoFactorAuth property to CurrentUser -sed -i '/public string UserIcon { get; set; }/a\\n /// \n /// An array of two-factor authentication methods available to use to with two factor authentication.\n /// \n [DataMember(Name = "requiresTwoFactorAuth", IsRequired = false, EmitDefaultValue = true)]\n public List RequiresTwoFactorAuth { get; set; }' src/VRChat.API/Model/CurrentUser.cs - -# Remove messily pasted markdown at top of every file -for i in src/VRChat.API/*/*.cs; do - sed -i '/VRChat API Banner/d' $i -done - cp README.md src/VRChat.API/ cp README.md src/ + +# The only edit to generated code that no template or partial class can express: CurrentUser is +# also returned mid-login, before two-factor verification, with most of its fields absent. The +# specification marks them required, which makes deserialising that response throw, so the +# generated attributes are relaxed for this one model. +patch_generated src/VRChat.API/Model/CurrentUser.cs \ + 'IsRequired = true' 's/IsRequired = true/IsRequired = false/g' + +# VRChat.API.Extensions.Hosting is hand-maintained, so its version is stamped rather than generated. +patch_generated wrapper/VRChat.API.Extensions.Hosting/VRChat.API.Extensions.Hosting.csproj \ + '' "s|[^<]*|${version}|g" diff --git a/templates/WebRequestPathBuilder.mustache b/templates/WebRequestPathBuilder.mustache index cc811ae4..a63cf080 100644 --- a/templates/WebRequestPathBuilder.mustache +++ b/templates/WebRequestPathBuilder.mustache @@ -18,11 +18,22 @@ namespace {{packageName}}.Client _path = path; } + /// + /// Percent-encodes a value, then restores "(" and ")". + /// + /// VRChat's API answers 400 "malformed url" when parentheses arrive + /// percent-encoded, and instance IDs contain them, for example + /// wrld_0000:12345~group(grp_0000)~groupAccessType(plus)~region(use). + private static string Escape(string value) + { + return Uri.EscapeDataString(value).Replace("%28", "(").Replace("%29", ")"); + } + public void AddPathParameters(Dictionary parameters) { foreach (var parameter in parameters) { - _path = _path.Replace("{" + parameter.Key + "}", Uri.EscapeDataString(parameter.Value)); + _path = _path.Replace("{" + parameter.Key + "}", Escape(parameter.Value)); } } @@ -32,7 +43,7 @@ namespace {{packageName}}.Client { foreach (var value in parameter.Value) { - _query = _query + parameter.Key + "=" + Uri.EscapeDataString(value) + "&"; + _query = _query + parameter.Key + "=" + Escape(value) + "&"; } } } diff --git a/templates/libraries/httpclient/api.mustache b/templates/libraries/httpclient/api.mustache index 6b5b86e4..e5a6b72c 100644 --- a/templates/libraries/httpclient/api.mustache +++ b/templates/libraries/httpclient/api.mustache @@ -487,7 +487,7 @@ namespace {{packageName}}.{{apiPackage}} // cookie parameter support if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) { - localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"), "/", "api.vrchat.cloud")); } {{/isKeyInCookie}} {{#isKeyInHeader}} @@ -507,7 +507,7 @@ namespace {{packageName}}.{{apiPackage}} // http basic authentication required if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) { - localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); + localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(System.Web.HttpUtility.UrlEncode(this.Configuration.Username) + ":" + System.Web.HttpUtility.UrlEncode(this.Configuration.Password))); } {{/isBasicBasic}} {{#isBasicBearer}} @@ -721,7 +721,7 @@ namespace {{packageName}}.{{apiPackage}} // cookie parameter support if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) { - localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"), "/", "api.vrchat.cloud")); } {{/isKeyInCookie}} {{#isKeyInHeader}} @@ -742,7 +742,7 @@ namespace {{packageName}}.{{apiPackage}} // http basic authentication required if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) { - localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); + localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(System.Web.HttpUtility.UrlEncode(this.Configuration.Username) + ":" + System.Web.HttpUtility.UrlEncode(this.Configuration.Password))); } {{/isBasicBasic}} {{#isBasicBearer}} diff --git a/templates/modelGeneric.mustache b/templates/modelGeneric.mustache index fca0622c..087dcd6c 100644 --- a/templates/modelGeneric.mustache +++ b/templates/modelGeneric.mustache @@ -364,13 +364,13 @@ return {{#vars}}{{#parent}}base.Equals(input) && {{/parent}}{{^isContainer}} ( this.{{name}} == input.{{name}} || - {{^vendorExtensions.x-is-value-type}} + {{^vendorExtensions.x-csharp-value-type}} (this.{{name}} != null && this.{{name}}.Equals(input.{{name}})) - {{/vendorExtensions.x-is-value-type}} - {{#vendorExtensions.x-is-value-type}} + {{/vendorExtensions.x-csharp-value-type}} + {{#vendorExtensions.x-csharp-value-type}} this.{{name}}.Equals(input.{{name}}) - {{/vendorExtensions.x-is-value-type}} + {{/vendorExtensions.x-csharp-value-type}} ){{^-last}} && {{/-last}}{{/isContainer}}{{#isContainer}} ( this.{{name}} == input.{{name}} || @@ -399,15 +399,15 @@ int hashCode = 41; {{/parent}} {{#vars}} - {{^vendorExtensions.x-is-value-type}} + {{^vendorExtensions.x-csharp-value-type}} if (this.{{name}} != null) { hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); } - {{/vendorExtensions.x-is-value-type}} - {{#vendorExtensions.x-is-value-type}} + {{/vendorExtensions.x-csharp-value-type}} + {{#vendorExtensions.x-csharp-value-type}} hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); - {{/vendorExtensions.x-is-value-type}} + {{/vendorExtensions.x-csharp-value-type}} {{/vars}} {{#isAdditionalPropertiesTrue}} if (this.AdditionalProperties != null) diff --git a/templates/netcore_project.additions.mustache b/templates/netcore_project.additions.mustache index 8c6f3ad5..69acd212 100644 --- a/templates/netcore_project.additions.mustache +++ b/templates/netcore_project.additions.mustache @@ -1 +1,39 @@ -{{! if needed users can add this file to their templates folder to append to the csproj }} \ No newline at end of file +{{! + Everything VRChat-specific about the generated csproj lives here, in the generator's own + extension point, rather than being sed-patched into the output afterwards. Rendered as the + last children of , so these PropertyGroup values override the ones above. +}} + + + true + + vrchat,vrcapi,vrc-api,vrc + Automated deployment + README.md + vrc_cat.ico + + + + + + + + + + + + + + + + True + \ + + + True + \ + + diff --git a/wrapper/VRChat.API.Extensions.Hosting/DefaultVRChatClientFactory.cs b/wrapper/VRChat.API.Extensions.Hosting/DefaultVRChatClientFactory.cs index 5f07958d..db635867 100644 --- a/wrapper/VRChat.API.Extensions.Hosting/DefaultVRChatClientFactory.cs +++ b/wrapper/VRChat.API.Extensions.Hosting/DefaultVRChatClientFactory.cs @@ -72,9 +72,8 @@ public bool TryAddClient(string clientName, VRChatClientBuilder vcb, bool overri { if (_builders.ContainsKey(clientName) && !overrideIfExists) // So that the default may be registered in the case of it being nonexistant return false; - else - _builders.Add(clientName, vcb); + _builders[clientName] = vcb; // Indexer rather than Add, so overrideIfExists actually overrides return true; } } diff --git a/wrapper/VRChat.API.Extensions.Hosting/IVRChatClientFactory.cs b/wrapper/VRChat.API.Extensions.Hosting/IVRChatClientFactory.cs index 83bb93d8..597da234 100644 --- a/wrapper/VRChat.API.Extensions.Hosting/IVRChatClientFactory.cs +++ b/wrapper/VRChat.API.Extensions.Hosting/IVRChatClientFactory.cs @@ -50,6 +50,16 @@ public interface IVRChatClientFactory /// Task LoginClientAsync(string name = "vrc_default", bool throwOnFail = false, CancellationToken ct = default); + /// + /// Registers a under a name, so + /// can later build a client from it. + /// + /// The name to register the builder under. + /// The to register. + /// Whether to replace an existing registration under the same name. + /// if the builder was registered; if a builder + /// is already registered under and + /// is . bool TryAddClient(string clientName, VRChatClientBuilder vcb, bool overrideIfExists = false); } } \ No newline at end of file diff --git a/wrapper/VRChat.API/Client/VRChat.cs b/wrapper/VRChat.API/Client/VRChat.cs index 9223ae29..fba7e622 100644 --- a/wrapper/VRChat.API/Client/VRChat.cs +++ b/wrapper/VRChat.API/Client/VRChat.cs @@ -203,6 +203,13 @@ public interface IVRChat Task LoginWithExternalCodeAsync(Func, ITwoFactorCode> codeAction, CancellationToken ct = default); } + /// + /// The default implementation, exposing every VRChat API surface through a + /// single client that shares one , cookie container and + /// . + /// + /// Instances are created through , or through + /// when you need to supply the underlying HTTP plumbing yourself. public class VRChatClient : IVRChat { private VRChatClient(Configuration configuration, string twoFactorSecret = null, ApiClient client = null, HttpClient httpClient = null, HttpClientHandler httpClientHandler = null) @@ -266,42 +273,93 @@ private VRChatClient(Configuration configuration, string twoFactorSecret = null, private readonly PlayermoderationApi _moderations; private readonly AuthenticationApi _authentication; + /// + /// The every API surface on this client was built with. + /// public Configuration Configuration => _configuration; + + /// + /// The that performs the underlying requests for every API surface on this client. + /// public ApiClient Client => _client; + + /// + /// The shared by every API surface on this client. + /// public HttpClient HttpClient => _httpClient; + + /// + /// The backing , whose + /// holds the session cookies returned by . + /// public HttpClientHandler HttpClientHandler => _httpClientHandler; + /// public IJamsApi Jams => _jams; + /// public IFilesApi Files => _files; + /// public IUsersApi Users => _users; + /// public IPropsApi Props => _props; + /// public IPrintsApi Prints => _prints; + /// public IGroupsApi Groups => _groups; + /// public IWorldsApi Worlds => _worlds; + /// public IInviteApi Invites => _invites; + /// public IAvatarsApi Avatars => _avatars; + /// public IEconomyApi Economy => _economy; + /// public IFriendsApi Friends => _friends; + /// public ICalendarApi Calendar => _calendar; + /// public IInventoryApi Inventory => _inventory; + /// public IInstancesApi Instances => _instances; + /// public IFavoritesApi Favorites => _favorites; + /// public IMiscellaneousApi Miscellaneous => _miscellaneous; + /// public INotificationsApi Notifications => _notifications; + /// public IPlayermoderationApi Moderations => _moderations; + /// public IAuthenticationApi Authentication => _authentication; + /// public bool IsLoggedIn { get; private set; } - // Creates a new VRChatClient, compatible with IVRChat - public static VRChatClient Create(Configuration configuration, string twoFactorSecret, ApiClient client, HttpClient httpClient, HttpClientHandler handler) => - new VRChatClient(configuration, twoFactorSecret, client, httpClient, handler); - + /// + /// Creates a new from pre-built components. + /// + /// Prefer unless you need to control the HTTP plumbing + /// yourself. Any argument left is replaced with a default instance, and a + /// yields a handler with cookies enabled. + /// The to build every API surface with. + /// The TOTP secret used by , or + /// if you intend to log in via . + /// The to issue requests through. + /// The to send requests with. + /// The handler backing ; its cookie + /// container carries the session returned by . + /// A new . It is not logged in until one of the login methods succeeds. + public static VRChatClient Create(Configuration configuration, string twoFactorSecret, ApiClient client, HttpClient httpClient, HttpClientHandler httpClientHandler) => + new VRChatClient(configuration, twoFactorSecret, client, httpClient, httpClientHandler); + + /// public List GetCookies() { return _httpClientHandler.CookieContainer?.GetAllCookies().ToList(); } + /// public async Task TryLoginAsync(CancellationToken ct = default) { CurrentUser user = null; @@ -317,6 +375,7 @@ public async Task TryLoginAsync(CancellationToken ct = defaul return new VRChatLoginResult(user == null, null); } + /// public async Task LoginAsync(CancellationToken ct = default) { if (_twoFactorSecret == null) @@ -348,6 +407,7 @@ public async Task LoginAsync(CancellationToken ct = default) return response.StatusCode == HttpStatusCode.OK ? user : null; } + /// public async Task LoginWithExternalCodeAsync(Func, ITwoFactorCode> codeAction, CancellationToken ct = default) { ApiResponse response = await this.Authentication.GetCurrentUserWithHttpInfoAsync(cancellationToken: ct); diff --git a/wrapper/VRChat.API/Client/VRChatClientBuilder.cs b/wrapper/VRChat.API/Client/VRChatClientBuilder.cs index 0d664542..8b7a6ce3 100644 --- a/wrapper/VRChat.API/Client/VRChatClientBuilder.cs +++ b/wrapper/VRChat.API/Client/VRChatClientBuilder.cs @@ -64,32 +64,31 @@ public static VRChatClientBuilder From(Configuration incomingConfiguration, ApiC new VRChatClientBuilder(incomingConfiguration, incomingClient); /// - /// + /// Sets the username, password and TOTP secret in one call. /// - /// - /// - /// - /// - /// + /// The username to authenticate with. + /// The password to authenticate with. + /// The TOTP secret used to answer a two-factor challenge. + /// This , so calls can be chained. public VRChatClientBuilder WithCredentials(string username, string password, string twoFactorSecret) => this .WithUsername(username) .WithPassword(password) .WithTwoFactorSecret(twoFactorSecret); /// - /// + /// Sets the username and password, for an account without two-factor authentication. /// - /// - /// - /// + /// The username to authenticate with. + /// The password to authenticate with. + /// This , so calls can be chained. public VRChatClientBuilder WithCredentials(string username, string password) => this.WithCredentials(username, password, null); /// - /// + /// Sets the username to authenticate with. /// - /// - /// + /// The account's username. VRChat also accepts the email address. + /// This , so calls can be chained. public VRChatClientBuilder WithUsername(string username) { _configuration.Username = username; @@ -97,10 +96,10 @@ public VRChatClientBuilder WithUsername(string username) } /// - /// + /// Sets the password to authenticate with. /// - /// - /// + /// The account's password. + /// This , so calls can be chained. public VRChatClientBuilder WithPassword(string password) { _configuration.Password = password; @@ -108,10 +107,13 @@ public VRChatClientBuilder WithPassword(string password) } /// - /// + /// Sets the TOTP secret used to answer a two-factor challenge. /// - /// - /// + /// This is the base32 secret shown when two-factor authentication is set up, not a + /// six-digit code. It is required by ; + /// without it, use to supply codes yourself. + /// The account's base32 TOTP secret. + /// This , so calls can be chained. public VRChatClientBuilder WithTwoFactorSecret(string twoFactorSecret) { _twoFactorSecret = twoFactorSecret; @@ -119,27 +121,33 @@ public VRChatClientBuilder WithTwoFactorSecret(string twoFactorSecret) } /// - /// + /// Authenticates with cookies from an earlier session instead of a username and password. /// - /// - /// - /// + /// Obtain these from on a logged-in client. + /// The auth cookie value. + /// The twoFactorAuth cookie value, which lets the session + /// skip the two-factor challenge. + /// This , so calls can be chained. public VRChatClientBuilder WithAuthCookie(string auth, string twoFactorAuth = null) { + // AddApiKey, not AddApiKeyPrefix: GetApiKeyWithPrefix returns `prefix + " " + value`, so + // storing a cookie as the prefix sent it with a trailing space and no value. if (auth != null) - _configuration.AddApiKeyPrefix("auth", auth); + _configuration.AddApiKey("auth", auth); if (twoFactorAuth != null) - _configuration.AddApiKeyPrefix("twoFactorAuth", auth); + _configuration.AddApiKey("twoFactorAuth", twoFactorAuth); return this; } /// - /// + /// Sets the User-Agent header sent with every request. /// - /// - /// + /// VRChat requires a descriptive User-Agent identifying your application and a way to + /// contact you. Prefer , which formats one correctly. + /// The User-Agent to send, or for the default. + /// This , so calls can be chained. public VRChatClientBuilder WithUserAgent(string userAgent) { _configuration.UserAgent = userAgent ?? _defaultUserAgent; @@ -147,10 +155,10 @@ public VRChatClientBuilder WithUserAgent(string userAgent) } /// - /// + /// Sets how long a request may take before it is abandoned. /// - /// - /// + /// The request timeout. + /// This , so calls can be chained. public VRChatClientBuilder WithTimeout(TimeSpan timeout) { _configuration.Timeout = timeout; // Using Miliseconds over TotalMilliseconds can cause issues when the timespan is empty @@ -158,10 +166,10 @@ public VRChatClientBuilder WithTimeout(TimeSpan timeout) } /// - /// + /// Routes every request through a proxy. /// - /// - /// + /// The proxy to send requests through. + /// This , so calls can be chained. public VRChatClientBuilder WithProxy(WebProxy proxy) { _configuration.Proxy = proxy; @@ -169,17 +177,24 @@ public VRChatClientBuilder WithProxy(WebProxy proxy) } /// - /// + /// Routes every request through the proxy at a URL. /// - /// - /// - /// + /// The proxy's address. + /// Whether to bypass the proxy for local addresses. + /// This , so calls can be chained. public VRChatClientBuilder WithProxy(string url, bool bypass = true) => this.WithProxy(new WebProxy(url, bypass)); /// + /// Sets the User-Agent from your application's details, in the form VRChat asks for. /// + /// Produces {name}/{version} ({contact}), VRChat.API/{libraryVersion}. VRChat may + /// block traffic whose User-Agent does not identify the application and a way to reach its author. + /// Your application's name. + /// Your application's version. + /// How VRChat can reach you, such as an email address or Discord handle. + /// This , so calls can be chained. public VRChatClientBuilder WithApplication(string name, string version, string contact) { var libraryVersion = Assembly.GetExecutingAssembly().GetName().Version!.ToString(); @@ -188,11 +203,15 @@ public VRChatClientBuilder WithApplication(string name, string version, string c } /// - /// + /// Builds the configured client. /// - /// - /// - /// + /// The returned client is not logged in. Call one of the login methods on + /// , or supply cookies through . + /// Whether to allow building a client that has no credentials, + /// which is valid for the endpoints that do not require authentication. + /// The configured client. + /// Thrown when is + /// and neither a username and password nor an auth cookie were set. public IVRChat Build(bool useWithoutCredentials = true) { if (!useWithoutCredentials) diff --git a/wrapper/VRChat.API/Model/GeneratedModelExtensions.cs b/wrapper/VRChat.API/Model/GeneratedModelExtensions.cs new file mode 100644 index 00000000..36ebeca2 --- /dev/null +++ b/wrapper/VRChat.API/Model/GeneratedModelExtensions.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace VRChat.API.Model +{ + /// + /// Additions to the generated , so it can be passed wherever a + /// two-factor code is accepted. + /// + public partial class TwoFactorAuthCode : ITwoFactorCode { } + + /// + /// Additions to the generated , so it can be passed wherever a + /// two-factor code is accepted. + /// + public partial class TwoFactorEmailCode : ITwoFactorCode { } + + /// + /// Additions to the generated . + /// + public partial class CurrentUser + { + /// + /// An array of two-factor authentication methods available to use to with two factor authentication. + /// + /// The API returns this field on the current-user response while a login is pending + /// two-factor verification, but the specification does not model it, so it is declared here. + [DataMember(Name = "requiresTwoFactorAuth", IsRequired = false, EmitDefaultValue = true)] + public List RequiresTwoFactorAuth { get; set; } + } +} diff --git a/wrapper/VRChat.API/VRChat.API.Wrapper.csproj b/wrapper/VRChat.API/VRChat.API.Wrapper.csproj deleted file mode 100644 index dc196511..00000000 --- a/wrapper/VRChat.API/VRChat.API.Wrapper.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - false - net8.0 - VRChat.API.Wrapper - VRChat.API.Wrapper - Library - VRChat API Docs Community - VRChat API Docs Community - VRChat API Library for .NET - VRChat API Library for .NET - Copyright © 2021 Owners of GitHub organisation "vrchatapi" and individual contributors. - VRChat.API - 1.20.5 - bin\$(Configuration)\$(TargetFramework)\VRChat.API.xml - MIT - https://github.com/vrchatapi/vrchatapi-csharp.git - git - Automated deployment - vrchat - README.md - - - - - - - - - -