Feature: Add re-capture to subscribe and contact form

As an Elliot For Water  User:
When I click the Subscribe Bbutton or Contact Form Send button
Then I should be presented with the "I am not a robot" re-captcha.
https://www.google.com/recaptcha/about/
It maybe also possible to re-implement the Aniti Forgery Token using React and WebApi based on this example;
https://stackoverflow.com/questions/11476883/web-api-and-validateantiforgerytoken
Either approach would be sufficient.
Give priorities to the approach that don't automatically fill in the re-captcha and avoid the user to interact. If possible.

@ElliotForWater/elliotforwater-team Hi, I've been reading over the project documentation and got the development server running. I would love to help contribute! Can I be assigned to this issue?

@karlrez hi and thank you for your interest in contributing to Elliot For Water. I have assigned the issue to you, feel free to work on it and let me know if you have any further questions or feedbacks.

@gsambrotta Hi, I just want to give an update on this and have a question about the UI.
I chose to use reCapcha v2, as it requires the user to interact more and it also generates a token which we can send to the backend: https://developers.google.com/recaptcha/intro
And I am implementing this with this react package: https://www.npmjs.com/package/react-google-recaptcha
Are you able to elaborate more on how this should look on the frontend?
Below shows the capcha added to both subscribe forms:

Would you rather the capcha to toggle visibility and only show when a user tries to submit their email? Do you have any suggestions on size location of the recapcha? Let me know your thoughts.

Hi @karlrez! Happy to read about your progress. It looks good already :D
I was wondering, why did you choose reCaptchav2 Robot instead invisible one?
Seem to me that the two have the same level of security but with the invisible one the user doesn't have to do any extra action and we have more chance he/she is gonna submit the form.
Also we can hide the element so doesn't visually disturb the user
What do you think?

I agree! Apologies, I guess I had misread the feature request and thought we wanted the one with more user interaction.
The invisible one is totally doable and shouldn't change the existing UI at all.


Query parameters in POST request are converted to formdata

It looks like it's not possible to have query parameters on POST requests, they are converted to formdata here: https://github.com/mtennoe/swagger-typescript-codegen/blob/master/templates/method.mustache#L179
Is there a specific reason this conversion is done? In our usecase, we generate a swagger spec from proto/grpc definitions using grpc-gateway, where query parameters are used to map fields in a Create request that aren't part of the actual resource to create: https://cloud.google.com/apis/design/standard_methods#create. When these fields are added as formdata in the request they are ignored when handled by the grpc-gateway.
It would be nice if there's a way to disable this conversion to formdata

Hey! The reason is that POST requests normally don't have query parameters, as all parameters are typically sent as part of the payload of the request (as all POST requests should have a payload containing this kind of stuff). To be clear though, AFAIK there is nothing in the spec saying that POST requests should not be allowed to have query parameters. Therefore, if this is handled explicitly in this generator I would be happy to take pull requests for this. If so, some questions:

Should it just be an optional bool flag for toggling this?
How do we handle a combination of form data and a query string?

Yeah, having query parameters on POST requests seems to be the standard for grpc-gateway and https://cloud.google.com/apis/design/standard_methods#create

Should it just be an optional bool flag for toggling this?

I think that makes the most sense, to prevent any breaking changes

How do we handle a combination of form data and a query string?

Looking at https://github.com/mtennoe/swagger-typescript-codegen/blob/master/templates/method.mustache#L179 I was thinking we could remove
form = queryParameters;
queryParameters = {};

and pass both form and query parameters to the request?
Happy to open a PR if the above makes sense

and pass both form and query parameters to the request?

Hm, I would prefer it if we could separate what params should go into the formdata and what should go into the query parameters. That however would lead to a solution that is overly complex or verbose, so I guess if we name the bool property correctly that would be good enough. Something like duplicatePostFormDataInQueryParameters?

Agree, what I meant was given the following example swagger spec for a POST request:
"post": {
  ...
  "parameters": [
    {
      "type": "boolean",
      "format": "boolean",
      "name": "some_query_parameter",
      "in": "query"
    },
    {
      "type": "boolean",
      "format": "boolean",
      "name": "some_form_parameter",
      "in": "formData"
    }
  ]
}

the current behaviour is that the some_query_parameter is sent as form data, and some_form_parameter is not sent at all as form data gets overwritten by the query parameters. I think the solution would be to add a flag, and when enabled, will send some_query_parameter as a query parameter and some_form_parameter as form data in the same request.
From having a look at the mustache template I think this should only be a case of removing
    {{^isBodyParameter}}
        {{#isPOST}}
            form = queryParameters;
            queryParameters = {};
        {{/isPOST}}
    {{/isBodyParameter}}

when the flag is enabled
What do you think?

Ah yes, thanks for clarifying! Agree, that sounds like a good approach. It also ensures backwards compatibility for people relying on the formdata behavior

Hey any updates on this?

Sorry for the slow response, no updates here sadly @Emyrk. Did you want to give your proposal a shot @WVerlaek?

Great, taking a look!

Approved, merged, and published as version 3.2.4. Thanks @WVerlaek!


Cream-installer release

a script that fully automates the installation of creamlinux and dlc id fetching. I plan in the future to add funtionality to place dlc files in the directory that is specified but as of now the script is fully working. below is the source of the script along with the github link where the script is hosted.
Example:

https://github.com/Novattz/creamlinux-installer
import os
import re
import requests
import zipfile
import time
import stat

def parse_vdf(file_path):
    library_paths = []
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
            paths = re.findall(r'"path"\s*"(.*?)"', content, re.IGNORECASE)
            library_paths.extend([os.path.normpath(path) for path in paths])
    except Exception as e:
        print(f"Failed to read {file_path}: {str(e)}")
    return library_paths

def find_steam_library_folders():
    base_paths = [os.path.expanduser('~/.steam/steam'), os.path.expanduser('~/.local/share/Steam'), '/mnt', '/media']
    library_folders = []
    for base_path in base_paths:
        if os.path.exists(base_path):
            for root, dirs, files in os.walk(base_path, topdown=True):
                if 'steamapps' in dirs:
                    steamapps_path = os.path.join(root, 'steamapps')
                    library_folders.append(steamapps_path)
                    vdf_path = os.path.join(steamapps_path, 'libraryfolders.vdf')
                    if os.path.exists(vdf_path):
                        additional_paths = parse_vdf(vdf_path)
                        for path in additional_paths:
                            new_steamapps_path = os.path.join(path, 'steamapps')
                            if os.path.exists(new_steamapps_path):
                                library_folders.append(new_steamapps_path)
                    dirs[:] = []  # Prevent further scanning into subdirectories
    return library_folders

def find_steam_apps(library_folders):
    acf_pattern = re.compile(r'^appmanifest_(\d+)\.acf$')
    games = {}
    for folder in library_folders:
        if os.path.exists(folder):
            for item in os.listdir(folder):
                if acf_pattern.match(item):
                    app_id, game_name, install_dir = parse_acf(os.path.join(folder, item))
                    if app_id and game_name:
                        install_path = os.path.join(folder, 'common', install_dir)
                        if os.path.exists(install_path):
                            cream_installed = 'Cream installed' if 'cream.sh' in os.listdir(install_path) else ''
                            games[app_id] = (game_name, cream_installed, install_path)
    return games

def parse_acf(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        data = file.read()
    app_id = re.search(r'"appid"\s+"(\d+)"', data)
    name = re.search(r'"name"\s+"([^"]+)"', data)
    install_dir = re.search(r'"installdir"\s+"([^"]+)"', data)
    return app_id.group(1), name.group(1), install_dir.group(1)

def fetch_dlc_details(app_id):
    base_url = f"https://store.steampowered.com/api/appdetails?appids={app_id}"
    response = requests.get(base_url)
    data = response.json()
    if app_id not in data or "data" not in data[app_id]:
        print("Error: Unable to fetch game details.")
        return []
    game_data = data[app_id]["data"]
    dlcs = game_data.get("dlc", [])
    dlc_details = []
    for dlc_id in dlcs:
        try:
            time.sleep(0.3)
            dlc_url = f"https://store.steampowered.com/api/appdetails?appids={dlc_id}"
            dlc_response = requests.get(dlc_url)
            if dlc_response.status_code == 200:
                dlc_data = dlc_response.json()
                if str(dlc_id) in dlc_data and "data" in dlc_data[str(dlc_id)]:
                    dlc_name = dlc_data[str(dlc_id)]["data"].get("name", "Unknown DLC")
                    dlc_details.append({"appid": dlc_id, "name": dlc_name})
                else:
                    print(f"Data missing for DLC {dlc_id}")
            elif dlc_response.status_code == 429:
                print("Rate limited! Please wait before trying again.")
                time.sleep(10)
            else:
                print(f"Failed to fetch details for DLC {dlc_id}, Status Code: {dlc_response.status_code}")
        except Exception as e:
            print(f"Exception for DLC {dlc_id}: {str(e)}")
    return dlc_details

def install_files(app_id, game_install_dir, dlcs, game_name):
    zip_url = "https://github.com/anticitizn/creamlinux/releases/latest/download/creamlinux.zip"
    zip_path = os.path.join(game_install_dir, 'creamlinux.zip')
    response = requests.get(zip_url)
    if response.status_code == 200:
        with open(zip_path, 'wb') as f:
            f.write(response.content)
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(game_install_dir)
        os.remove(zip_path)

        cream_sh_path = os.path.join(game_install_dir, 'cream.sh')
        os.chmod(cream_sh_path, os.stat(cream_sh_path).st_mode | stat.S_IEXEC)

        dlc_list = "\n".join([f"{dlc['appid']} = {dlc['name']}" for dlc in dlcs])
        cream_api_path = os.path.join(game_install_dir, 'cream_api.ini')
        with open(cream_api_path, 'w') as f:
            f.write(f"APPID = {app_id}\n[config]\nissubscribedapp_on_false_use_real = true\n[methods]\ndisable_steamapps_issubscribedapp = false\n[dlc]\n{dlc_list}")
        print(f"Custom cream_api.ini has been written to {game_install_dir}.")
        print(f"Installation complete. Set launch options in Steam: 'sh ./cream.sh %command%' for {game_name}.")
    else:
        print("Failed to download the files needed for installation.")

def main():
    library_folders = find_steam_library_folders()
    games = find_steam_apps(library_folders)
    if games:
        print("Select the game for which you want to fetch DLCs:")
        games_list = list(games.items())
        GREEN = '\033[92m'
        RESET = '\033[0m'
        for idx, (app_id, (name, cream_status, _)) in enumerate(games_list, 1):
            if cream_status:
                print(f"{idx}. {GREEN}{name} (App ID: {app_id}) - Cream installed{RESET}")
            else:
                print(f"{idx}. {name} (App ID: {app_id})")

        choice = int(input("Enter the number of the game: ")) - 1
        selected_app_id, (selected_game_name, _, selected_install_dir) = games_list[choice]
        print(f"You selected: {selected_game_name} (App ID: {selected_app_id})")

        dlcs = fetch_dlc_details(selected_app_id)
        print("DLC IDs found:", [dlc['appid'] for dlc in dlcs])  # Only print app IDs for clarity
        install_files(selected_app_id, selected_install_dir, dlcs, selected_game_name)
    else:
        print("No Steam games found on this computer or connected drives.")

if __name__ == "__main__":
    main()

Wow, great work! Sorry for responding so late. This is very convenient, I will update the build instructions in the readme to point people to your script as the primary method of installation. Will keep the manual way of doing it there as well just in case.

Closing as the script was linked in the readme. Thank you again.


docs(error/infdig): Boolean directives taking toggle values by mistake can generate this error

One common mistake is passing a toggle value to a directive which takes boolian.

boolian --> boolean
Besides that, I don't think it is that common an error...
I am not sure this notice helps more than it confuses (e.g. what are directives that take a boolean).
BTW, the example works fine. Did you mean showMyDialog=!showMyDialog ?

I agree with @gkalpak, this is not that common. Stackoverflow questions with lots of upvotes or similar might make me change my mind, though :)