Skip to content

Fixes deserialization on events StreamLabels.Underlying and StreamLabels#283

Closed
PedroCavaleiro wants to merge 5 commits intomeenzen:mainfrom
PedroCavaleiro:main
Closed

Fixes deserialization on events StreamLabels.Underlying and StreamLabels#283
PedroCavaleiro wants to merge 5 commits intomeenzen:mainfrom
PedroCavaleiro:main

Conversation

@PedroCavaleiro
Copy link
Contributor

@PedroCavaleiro PedroCavaleiro commented Feb 17, 2026

A required property that shouldn't be on StreamLabels event and a incorrect property type on StreamLabels.Underlying event were fixed

Here's a screenshot of one of the issues

Screenshot 2026-02-16 at 22 21 42

Summary by CodeRabbit

  • Bug Fixes

    • Streamlabs integration now tolerates optional/missing fields (reducing parse/display errors for absent counters and metrics).
  • New Features

    • Donation goal data is parsed as structured fields so title/current/target display reliably.
    • Top-donator and donation/metric feeds now accept varied input formats for more robust display of leaderboards.
  • Tests

    • Added comprehensive sample payload to improve coverage for streamlabels events.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Made multiple Streamlabels payload properties nullable and switched several string primitives to typed records/collections with a flexible JSON converter; added FlexibleObjectConverter<T>, a new StreamlabelsUnderlyingMessageDonationGoal record, updated related data types, and added a large test JSON plus a test case entry.

Changes

Cohort / File(s) Summary
Streamlabels message optional field
src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
Changed CloudbotCounterDeaths from required string to string? (commented as possibly absent) in two occurrences.
Underlying message data type changes
src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
Many properties changed from non-nullable primitives/strings to nullable complex types (e.g., DonationGoalStreamlabelsUnderlyingMessageDonationGoal?, various top-donator/donation fields → TopDonator?/IReadOnlyCollection<TopDonator>?, CounterCounter?) and multiple properties gained [JsonConverter(typeof(FlexibleObjectConverter<...>))] to allow flexible deserialization.
New donation-goal record
src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
Added public sealed record StreamlabelsUnderlyingMessageDonationGoal with required Title, CurrentAmount, and GoalAmount (JSON property mappings).
Flexible JSON deserializer
src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
Added FlexibleObjectConverter<T> (generic JsonConverter<T>) that deserializes from objects/arrays or JSON-in-a-string; returns null for unsupported tokens; writes via JsonSerializer.
Tests and fixtures
test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json, test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs
Added a large test fixture streamlabelsUnderlying3.json and registered it in MessageTypeTests data set to exercise the updated Streamlabels underlying payload shapes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I munched through JSON, nibbled types anew,
Made fields optional and converters chew.
Donation goals sprouted three tidy stems,
Tests hopped in, validating the gems. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main changes: fixing deserialization issues in StreamLabels.Underlying and StreamLabels events by making properties nullable and adding flexible JSON converter support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/meenzen/Streamlabs.SocketClient/issues/comments/3915223643","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: failure by coderabbit.ai -->\n\n> [!CAUTION]\n> ## Review failed\n> \n> Failed to post review comments\n\n<!-- end of auto-generated comment: failure by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nRefactored message data types by making the `CloudbotCounterDeaths` field optional, extracting the `DonationGoal` property into a dedicated sealed record type with three required string properties, and updating the corresponding property declaration to use the new type.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**CloudbotCounterDeaths Optional Field** <br> `src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs`|Made `CloudbotCounterDeaths` property optional (non-required) with added comment indicating potential absence from message payload.|\n|**DonationGoal Type Extraction** <br> `src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs`, `src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs`|Extracted `DonationGoal` from string type to new sealed record `StreamlabelsUnderlyingMessageDonationGoal` with three required string properties (`Title`, `CurrentAmount`, `GoalAmount`) and corresponding JSON property mappings.|\n\n## Estimated code review effort\n\n🎯 2 (Simple) | ⏱️ ~10 minutes\n\n## Poem\n\n> 🐰 A hop and a skip through data we go,  \n> Making fields soft when they needn't show,  \n> DonationGoals now have structure so fine,  \n> Three properties dancing in perfect align! ✨\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 3</summary>\n\n<details>\n<summary>✅ Passed checks (3 passed)</summary>\n\n|     Check name     | Status   | Explanation                                                                                                                                                                                   |\n| :----------------: | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                   |\n|     Title check    | ✅ Passed | The title directly and accurately summarizes the main changes: fixing deserialization issues in StreamLabels.Underlying and StreamLabels events by correcting property types and nullability. |\n| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.                                                                                    |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- announcements_start -->\n\n> [!TIP]\n> [Issue Planner](https://www.coderabbit.ai/issue-planner) is now in beta. Read the [docs](https://docs.coderabbit.ai/issues/planning) and try it out! Share your feedback on [Discord](https://discord.com/invite/coderabbit).\n\n<!-- announcements_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=meenzen/Streamlabs.SocketClient&utm_content=283)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/meenzen/Streamlabs.SocketClient/issues/comments/3915223643","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 17 Feb 2026 15:02:04 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"3434:1FAD5D:D6C6A:395842:699482EB","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

@meenzen
Copy link
Owner

meenzen commented Feb 18, 2026

Do you by any chance have the full json payloads which triggered the deserialization failures? That would allow us to add a test case here:

public static IEnumerable<Func<JsonFile>> GetData()
{
yield return () => new JsonFile("alertPlaying.json", typeof(AlertPlayingEvent), "FollowAlertPlaying");
yield return () =>
new JsonFile("alertPlaying_subscription.json", typeof(AlertPlayingEvent), "SubscriptionAlertPlaying");
yield return () => new JsonFile("alertPlaying_bits.json", typeof(AlertPlayingEvent), "BitsAlertPlaying");
yield return () =>
new JsonFile("alertPlaying_subMysteryGift.json", typeof(AlertPlayingEvent), "SubMysteryGiftAlertPlaying");
yield return () => new JsonFile("alertPlaying_raid.json", typeof(AlertPlayingEvent), "RaidAlertPlaying");
yield return () => new JsonFile("alertPlaying_follow.json", typeof(AlertPlayingEvent), "FollowAlertPlaying");
yield return () => new JsonFile("bits.json", typeof(BitsEvent));
yield return () => new JsonFile("bits2.json", typeof(BitsEvent));
yield return () => new JsonFile("donation.json", typeof(DonationEvent));
yield return () => new JsonFile("donationDelete.json", typeof(DonationDeleteEvent));
yield return () => new JsonFile("follow.json", typeof(FollowEvent));
yield return () => new JsonFile("muteVolume.json", typeof(MuteVolumeEvent));
yield return () => new JsonFile("raid.json", typeof(RaidEvent));
yield return () => new JsonFile("rollEndCredits.json", typeof(RollEndCreditsEvent));
yield return () => new JsonFile("streamlabels.json", typeof(StreamlabelsEvent));
yield return () => new JsonFile("streamlabelsUnderlying.json", typeof(StreamlabelsUnderlyingEvent));
yield return () => new JsonFile("streamlabelsUnderlying2.json", typeof(StreamlabelsUnderlyingEvent));
yield return () => new JsonFile("subMysteryGift.json", typeof(SubMysteryGiftEvent));
yield return () => new JsonFile("subMysteryGift1.json", typeof(SubMysteryGiftEvent));
yield return () => new JsonFile("subscription.json", typeof(SubscriptionEvent));
yield return () => new JsonFile("subscription2.json", typeof(SubscriptionEvent));
yield return () => new JsonFile("subscription3.json", typeof(SubscriptionEvent));
yield return () =>
new JsonFile("subscriptionPlaying.json", typeof(SubscriptionPlayingEvent), "SubscriptionPlaying");
}

@PedroCavaleiro
Copy link
Contributor Author

I do, for both events, I'll add them later today when I get home.

Introduces a `FlexibleObjectConverter` to handle varying JSON input types for properties within Streamlabs messages. This converter allows deserialization from direct JSON objects/arrays, or from double-encoded JSON strings, preventing errors due to API inconsistencies.

Applies the `FlexibleObjectConverter` to properties in `StreamlabelsUnderlyingMessageData` that exhibit this flexible behavior, such as donator objects and lists, making them nullable to accommodate unparseable string values.

Adds a new test case to validate the robust deserialization of these flexible JSON structures.
@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs (1)

93-121: ⚠️ Potential issue | 🟠 Major

Apply FlexibleObjectConverter<TopDonator> to monthly, weekly, 30day, and session top monthly donator properties

The type mismatch identified is real and affects 10 properties (5 singular, 5 plural). AllTimeTopMonthlyDonator and AllTimeTopMonthlyDonators are correctly typed as TopDonator and IReadOnlyCollection<TopDonator> respectively, but all other variants (MonthlyTopMonthlyDonator, WeeklyTopMonthlyDonator, ThirtyDayTopMonthlyDonator, SessionTopMonthlyDonator and their plural forms) remain as required string.

Test fixtures currently contain only empty strings for these fields, masking the issue. However, when the API returns objects (analogous to the pattern that occurs with AllTimeTopMonthlyDonator), deserialization will fail with Cannot get the value of a token type 'StartObject' as a string.

Apply the same fix used for MonthlyTopDonator (line 43–44): add [JsonConverter(typeof(FlexibleObjectConverter<TopDonator>))] attributes and change the singular variants to public TopDonator? [PropertyName] { get; init; }, and the plural variants to public IReadOnlyCollection<TopDonator>? [PropertyName] { get; init; }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs`
around lines 93 - 121, The monthly/weekly/30day/session top monthly donator
properties are wrongly typed as string; change each singular property
(MonthlyTopMonthlyDonator, WeeklyTopMonthlyDonator, ThirtyDayTopMonthlyDonator,
SessionTopMonthlyDonator) to nullable TopDonator? and each plural property
(MonthlyTopMonthlyDonators, WeeklyTopMonthlyDonators,
ThirtyDayTopMonthlyDonators, SessionTopMonthlyDonators) to nullable
IReadOnlyCollection<TopDonator>? and add the
[JsonConverter(typeof(FlexibleObjectConverter<TopDonator>))] attribute to each
of those eight properties (matching the pattern used on MonthlyTopDonator), so
deserialization accepts either strings or objects; leave
AllTimeTopMonthlyDonator and AllTimeTopMonthlyDonators as-is.
🧹 Nitpick comments (2)
src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs (2)

36-43: Silent null on JsonException discards data without any diagnostic

When a heuristically-detected JSON string fails to parse, the exception is swallowed and null is returned. This makes it hard to distinguish a legitimate "no data" null from a malformed payload.

Consider at minimum a Debug/Trace log entry before returning null, so failures are observable without impacting production behaviour.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs` around
lines 36 - 43, In FlexibleObjectConverter (the code path that tries
JsonSerializer.Deserialize<T>(trimmed, options)), don’t silently swallow
JsonException — log the exception and relevant context before returning null;
update the catch (JsonException) block to write a Debug/Trace (or ILogger if
available) entry that includes the exception message and the trimmed input (or
at least its length/preview) and then return null so malformed payloads are
observable during debugging.

53-56: Write risks infinite recursion if the converter is ever registered globally

JsonSerializer.Serialize(writer, value, options) dispatches through options.Converters. If this converter is added to JsonSerializerOptions.Converters rather than only used via [JsonConverter] attribute, calling Write would re-enter itself indefinitely.

The standard mitigation is to use the object-typed overload so the call skips the generic converter cache entry for T:

🛡️ Safer Write pattern
 public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
 {
-    JsonSerializer.Serialize(writer, value, options);
+    JsonSerializer.Serialize(writer, (object?)value, typeof(T), options);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs` around
lines 53 - 56, The Write method on FlexibleObjectConverter<T> can recurse if
this converter is globally registered because JsonSerializer.Serialize(writer,
value, options) will re-dispatch through options.Converters; change the
implementation of Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
options) to call the non-generic/object overload so the generic converter cache
is bypassed — e.g. use JsonSerializer.Serialize(writer, (object)value,
typeof(T), options) (or JsonSerializer.Serialize(writer, (object)value, options)
with explicit Type) to serialize without re-entering FlexibleObjectConverter<T>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs`:
- Around line 93-121: The monthly/weekly/30day/session top monthly donator
properties are wrongly typed as string; change each singular property
(MonthlyTopMonthlyDonator, WeeklyTopMonthlyDonator, ThirtyDayTopMonthlyDonator,
SessionTopMonthlyDonator) to nullable TopDonator? and each plural property
(MonthlyTopMonthlyDonators, WeeklyTopMonthlyDonators,
ThirtyDayTopMonthlyDonators, SessionTopMonthlyDonators) to nullable
IReadOnlyCollection<TopDonator>? and add the
[JsonConverter(typeof(FlexibleObjectConverter<TopDonator>))] attribute to each
of those eight properties (matching the pattern used on MonthlyTopDonator), so
deserialization accepts either strings or objects; leave
AllTimeTopMonthlyDonator and AllTimeTopMonthlyDonators as-is.

---

Nitpick comments:
In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs`:
- Around line 36-43: In FlexibleObjectConverter (the code path that tries
JsonSerializer.Deserialize<T>(trimmed, options)), don’t silently swallow
JsonException — log the exception and relevant context before returning null;
update the catch (JsonException) block to write a Debug/Trace (or ILogger if
available) entry that includes the exception message and the trimmed input (or
at least its length/preview) and then return null so malformed payloads are
observable during debugging.
- Around line 53-56: The Write method on FlexibleObjectConverter<T> can recurse
if this converter is globally registered because
JsonSerializer.Serialize(writer, value, options) will re-dispatch through
options.Converters; change the implementation of Write(Utf8JsonWriter writer, T
value, JsonSerializerOptions options) to call the non-generic/object overload so
the generic converter cache is bypassed — e.g. use
JsonSerializer.Serialize(writer, (object)value, typeof(T), options) (or
JsonSerializer.Serialize(writer, (object)value, options) with explicit Type) to
serialize without re-entering FlexibleObjectConverter<T>.

@PedroCavaleiro
Copy link
Contributor Author

PedroCavaleiro commented Feb 21, 2026

So it appears that when there are no values StreamLabs sends a empty string, I've created a custom converter that when applied to a property tries to do the following

  1. Checks if it's a object if it is it tries to deserialize it
  2. If it's not a direct object but it's a json serialized string it attempts to deserialize it
  3. If it's a plain string it returns null

For this to work I needed to make some properties non required and nullable

As requested I added the test of the exact JSON that was causing the issue, all 3 are now passing

@meenzen
Copy link
Owner

meenzen commented Feb 22, 2026

Thanks for your contribution!

I've merged these changes in #289 since I couldn't modify your branch. I'll release this as v2.0.0 soon, since this technically is a breaking change.

Regarding the custom JSON converter, yeah the Streamlabs api is seriously messed up. It makes no sense that they're sending an empty string for a missing object instead of null. The property naming and data types are also very inconsistent. Their backend likely is a messy Node.js service.

That's actually the reason I made this library, to make using the Streamlabs API more ergonomic. I hope this works well for you.

@meenzen meenzen closed this Feb 22, 2026
@meenzen
Copy link
Owner

meenzen commented Feb 22, 2026

The v2.0.0 release is available now: https://github.com/meenzen/Streamlabs.SocketClient/releases/tag/v2.0.0

Thanks again for opening this PR, you're the first contributor who's helped out with the project. If you need anything or find more issues, feel free to let me know.

@PedroCavaleiro
Copy link
Contributor Author

Thank you for your work. I will provide any more fixes if I find them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants