Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion .openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
src/VRChat.API.Test/**
docs/**
api/**
appveyor.yml
git_push.sh
build.sh
build.bat
mono_nunit_test.sh
nuget.exe
6 changes: 0 additions & 6 deletions VRChat.API.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand Down
105 changes: 36 additions & 69 deletions generate.sh
Original file line number Diff line number Diff line change
@@ -1,92 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail

if [ ${#} -le 1 ]
then
echo "Usage: generate.sh <openapi.yaml> <version>" >&2
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|\(.*\)</ItemGroup>|\1\t <Content Include="vrc_cat.ico" />\n\t <None Include="..\\README.md">\n\t <Pack>True</Pack>\n\t <PackagePath>\\</PackagePath>\n\t </None>\n\t <None Include="vrc_cat.png">\n\t <Pack>True</Pack>\n\t <PackagePath>\\</PackagePath>\n\t </None>\n </ItemGroup>|' src/VRChat.API/VRChat.API.csproj
sed -i '/<PackageTags>vrchat<\/PackageTags>/a\ <ApplicationIcon>vrc_cat.ico</ApplicationIcon>' src/VRChat.API/VRChat.API.csproj

# Adjust package tags
sed -i 's/<PackageTags>vrchat<\/PackageTags>/<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/<GenerateAssemblyInfo>false<\/GenerateAssemblyInfo>/<GenerateAssemblyInfo>true<\/GenerateAssemblyInfo>/' src/VRChat.API/VRChat.API.csproj

# Update VRChat.API.Extensions.Hosting version
sed -i "s|<Version>[^<]*</Version>|<Version>${2}</Version>|g" wrapper/VRChat.API.Extensions.Hosting/VRChat.API.Extensions.Hosting.csproj

# Add README.md to fields
sed -i '/PackageTags/a \ <PackageReadmeFile>README.md<\/PackageReadmeFile>' src/VRChat.API/VRChat.API.csproj
sed -i '/System.ComponentModel.Annotations/a \ <None Include="..\\README.md" Pack="true" PackagePath="\\"/>' src/VRChat.API/VRChat.API.csproj

# Add Otp.NET package to project
sed -i '/JsonSubTypes/a \ <PackageReference Include="Otp.NET" Version="1.4.0" \/>' 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 /// <summary>\n /// An array of two-factor authentication methods available to use to with two factor authentication.\n /// </summary>\n [DataMember(Name = "requiresTwoFactorAuth", IsRequired = false, EmitDefaultValue = true)]\n public List<string> 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 \
'<Version>' "s|<Version>[^<]*</Version>|<Version>${version}</Version>|g"
15 changes: 13 additions & 2 deletions templates/WebRequestPathBuilder.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,22 @@ namespace {{packageName}}.Client
_path = path;
}

/// <summary>
/// Percent-encodes a value, then restores "(" and ")".
/// </summary>
/// <remarks>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).</remarks>
private static string Escape(string value)
{
return Uri.EscapeDataString(value).Replace("%28", "(").Replace("%29", ")");
}

public void AddPathParameters(Dictionary<string, string> parameters)
{
foreach (var parameter in parameters)
{
_path = _path.Replace("{" + parameter.Key + "}", Uri.EscapeDataString(parameter.Value));
_path = _path.Replace("{" + parameter.Key + "}", Escape(parameter.Value));
}
}

Expand All @@ -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) + "&";
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions templates/libraries/httpclient/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand All @@ -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}}
Expand Down Expand Up @@ -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}}
Expand All @@ -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}}
Expand Down
16 changes: 8 additions & 8 deletions templates/modelGeneric.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}} ||
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 39 additions & 1 deletion templates/netcore_project.additions.mustache
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
{{! if needed users can add this file to their templates folder to append to the csproj }}
{{!
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 <Project>, so these PropertyGroup values override the ones above.
}}
<PropertyGroup>
<!-- The stock template sets this to false for non-generic-host clients, which breaks the build:
https://github.com/dotnet/project-system/issues/3934 -->
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<!-- Set here because generator property values cannot contain a comma. -->
<PackageTags>vrchat,vrcapi,vrc-api,vrc</PackageTags>
<PackageReleaseNotes>Automated deployment</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<ApplicationIcon>vrc_cat.ico</ApplicationIcon>
</PropertyGroup>

<ItemGroup>
<!-- Hand-written code that ships as part of this package, compiled straight from wrapper/
so there is exactly one definition of each type. Client/ holds the VRChatClient wrapper;
Model/ holds partial halves of generated model types. -->
<Compile Include="..\..\wrapper\VRChat.API\**\*.cs" Exclude="..\..\wrapper\VRChat.API\bin\**\*.cs;..\..\wrapper\VRChat.API\obj\**\*.cs" />
</ItemGroup>

<ItemGroup>
<!-- Required by the TOTP login path in wrapper/VRChat.API/Client/VRChat.cs. -->
<PackageReference Include="Otp.NET" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<Content Include="vrc_cat.ico" />
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="vrc_cat.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
10 changes: 10 additions & 0 deletions wrapper/VRChat.API.Extensions.Hosting/IVRChatClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public interface IVRChatClientFactory
/// </returns>
Task<VRChatLoginResult> LoginClientAsync(string name = "vrc_default", bool throwOnFail = false, CancellationToken ct = default);

/// <summary>
/// Registers a <see cref="VRChatClientBuilder"/> under a name, so <see cref="CreateClient(string)"/>
/// can later build a client from it.
/// </summary>
/// <param name="clientName">The name to register the builder under.</param>
/// <param name="vcb">The <see cref="VRChatClientBuilder"/> to register.</param>
/// <param name="overrideIfExists">Whether to replace an existing registration under the same name.</param>
/// <returns><see langword="true"/> if the builder was registered; <see langword="false"/> if a builder
/// is already registered under <paramref name="clientName"/> and <paramref name="overrideIfExists"/>
/// is <see langword="false"/>.</returns>
bool TryAddClient(string clientName, VRChatClientBuilder vcb, bool overrideIfExists = false);
}
}
Loading