Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add panic handler #140

Merged
merged 4 commits into from
Apr 19, 2023
Merged

Add panic handler #140

merged 4 commits into from
Apr 19, 2023

Conversation

HigherOrderLogic
Copy link
Contributor

@HigherOrderLogic HigherOrderLogic commented Feb 8, 2023

@HigherOrderLogic HigherOrderLogic marked this pull request as draft February 8, 2023 16:19
@HigherOrderLogic HigherOrderLogic marked this pull request as ready for review February 9, 2023 05:44
@HigherOrderLogic
Copy link
Contributor Author

@kangalioo this has been tested and is ready for review.

@GnomedDev
Copy link
Member

I don't believe poise should be catching these panics, as panics are catastrophic errors caused by programmer error, generally meaning state is no longer valid.

I can't even see this helping rust beginners, as it would teach them that panics are like exceptions in other languages and should be caught.

The only valid use of catch_unwind machinery in my head is FFI where it is "catch or UB", which this definitely isn't.

@HigherOrderLogic HigherOrderLogic changed the title wip: add panic handler Add panic handler Feb 9, 2023
@tazz4843
Copy link

tazz4843 commented Feb 9, 2023

I agree with Gnome here, and to add to that, it's nowhere near guaranteed that panics will be caught. This definitely will trick some beginners I think.

@cheesycod
Copy link

I agree too, this sounds like a antifeature to me

@kangalio
Copy link
Collaborator

kangalio commented Feb 9, 2023

What about the use case of emitting an error message to the user on bot panic? I don't think the current situation is user-friendly, where the bot just doesn't respond whatsoever when it hits an internal error / panic @GnomedDev @tazz4843 @cheesycod

@HigherOrderLogic
Copy link
Contributor Author

What about the use case of emitting an error message to the user on bot panic? I don't think the current situation is user-friendly, where the bot just doesn't respond whatsoever when it hits an internal error / panic

Does this mean that we would introduce poise's own Event enum?

@kangalio
Copy link
Collaborator

kangalio commented Feb 9, 2023

Does this mean that we would introduce poise's own Event enum?

What do you mean? I was responding to the other people, by the way. Also, thank you for making this pull request, I personally think it's an awesome future to have!

@HigherOrderLogic
Copy link
Contributor Author

What do you mean?

I mean if we want to emit a "panic event" then we have to introduce our own event system, don't we?

@kangalio
Copy link
Collaborator

kangalio commented Feb 9, 2023

I mean if we want to emit a "panic event" then we have to introduce our own event system, don't we?

Who said we want to emit a panic event 👀 With "emitting an error message" I just meant something like ctx.say("Bot unexpectedly panicked")

@HigherOrderLogic
Copy link
Contributor Author

With "emitting an error message" I just meant something like ctx.say("Bot unexpectedly panicked")

Oh, I see...

@GnomedDev
Copy link
Member

Emitting an error to the user on panic feels like trying to emit an error to the user when the network dies. If it is possible (as in, panicking will actually unwind, instead of aborting), the only thing that could be emitted is "something went wrong" which a user would be able to see anyway because the bot didn't respond to them.

@kangalio
Copy link
Collaborator

That's an extreme comparison. The emitted error can be more specific than you suggest: "Sorry, the command crashed unexpectedly (). The crash has been recorded and will be investigated" or whatever. That's certainly better than dead silence

Also, a panic handler allows you to record and track panics in the first place, which is also good to fix bugs

@tazz4843
Copy link

This still feels like it will very much encourage people to catch panics and treat them as recoverable errors.

@kangalio
Copy link
Collaborator

I think we shouldn't forgo useful features just because they can be misused. Also, I think it's worth exploring new design spaces. For example, I impulsively thought #51 was just an indicator for bad practice, but I implemented it anyways and it turned out very useful every now and then - I think the same can happen with a panic handler.

And in any case, if newbie Rust programmers indeed starting panicking everywhere due to this change, it can always be deprecated and removed

@kangalio
Copy link
Collaborator

kangalio commented Apr 1, 2023

Status: I like this feature very much. I still need to go through this PR carefully and check if every place that executes user code is guarded with a catch_unwind. I'm currently procrastinating this

@kangalio
Copy link
Collaborator

It would be best to update the existing FrameworkError variants Setup, EventHandler, Command, ArgumentParse, CommandCheckFailed, DynamicPrefix to also be able to contain a panic payload instead of an instance of the global error type E.

But I'm not sure how to do that without annoying churn for users. The cleanest API would be to replace the error: E fields with error: PanicOrError<E>, where PanicOrError is:

pub enum PanicOrError<E> {
    Panic(Box<dyn Any + Send + 'static>),
    Error(E),
}

But then, users will have to change all their code that interacts with the error to destructure the enum and handle the panic too, including downcasting to &str/String. I need opinions from everyone: is that ok? Are there ideas for a better solution?

@tazz4843
Copy link

Personally I feel like that adds another annoying edge case for error handling. I would like a feature flag to be able to opt out of that if it's going to end up with that enum.

@GnomedDev
Copy link
Member

Panic payloads are inherently dangerous as dropping them while handling a panic can cause an abort, this is one of the reasons I disagree with poise interacting with panic machinery (intended for unsafe FFI to be sound) at all.

@cheesycod
Copy link

If we are adding panic handling, i think it would be better to ensure that a panic just calls the handler and then ALWAYS kills the bot afterwards as a panic could mean that there's a unrecoverable error

@cheesycod
Copy link

I personally use panic = abort personally so I'm not the best to comment on this PR

@kangalio
Copy link
Collaborator

kangalio commented Apr 18, 2023

Personally I feel like that adds another annoying edge case for error handling. I would like a feature flag to be able to opt out of that if it's going to end up with that enum.

@tazz4843 I agree it's annoying. A feature flag would be an option, true. But not a great option imo, because feature flags are also annoying

Panic payloads are inherently dangerous as dropping them while handling a panic can cause an abort

@GnomedDev This is nothing special to panic payloads; anything that can panic while handling another panic will cause an abort. And this is a fair point, we need to avoid this trap if a panic handler were to be implemented. (For example tokio::spawn-ing the panic handler should work).

If we are adding panic handling, i think it would be better to ensure that a panic just calls the handler and then ALWAYS kills the bot afterwards as a panic could mean that there's a unrecoverable error

@cheesycod I still don't understand this radical view :/ What kind of panic can happen in a Discord bot that's so unrecoverable that you can't even post a Discord error message after it happens? What state can it even supposedly corrupt?

The one panic I can remember from my Discord bots right now was a bug in a graphing lib that panicked when given an empty x axis. When my bot hit that in a command, it just didn't respond anything. With a panic handler, I'd just write ctx.say(format!("Unexpected crash inside the bot: {}", panic)).await, and users would see that and everyone is happy.

Let me try to articulate my motivation here:

Panics are not an evil to be avoided always - it is just an error mechanism for unexpected states within the application. A panic is appropriate when the caller of the function typically doesn't need to consider the possibility of an error.

I believe many of the -> Result<_, Error> return types in a typical Discord bot codebase actually fall under that category. "Yeah technically the network could go down and the Discord message would fail to send, let's just return Result and slap an ? on every call" - this practice pollutes the meaning of Result.

If such errors that are always just propagated away disappeared from function signatures, the remaining errors that actually need specialized inline handling get the visibility they deserve. The mindset would shift from "Result? Let's use ?" to "Result? Let's check what can go wrong in practice and recover, depending on context". In the end, this will make for more robust programs.

@GnomedDev
Copy link
Member

The problem is that you are disagreeing with the entire rest of the Rust ecosystem, and this will be another step to understanding error handling for beginners.

Error handling is complicated enough without "oh and poise also catches and makes you handle panic payloads". This wouldn't clean up network error conditions either as most serenity results cannot be unwrapped as permission failures or other runtime issues would then be raised to a panic.

If this is merged I will be forced to start using panic=abort or maintain a fork with this reverted.

@kangalio
Copy link
Collaborator

We continued this discussion in the Discord and tldr I was completely flooded with disagreement and nobody except me seems to like making the panic handler custom.

This, coupled with the fact that API design for this feature is a bit hard, means I'll just surrender and close this PR. Sorry @HigherOrderLogic and thanks again

@kangalio kangalio closed this Apr 18, 2023
@kangalio
Copy link
Collaborator

  • I spoke to members of the Rust community Discord (link) and they said this is a totally valid use case of catch_unwind
  • I also did some benchmarking and catch_unwind seems to be zero-cost when no panic is thrown (@GnomedDev had performance concerns about wrapping all command code in catch_unwind)
  • I got an idea for a clean API, and a clean code implementation of it
  • I found out there's no hazard with double panics either

Soo I guess I will revive this.

This will be an optional and backwards-compatible change and I will note in the docs that panics are not meant as a general error mechanism - please don't despair.

@kangalio kangalio reopened this Apr 19, 2023
Forgot some stuff from restructguring
- Adds FrameworkError::CommandPanic
    - The default on_error impl spawns a nice error embed
- Wraps all instances of run_invocation/run_interaction/run_autocomplete in catch_unwind
- Adds a `div` command to the testing crate to test the panic catching (by triggering a div by zero)
@kangalio kangalio merged commit 707ceef into serenity-rs:develop Apr 19, 2023
kangalio added a commit that referenced this pull request Apr 19, 2023
Changes extracted from the pre-force-push #140 (6c082b5...1bac198)
kangalio added a commit that referenced this pull request Apr 20, 2023
* Restructure dispatch::slash functions like prefix

Forgot some stuff from restructguring

* Catch panics during command execution

- Adds FrameworkError::CommandPanic
    - The default on_error impl spawns a nice error embed
- Wraps all instances of run_invocation/run_interaction/run_autocomplete in catch_unwind
- Adds a `div` command to the testing crate to test the panic catching (by triggering a div by zero)

* Replace let-else with match to please MSRV

* Gate catch_unwind call behind feature flag

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>
kangalio added a commit that referenced this pull request Apr 20, 2023
Changes extracted from the pre-force-push #140 (6c082b5...1bac198)
kangalio added a commit that referenced this pull request Sep 19, 2023
* Move check inherit testing code to separate file

* Quickstart: auto-register commands on startup

* user_data_setup->setup

But keep a deprecated function alias so this is not breaking

* Rearrange quickstart type definitions

To save one line and to make the ordering more logical (Data and Error always appear in this order in poise, and Context should follow after because it refers to the previous two)

* Add register_in_guild and register_globally convenience

* Rename listener to event_handler

* Fix fmt and docs

* Change autocomplete callback value conversion from json::Value::from() to json::to_value() [json!() macro does this]. (#128)

* serenity_context and poise::Context traits

* Allow supplying timeout to modal execute

* Fix example

* Fix docs

* Bump to 0.5.0

* [Feature] Allow to bypass command checks for owners (#130)

* introduce skip_checks_for_owner feature

* fix skip_checks_for_owners

* fix missing owner check for `skip_checks_for_owners`

* Separate cooldowns and min max testing

It was stuffed into the add command before, now it's separate commands `cooldowns` and `minmax`

* Don't panic on modal timeout

* Fallback to HTTP in user_permissions even with cache enabled

* Update rustdoc-scrape-examples invocation

* Bump to 0.5.1

* Update Cargo.lock

To get rid of "cpufeatures 0.2.2 is yanked" message

* Impl SlashArgument instead of SlashArgumentHack

For primitive types and model types, the slash argumen trait impl is using SlashArgument instead of SlashArgumentHack now, which means those implementations are visible in the docs. Which is very much nicer than before.

Also, this commit makes SlashArgument::choices a default method. I decided it's annoying to have to unconditionally implement it even though it's `Vec::new()` in most cases. I originally didn't make it a default method just for "safety" so you don't forget to implement it _if_ you need it but I think that's a bad argument

* Fix sneaky bug

Welp and I was wondering why editing non-track_edits command `~say` re-runs the command (but not reuses response, or tracks deletions (coming in next commit))... It's because dispatch_message gets given a MessageEditFromInvalid... But why? Yeah because I mixed up the two match cases

* Add track_deletion

* Make builtins::servers aware of teams. (#133)

* Add all Context's methods to Prefix/ApplicationContext

Theoretically a small breaking change (some Context methods changed from &self to self. Some lifetimes were also changed from the &self lifetime to 'a, but this is not breaking because the lifetime has been strictly expanded. Anyways I'm not making a breaking release for this because it will realistically affect nobody and it causes more total pain making this a breaking release)

* Increase MSRV to 1.60.0 due to time crate

And set rust-version Cargo.toml field for the first time (it was added in 1.56)

* Update CI badge in README

badges/shields#8671

* Bump to 0.5.2

* Add missing `Detailed changelog`'s in CHANGELOG

* Add paginate

* Always print Command errors to console also

The result of a lengthy debugging ssession with Logan King. The problem: if a slash command initial response has an error (e.g. empty content), the interaction completely expires, you can't use it anymore. Any further attempt at sending a response is `Unknown Interaction` or `Unknown Webhook`. So the default error handler for command errors, which just sends the error message to Discord, ran into this, and was unable to send the error message to Discord. And the FrameworkOptions::on_error default error handler emits a log::error in that case, but obv the user doesn't see that when not having logging configured. So Logan King was rightly really confused why their bot was just endlessly showing "... Sending command ..." - that's apparently Discord's helpful way of saying "the bot did respond, but with an error, so we locked this interaction ^.^ and instead of 'this bot responded incorrectly' or something we'll show an endless loading message! this will definitely not confuse anyone ever! ^.^". ANYHOW, this commit unconditionally prints Command errors via eprintln!() as well in the default error handler, so it can't be missed

* Feature guard pagination example (#138)

* Print login message in framework_usage

* Add missing commas in macros for localizations (#143)

* Add missing commas in macros for localizations

* Add testing code

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>

* Document default_member_permissions

Fixes #145

* Add EventWrapper example

Fixes #146

* Add missing events to EventWrapper (#144)

* Add unit test example

Fixes #142

* Update links in README.md (kangalioo -> serenity-rs)

* Add panic handler (#140)

* Restructure dispatch::slash functions like prefix

Forgot some stuff from restructguring

* Catch panics during command execution

- Adds FrameworkError::CommandPanic
    - The default on_error impl spawns a nice error embed
- Wraps all instances of run_invocation/run_interaction/run_autocomplete in catch_unwind
- Adds a `div` command to the testing crate to test the panic catching (by triggering a div by zero)

* Replace let-else with match to please MSRV

* Gate catch_unwind call behind feature flag

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>

* Update kangalioo -> serenity-rs and fix typos

Changes extracted from the pre-force-push #140 (6c082b5...1bac198)

* Split develop branch into current and next

* Bump to 0.5.3

* Fix FrameworkContext::user_data being async (#137)

* Fix FrameworkContext::user_data being async

* fmt

* Make FrameworkContext::user_data async again

I realized the function was supposed to be async to stay API-compatible with the Framework methods, that's how it was created in c10ec4d.

This commit basically reverts #137, except for the lint changes

* Optionally display subcommands in help menu

Only one recursion level deep so far because infinite recursion would have been a pain and I don't even know if that's what users want so it's single recursion level for now and if someone complains I'll go through the async recursion trouble

* Amend to previous commit: clippy allow

* Remove redundant import in testing crate

* Change CommandPanic payload type to Option<String>

Previously, it was Box<dyn Any + Send> - making the entire type not Sync. This wasn't intentional.

This is technically a breaking change, but since CommandPanic was only introduced a few days ago, and was itself a breaking change, this is basically just a quick hotfix reversal of a breaking change.

* Bump to 0.5.4

* Support #[min_length] #[max_length]

Fixes #154

* Bump to 0.5.5

* Make a bunch of structs non_exhaustive...

...and rename AutocompleteChoice.name to label (I think it fits better)

* Forgot enums (while applying non_exhaustive)

* Test RoleParseError downcasting

* Rework of builtins::help() (#161)

* Rework of builtins::help()

* Use find_command instead of duplicating code

* Fix env variable `set` -> `export` for Unix

* Fix unneeded mut

* cargo fmt

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>

* Add HelpConfiguration:include_description

#161 changed single help command to include description. This commit makes it optional (but keeps the new behavior as default)

* Fix cargo test

* Change Command field types

- category: `Option<&'static str>` -> `Option<String>`
- help_text: `Option<fn() -> String>` -> `Option<String>`
- aliases: `&'static [&'static str]` -> `Vec<String>`
- context_menu_name: `Option<&'static str>` -> `Option<String>`

The &'static references meant you couldn't dynamically change those values which is very powerful so better do this breaking change now than potentially later.
It's not like it hurts to use String and Vec here due to the allocations - every bot will have a two-digit, max three-digit number of Command instances anyways and they will all be initialized on startup.

Also, the fn() in help_text was really ugly and was just a hack for rustbot to share common parts of the help text. But this should be done by just changing the help_text field in main() or whatever. Actually though, I was able to keep the #[command] attribute's help_text_fn field working. But it runs the function at command construction time now instead of on demand when help is called. So, behavior change technically

* Deduplicate CreateReply creation code

Adds Context::reply_builder

* Remove outdated TODO

* Remove pub(super) and make private instead

In my opinion pub(crate) or pub(any other path) is a code smell. If you're trying to use it, then either:
- your module structure is messed up and you should move code that accesses implementation detail of your type into the same module as that type (this was the reason for the pub(super) from this commit. The required restructuring I apparently already did some time ago)
- those "implementation" details should actually be public because trying to fully use that type evidently requires accessing those details (that's why for example poise's EditTracker methods are public, or why the CreateReply::reply_builder method I recently added is public)

* Remove unused function warning when testing

* Okay apparently Discord needs "es-ES" instead of "es" now

* Add ChoiceParameter trait instead of direct impls

Fixes #126

* fix: typo (#165)

Co-authored-by: xtfdfr <sadorowo@gmail.com>

* Support pattern parameters in #[command]

Fixes #158

Wow this more complicated than I expected

* Fix with simd-json (amend to 8471b6e)

* Restructure `#[command]` docs into sub headings

* Refinements to builtins::help (#167)

* Refinements to builtins::help

* Make rustfmt happy

* Make MSRV happy

---------

Co-authored-by: Rogier 'DocWilco' Mulhuijzen <github@bsdchicks.com>

* Add Infallible to __NonExhaustive enum variants

* Restructure examples

framework_usage and testing are gone in favor of basic_structure and framework_features. The examples/README now describes what each example contains. And the root README points to the examples folder

[amend commit] fix rustfmt

[amend commit] fix some things to make the example actually runnable (lol), fix feature gates, use poise::builtins::register_globally

* Add Context::{cache, http} shorthands

And remove the internal `.sc()` function in favor of the `.cache()` shorthand or full blown `.serenity_context()`

[amend commit] fix feature gate

* Use eprintln in default Setup error handler

Previously it used log::error which is not visible enough. If someone doesn't have a logger configured, they will miss it and it will cause BIG TIME confusion. Had it happen to someone I helped debug in #poise, and I Just fell into that trap myself and was downloading and using CodeLLDB, looking through tokio internals, looking up async debuggers, placing dbg!()'s deep within serenity's HTTP code; before finding out it was just some setup error

* feature: subcommmand_required parameter (#170)

* Add Infallible to __NonExhaustive enum variants

* add: subcommmand_required

* Fix a few things

- The code was not properly formatted (`cargo fmt --all`)
- There was a redundant is_subcommand value added to a bunch of things (including find_command - this PR was a breaking change and would have belonged to the `next` branch! But since is_subcommand was redundant and I removed it, this PR becomes not-breaking again (which makes sense; this kind of small feature shouldn't need breaking changes))
- The is_subcommand check was in the wrong place. The parse_invocation is really only for parsing, not for any action. If it returns an error, it should be an error in _parsing_, not in execution or usage. And it also belongs after the track_edits related checks, because if a message was edited that poise isn't even allowed to care about, it shouldn't throw an error too (this would have been another problem with the is_subcommand check in parse_invocation)

* Improve text

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>
Co-authored-by: xtfdfr <sadorowo@gmail.com>

* Make FrameworkError and SlashArgError variants non_exhaustive

* Added Support for Opening Modals as an MCI Response (#173)

* feat: added support for responding to an mci with a modal

* feat: added example, simplified modal send as mci interface

* fix: fixed example

* Some changes ™

- Move component modal example into modal.rs instead of making new file
- Make component modal example prefix_command too to catch any oversights that may make component modals rely on a slash command interaction being present (e.g. previously, execute_component_interaction took ApplicationContext and couldn't be invoked in prefix context at all)
- Deduplicate code between execute_modal and execute_component_interaction (and rename the latter to be clearer)
- Remove Modal::execute_component_interaction function in favor of free-standing execute_modal_on_component_interaction function - I want to avoid an explosion of utility trait functions for any combination of defaults yes/no, component yes/no... That said, I realize we're already halfway in this mess with execute and execute_with_defaults both existing so I'd also be open to go all the way and add execute_component_interaction and execute_component_interaction_with_defaults
- Add `impl AsRef<serenity::Context> for poise::Context` to make free-standing execute_modal_on_component_interaction function work well; and because it makes sense in general to have

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>

* Add CooldownContext struct to make Cooldowns usable in more situations (#175)

* Convert Cooldowns to use HashMap instead of OrderedMap for better scaling (#180)

* Fix failing CI build (#181)

- Update serenity 0.11.6 -> 0.11.5
- Update proc_macro2 1.0.47 -> 1.0.64
- Disambiguate several serenity re-exports
- Temporarily disable serenity/simdjson test since feature is broken upstream (serenity-rs/serenity#2474)

* add `reply()` for `poise::context` (#183)

* fix the behavior of poise::reply::builder::CreateReply.reply

* unnecessary change by mistake

* change back to original

* add reply()

* remove reply_reply()

* Adhere to unwritten codebase conventions

Made on phone in GitHub's absolutely janky web UI, and untested. Let's see how this goes

---------

Co-authored-by: kangalio <jannik.a.schaper@web.de>

* Two dependency bumps (#186)

* Bump `tokio` to `1.25.1`

* Bump `env_logger` to `1.10.0`

* Bump `tokio` to `1.25.1`

* Allow passing cooldown config on demand

That way, you can have different cooldown durations depending on which user / guild / whatever is invoking the command

* Remove old `CooldownTracker` methods and migrate internals

- Renames `CooldownTracker::new_2` and `CooldownTracker::remaining_cooldown_2` to `CooldownTracker::new` and `CooldownTracker::remaining_cooldown` respectively.
- Adds a new `cooldown_config` field to `Command` wrapped in a Mutex to allow updating by the user
- Adds a new example using a command check to modify the `cooldown_config` on the `Command`
- updates `#[poise::command]` to new `Command` structure

* Bump MSRV to 1.63

Needed for dependency updates in #186 that
were done to resolve some `cargo audit`
vulnerabilities

* Remove uneccessary NonZeroU64 usage

The new serenity id types impl PartialEq<u64> and From<u64> so we don't need to bother with hhe NonZeroU64 directly

* Fix copy paste typo in pagination button IDs

Co-authored-by: jamesbt365 <jamesbt365@gmail.com>

* Fix empty initial message in pagination

Co-authored-by: jamesbt365 <jamesbt365@gmail.com>

---------

Co-authored-by: kangalioo <jannik.a.schaper@web.de>
Co-authored-by: ChancedRigor <chancedrigor@users.noreply.github.com>
Co-authored-by: Peanutbother <6437182+peanutbother@users.noreply.github.com>
Co-authored-by: Andre Julius <noromoron@gmail.com>
Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com>
Co-authored-by: Maximilian Mader <max@bastelstu.be>
Co-authored-by: Whitney <67877488+whitbur@users.noreply.github.com>
Co-authored-by: Horu <73709188+HigherOrderLogic@users.noreply.github.com>
Co-authored-by: docwilco <66911096+docwilco@users.noreply.github.com>
Co-authored-by: franuś <97941280+xtfdfr@users.noreply.github.com>
Co-authored-by: xtfdfr <sadorowo@gmail.com>
Co-authored-by: Rogier 'DocWilco' Mulhuijzen <github@bsdchicks.com>
Co-authored-by: G0ldenSp00n <71467406+G0ldenSp00n@users.noreply.github.com>
Co-authored-by: B-2U <82710122+B-2U@users.noreply.github.com>
Co-authored-by: Overzealous Lotus <103554783+OverzealousLotus@users.noreply.github.com>
Co-authored-by: jamesbt365 <jamesbt365@gmail.com>
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.

5 participants