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

Athena's Shield | The Extra File Verification System #364

Open
wants to merge 56 commits into
base: master
Choose a base branch
from

Conversation

Sandro642
Copy link

Hello Daniel Scalzi,

I am reaching out to present a project I have been working on called Athena's Shield. This security system is designed to integrate with your game launcher, HeliosLauncher, to ensure a secure and reliable gaming experience for Minecraft users.

What is Athena's Shield?

Athena's Shield verifies the integrity of the mods used in HeliosLauncher. It ensures that only authorized and validated mods are present by blocking unauthorized modifications. With a validation system based on the names and digital fingerprints of the mods, Athena's Shield protects users from malicious or altered mods.

Why should you consider integrating Athena's Shield?

Enhanced Security: Athena's Shield prevents the use of unauthorized mods, protecting users from security risks.
Reliability: This system guarantees a stable gaming experience by ensuring that only verified mods are used.
Ease of Use: Clear error messages and instructions help users resolve potential issues.
I have dedicated a lot of time and effort to developing Athena's Shield, taking into account feedback from other users. Although I attempted a similar project in the past without success, I have learned from my mistakes and improved this project to make it more credible and functional.

I invite you to try Athena's Shield on my GitHub repository, where you will also find a detailed explanatory video outlining its functionality and features just below this paragraph. This will allow you to personally evaluate the effectiveness of this system and see the efforts invested in its development.

I am confident that Athena's Shield could bring real added value to HeliosLauncher, and I hope to have the opportunity to discuss this proposal with you.

I am currently seeking your trust to assist you with this project by demonstrating my genuine dedication to you and HeliosLauncher.

Thank you for your time and consideration.

Respectfully,
Soria Sandro (Sandro642).

https://youtu.be/wpsLfU47tG0

Introduce AthenaShield class to manage configuration, CLI for user setup, and update package.json with new script. This enhances the application's configurability and user interaction.
Implemented a new `dlAsync.js` file and updated the `en_US.toml` with new mod validation messages. This includes the logic for mod verification and error handling, ensuring the integrity of mods before launching the game.

Ajout prochainement de l'utilitaire npm run athshield
Replaced 'path' with 'fs' module and removed unused 'Lang'. Added support for the new HeliosLauncher version and implemented a whitelist for mods. Fixed a typo in the logging message.
Corrected the typo from "launchingGame" to "waintingLaunchingGame" in the en_US language file. This ensures the message displayed to users is accurate and free of errors.
Implemented a whitelist for mods and added support for the new version of HeliosLauncher. Also corrected a language key for launch information logging.
Implemented mod verification using AthShield when enabled. Added detailed mod identity extraction and validation logic for better integrity checks. Added logs for each verification step and fallback to hash-based identity if the manifest is missing.

Grande ligne : quand tu actives ath shield alors il utilise le système Athena's Shield.
Integrated Athena's Shield for mod verification, including mod identity extraction and validation against expected identities. Added exclusion list for mods, and implemented fallbacks for missing mod identities using MD5 hashes. Adjusted logging and error reporting to provide clearer feedback.
Relocated athshield files to app/assets/athshield for better organization and maintainability. This change improves the project's folder structure and simplifies file management.
Include messages for when Athena's Shield is activated and deactivated in the English localization file. This will provide users with clear notifications on the status of Athena's Shield.
Relocated the athshield.js script from ./athshield/ to app/assets/athshield/. This change ensures better organization of script files within the project structure.
Renamed the `view` method to `type` in parserAthShield.js to better reflect its purpose. Removed unused `athShield` import from landing.js and added it to settings.js. Added a manageModCategory function in settings.js to manage the display and interaction state of the Mods tab based on the type of `athShield`.
Separate logic for 'cacher' and 'bloquer' options and update corresponding values to 'hidden' and 'blocked' respectively. Ensure configuration is saved after setting the menu visibility.
Updated all comments and user prompts in athshield.js from French to English for better code readability and broader usability. No functional changes were made.
Updated console log messages in athshield.js to use single quotes for consistent escape character handling. This adjustment ensures better compatibility and readability of string literals in the code.
Replaced direct assignment of `dataPath` with variable `nameDataPath` for consistent naming. Introduced `getNameDataPath` function to return the launcher directory name string.
Remove unused dataPath constant and replace its usage in mod validation error message with ConfigManager.getNameDataPath(). This ensures the config path is retrieved dynamically.
Changed "private" field in package.json from true to false. This will make the package accessible on npm and allow others to install it.
Added .gitignore to exclude IDE-specific files, set up project code style configuration, included game and launcher settings in config.json, and created minimal discord and distribution JSON files for project setup.
Introduced an option to activate debug mode in Athena's Shield. Updated relevant JavaScript files and configuration to handle the debug mode setting. Added a new method for retrieving the debug status within the parserAthShield.js class.
Wrapped several logging statements related to module identity extraction and validation with a conditional check on the athShield.debug flag. This ensures that detailed logging information is recorded only when debugging is enabled, optimizing performance and log clarity.
Switched the distribution URL to a more reliable API endpoint to improve stability and performance. The new URL is 'https://api.skym-mc.fr/api/v1/servers/distro', which replaces the old one.
Simplify the log message for identity not found in the manifest by combining it with the hash usage statement. This improves readability and reduces redundancy in the code.
Revert distribution URL to 'https://helios-files.geekcorner.eu.org/distribution.json'. This change ensures compatibility with the older distribution endpoint and corrects the previous, unintended URL.
This commit refactors the mod identity extraction and validation process to include detailed debug logs, which are conditionally logged based on the `athShield.debug` flag. It also updates the import statements and correctly references the `ConfigManager.getNameDataPath` function for error messages.
Corrected the typo "discovereds" to "discovered" in the comment section of the landing.js file. This ensures accuracy and professionalism in the documentation.
Deleted the dlAsync.js and landing.js files from both "ancien code" and "version code final" directories. This cleanup removes outdated functionality related to mod validation and launcher processes, streamlining the codebase.
Renamed Athena's Shield documentation file and added detailed sections explaining its purpose, key features, and user benefits. Introduced a new security feature for HeliosLauncher, ensuring the integrity of installed mods.
The crypto package is added to the dependencies in package.json to support encryption-related functionalities. This addition helps in enhancing the security features of the application.
@Sandro642
Copy link
Author

I made a lot of commits, but I wanted my pull request to be really clean, and all the code is available in the "changed files" section.

@Shisuiicaro
Copy link

Does it fix deleting something from nebula but i does not get deleted in the launcher?

@Sandro642
Copy link
Author

Does it fix deleting something from nebula but i does not get deleted in the launcher?

What do you mean by deleting something from Nebula? All the mods in your distribution are compared by file name and hash (MD5). The Athena’s Shield code is integrated into the launcher, and when you add the feature, it doesn’t cause any issues, even if you have an older version of the launcher.

@Shisuiicaro
Copy link

I mean, sometimes you delete something from the distro, but it dosen't get deleted form the user instance, like config files

@Sandro642
Copy link
Author

I mean, sometimes you delete something from the distro, but it dosen't get deleted form the user instance, like config files

That’s completely normal; it’s due to the application’s cache. The cache stores information it expects to reuse often, so it doesn’t check the data and instead uses the cached version to speed things up. However, Athena's Shield doesn’t manage this aspect. I’d recommend clearing the cache if this issue occurs.

Copy link
Contributor

@GeekCornerGH GeekCornerGH left a comment

Choose a reason for hiding this comment

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

Overall, it looks nice, thanks for your contribution.
I think @dscalzi will probably not merge it under the "Athena's Shield" name however.
I also added a few comments.

README.md Outdated
Comment on lines 135 to 164
**How to active this ?**

```console
> npm run athshield
```

```
Would you like to activate Athena's Shield? (yes/no): yes
Would you like to activate debug mode? (yes/no): yes
Would you like to hide or block the menu? (hide/block): block

Athena's Shield activated. Menu blocked.
```

You can choose whether Athena's Shield hides the mod category or blocks interaction with the drop-in mod.

```console
▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ███▄ █ ▄▄▄ ██████ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ██ ▀█ █ ▒████▄ ▒██ ▒ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ▀█ ██▒▒██ ▀█▄ ░ ▓██▄ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒ ██▒ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒▒██░ ▓██░ ▓█ ▓██▒▒██████▒▒ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░▒ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
░ ▒ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
```

---

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this belongs in the readme. I'd put it either in the docs folder, or in the wiki

Copy link
Author

Choose a reason for hiding this comment

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

Pour ça si le code sera merge il faudra le faire mais pour l'instant je ne peux pas

Copy link
Author

Choose a reason for hiding this comment

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

For that, if the code is going to be merged, it will have to be done, but for now, I can’t.

const path = require('path')

// Chemin vers le fichier de configuration
const configPath = path.join(__dirname, 'variables.athshield')
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not simply use a json extension?

Copy link
Author

Choose a reason for hiding this comment

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

Je vais faire en sorte d'utiliser un fichier json

Copy link
Author

Choose a reason for hiding this comment

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

I'll make sure to use a JSON file.

Comment on lines 440 to 469
/**
* @Name dlAsync Function
* @returns {Promise<void>}
*
* @author Sandro642
* @Cheating Athena's Shield
*
* @Added whitelist for mods
* @Added support for the new HeliosLauncher version
*/

/**
* @Reviewed on 10.26.2024 expires on XX.XX.2025
* @Bugs discovereds: 0
* @Athena's Shield
* @Sandro642
*/


// ▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ███▄ █ ▄▄▄ ██████ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
// ▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ██ ▀█ █ ▒████▄ ▒██ ▒ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
// ▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ▀█ ██▒▒██ ▀█▄ ░ ▓██▄ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
// ░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒ ██▒ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
// ▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒▒██░ ▓██░ ▓█ ▓██▒▒██████▒▒ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
// ▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
// ▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░▒ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
// ░ ▒ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
// ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
// ░

Copy link
Contributor

Choose a reason for hiding this comment

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

Was this necessary?

Suggested change
/**
* @Name dlAsync Function
* @returns {Promise<void>}
*
* @author Sandro642
* @Cheating Athena's Shield
*
* @Added whitelist for mods
* @Added support for the new HeliosLauncher version
*/
/**
* @Reviewed on 10.26.2024 expires on XX.XX.2025
* @Bugs discovereds: 0
* @Athena's Shield
* @Sandro642
*/
// ▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ███▄ █ ▄▄▄ ██████ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
// ▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ██ ▀█ █ ▒████▄ ▒██ ▒ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
// ▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ▀█ ██▒▒██ ▀█▄ ░ ▓██▄ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
// ░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒ ██▒ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
// ▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒▒██░ ▓██░ ▓█ ▓██▒▒██████▒▒ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
// ▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
// ▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░▒ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
// ░ ▒ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
// ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
// ░

Copy link
Author

Choose a reason for hiding this comment

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

It's just a matter of aesthetics, but it's not necessary.

Comment on lines 1 to 5
{
"athenaShieldActivated": false,
"menuVisibility": "visible",
"debug": "false"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, why not use a json file
Also please add a new line at the end of this file.

Suggested change
{
"athenaShieldActivated": false,
"menuVisibility": "visible",
"debug": "false"
}
{
"athenaShieldActivated": false,
"menuVisibility": "visible",
"debug": "false"
}

Copy link
Author

Choose a reason for hiding this comment

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

Got it about the JSON file, and I’ll add a line at the end.

mdls.forEach(mdl => {
if (mdl.rawModule.name.endsWith('.jar')) {
const modPath = path.join(modsDir, mdl.rawModule.name)
const modIdentity = mdl.rawModule.identity || mdl.rawModule.artifact.MD5
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the identity property is not a thing

Copy link
Author

Choose a reason for hiding this comment

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

I'll take a look at that.

loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modIdentityExtraction', {'filePath': filePath}))
}

// Fall back to a hash if no identity is found
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see other code in that function other than the one generating a md5 hash?

Copy link
Author

Choose a reason for hiding this comment

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

Previously, there was an old function, but I removed it and forgot to delete the comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why are the comments in french?

Copy link
Author

Choose a reason for hiding this comment

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

I started my code in French, but I had to translate it into English, and this is the only file I didn’t translate—a lapse in attention.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this file meant to be here?

Copy link
Author

Choose a reason for hiding this comment

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

This is the documentation to check if the system works, but I can remove it. These are the procedures to ensure the extra file verification system functions correctly.

@Sandro642
Copy link
Author

Overall, it looks nice, thanks for your contribution.

I think @dscalzi will probably not merge it under the "Athena's Shield" name however.

I also added a few comments.

Thank you GeekCorner for responding to my message; of course, it will always be possible to change the name.

And I will redo certain parts in the code as you reviewed some sections of it.

@Sandro642
Copy link
Author

I have revised my code based on the reviews you provided. I changed the name from 'Athena's Shield' to 'Extra File Verification,' removed unnecessary aesthetic lines, and eliminated the PDF file. Additionally, for the text in the readme.md file, if my pull request is merged, please add it to the wiki

Copy link
Contributor

@GeekCornerGH GeekCornerGH left a comment

Choose a reason for hiding this comment

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

A few more changes needed

"extraFileVerifActivated": false,
"menuVisibility": "visible",
"debug": false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}
}

Copy link
Author

Choose a reason for hiding this comment

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

I had deliberately added a space, but when I commit and push, it doesn't show, but there is one.

mdls.forEach(mdl => {
if (mdl.rawModule.name.endsWith('.jar')) {
const modPath = path.join(modsDir, mdl.rawModule.name)
const modIdentity = mdl.rawModule || mdl.rawModule.artifact.MD5
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const modIdentity = mdl.rawModule || mdl.rawModule.artifact.MD5
const modIdentity = mdl.rawModule

MD5 can be blank, so I don't think it's a good idea to base the mod identity on that value. Also there will always be a rawModule property afaik

Copy link
Author

Choose a reason for hiding this comment

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

I need to keep the .md5 file because if I don't include it, I won't be able to perform the verification. The MD5 is based on the file size, and during the creation of the distribution, it generates the hash. The value cannot be null.

package.json Outdated
},
"engines": {
"node": "20.x.x"
},
"dependencies": {
"@electron/remote": "^2.1.2",
"adm-zip": "^0.5.16",
"crypto": "^1.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"crypto": "^1.0.1",

Crypto module is already included in nodejs

Copy link
Author

Choose a reason for hiding this comment

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

Are you sure? I had to add it because it was telling me that it couldn't find the crypto dependency.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Yep, I just tried it. My mistake, it works. But by the way, when I remove the identity, I can't retrieve the MD5 hash; it returns [Object object]. I put the identity back, and it works.

Copy link
Author

Choose a reason for hiding this comment

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

image

without identity
image

with identity
image

Copy link
Author

Choose a reason for hiding this comment

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

and when i remove || mdl.rawModule.artifact.MD5
image

it returns undefined.

Copy link
Contributor

Choose a reason for hiding this comment

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

Weird, but fine

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I know, but so far I haven’t noticed any bugs related to that.

@Sandro642
Copy link
Author

Let me know if there's anything else to change or not ;)


Copy link
Contributor

Choose a reason for hiding this comment

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

Is that a workaround for your editor?

Copy link
Author

Choose a reason for hiding this comment

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

What do you mean by that?

Copy link
Contributor

Choose a reason for hiding this comment

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

The empty line before the closing bracket

Copy link
Author

Choose a reason for hiding this comment

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

I will remove the space

Copy link
Contributor

@GeekCornerGH GeekCornerGH left a comment

Choose a reason for hiding this comment

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

LGTM overall, Dan will probably request more changes when he'll have enough time to review this PR

@Sandro642
Copy link
Author

Thank you, GeekCorner! I wish you a good day. I just hope that Daniel will see my PR.

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.

3 participants