mealie-recipes / mealie

Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and mealie will automatically import the relevant data or add a family recipe with the UI editor
https://docs.mealie.io
GNU Affero General Public License v3.0
7.42k stars 743 forks source link

Webhooks not working #461

Closed dennyreiter closed 3 years ago

dennyreiter commented 3 years ago

Describe the bug When testing a webhook to Home Assistant, the automation fails as it says that trigger.json is a string and not an object. When testing a webhook to a simple Node-Red HTTP-In, nothing is received at all

Steps To Reproduce

  1. Go to Settings, Meal Planner Webhooks
  2. Create webhook such as: http://192.168.0.32:8123/api/webhook/mealie-meals
  3. Click on 'TEST WEBHOOKS'

Sample Code Home Assistant Automation

- id: '1622593710519'
  alias: Mealie Meals
  description: ''
  trigger:
  - platform: webhook
    webhook_id: mealie-meals
  condition: []
  action:
  - service: input_text.set_value
    data:
      value: "{{ trigger.json.name }}"
    target:
      entity_id: input_text.dinner_tonight
  mode: single

Expected behavior The entity input_text.dinner_tonight will have it's value set to the 'name' from the json sent by Mealie

Actual Behavior Error is logged: WARNING (MainThread) [homeassistant.helpers.template] Template variable warning: 'str object' has no attribute 'name' when rendering '{{ trigger.json.name }}'

Additional context If I use Curl to send a POST to either Home Assistant or Node-Red, such as below, the Home Assistant automation succeeds, and Node-Red will log the json as my msg.payload. If I do not set the Content-Type, then Home Assistant logs the same error as above.

curl -d '{"name": "Leftover Mashed Potato Soup", "slug": "leftover-mashed-potato-soup", "image": "no image", "description": "Great way to use up leftover mashed potatoes. Perfect for a light lunch or serving with sandwiches. Enjoy!", "recipeCategory": [], "tags": [], "rating": null, "recipeYield": "3-4 serving(s)", "recipeIngredient": ["2 tablespoons butter or 2 tablespoons bacon drippings", "1/2 cup chopped onion", "1 cup chopped fresh mushrooms", "14 1/2 ounces chicken broth", "1/2 teaspoon kosher salt", "1/4 teaspoon black pepper", "1/2 teaspoon sweet paprika", "2 cups prepared mashed potatoes (leftover is great)", "1/2 cup cheddar cheese", "2 scallions, finely chopped ", "3 slices bacon, cooked and crumbled ", "2 tablespoons sour cream", "2 tablespoons cream or 2 tablespoons half-and-half"], "recipeInstructions": [{"text": "Cook bacon, and set aside to crumble."}, {"text": "In a large saucepan, cook onion in butter or dripping until softened."}, {"text": "Add chopped mushrooms and cook until tender and onion is golden."}, {"text": "Add chicken broth, salt, pepper, and paprika, stirring to mix."}, {"text": "Blend in mashed potatoes, stirring until lumps are gone."}, {"text": "Bring soup to a boil, then lower heat and add cheddar cheese, stirring until it is melted and smooth."}, {"text": "Mix in scallions, crumbled bacon, sour cream, and cream/half and half, stirring to heated through but not boiling."}], "nutrition": {"calories": null, "fatContent": null, "fiberContent": null, "proteinContent": null, "sodiumContent": null, "sugarContent": null}, "tools": [], "totalTime": null, "prepTime": null, "performTime": null, "dateAdded": "2021-05-31", "notes": [], "orgURL": "https://www.food.com/recipe/leftover-mashed-potato-soup-102745", "extras": {}}' -H 'Content-Type: application/json' -X POST http://192.168.0.32:8123/api/webhook/mealie-meals

I attempted to modify the mealie/util/post_webhooks.py file to:

    headers = {'Content-Type': 'application/json'}
    for url in group_settings.webhook_urls:
        requests.post(url, headers=headers, json=todays_recipe.json())

    session.close()

but to no avail.

hay-kot commented 3 years ago

If you don't have any meals scheduled for the day it won't send anything. Do you have a meal scheduled for today?

dennyreiter commented 3 years ago

I do see now in the requests documentation that the Content-Type is set automatically: Using the json parameter in the request will change the Content-Type in the header to application/json.

dennyreiter commented 3 years ago

If you don't have any meals scheduled for the day it won't send anything. Do you have a meal scheduled for today?

I do have one scheduled for today, yes.

dennyreiter commented 3 years ago

Here is the body of what Mealie is sending:

{"name": "Julia Child's Quiche Lorraine | The Foodies' Kitchen", "slug": "julia-child-s-quiche-lorraine-the-foodies-kitchen", "image": "no image", "description": "Our second Julia Child recipe, this Quiche Lorraine is the perfect dish to serve for brunch as you can prepare it ahead and with different variations!", "recipeCategory": [], "tags": [], "rating": null, "recipeYield": "4-6", "recipeIngredient": ["3 to 4 ounces of bacon", "1 quart (4 cups) of water", "One 8-inch partially cooked pastry shell", "3 eggs", "1 \u00bd cups whipping cream", "\u00bd teaspoon of salt", "Pinch of nutmeg", "Pinch of pepper", "1 to 2 tablespoons of butter cut into pea-sized dots"], "recipeInstructions": [{"text": "Partially cooked pastry shell:"}, {"text": "You can use half of our recipe for Pate Bris\u00e9e, and bake it on a Quiche or pie mold. Make sure you fill it with pie weights or dried beans so it won\u2019t puff up when baking. As this is a partially baked pastry shell, you can bake it from 7 to 10 minutes in a 375\u00baF degree oven, or until the shell is very lightly browned."}, {"text": "Quiche Preparation:"}, {"text": "Preheat the oven at 375\u00baF degrees."}, {"text": "Cut bacon into pieces about an inch long and \u00bc inch wide. Simmer for 5 minutes in the water. Rinse them in cold water. Dry on paper towels. Brown lightly in a skillet. Press bacon into bottom of pastry shell."}, {"text": "Beat the eggs, cream and seasoning in a mixing bowl until blended. Check seasonings. Pour into pasty shell and distribute the butter pieces on top."}, {"text": "Set in upper third of preheated oven and bake for 25 to 30 minutes, or until the Quiche has puffed and browned. Slide Quiche onto a hot platter and serve."}], "nutrition": {"calories": null, "fatContent": null, "fiberContent": null, "proteinContent": null, "sodiumContent": null, "sugarContent": null}, "tools": [], "totalTime": null, "prepTime": null, "performTime": "45", "dateAdded": "2021-05-31", "notes": [], "orgURL": "https://www.thefoodieskitchen.com/2013/10/20/julia-childs-quiche-lorraine/", "extras": {}}

and the headers:

{"host":"enyuym2px8lt2m4.m.pipedream.net","x-amzn-trace-id":"Root=1-60b84c65-05e3d89b0b93d9784f723205","content-length":"2207","user-agent":"python-requests/2.25.1","accept-encoding":"gzip, deflate","accept":"*/*","content-type":"application/json"}
hay-kot commented 3 years ago

Here is the body of what Mealie is sending:

Ah, I thought you were saying the Webhooks were broken or not sending.

The json you're getting is a valid json string. You can parse it in Node-Red with the json node as I'm doing it on my install and I've verified that it is still working. It would seem that something on the Home Assistant side doesn't like the headers. They all appear valid to me, maybe someone else can chime in.

dennyreiter commented 3 years ago

See my Node Red flow acts like it receives nothing. This is what I'm using:

[
    {
        "id": "52518dab.8f39e4",
        "type": "debug",
        "z": "bd79cf9e.d7557",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 430,
        "y": 4560,
        "wires": []
    },
    {
        "id": "75e3ae00.659d24",
        "type": "http in",
        "z": "bd79cf9e.d7557",
        "name": "Mealie",
        "url": "/api/webhook/mealie-meals",
        "method": "post",
        "upload": false,
        "swaggerDoc": "",
        "x": 260,
        "y": 4600,
        "wires": [
            [
                "52518dab.8f39e4",
                "d7c86937.4ae598"
            ]
        ]
    },
    {
        "id": "d7c86937.4ae598",
        "type": "http response",
        "z": "bd79cf9e.d7557",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 440,
        "y": 4640,
        "wires": []
    }
]

When I use Curl to POST, it works and the payload is parseable JSON. Could you show me what you use?

I did find something interesting but I can't figure out the difference. Google sent me to this site: https://requestbin.com/ where I could create a listener. When I send my POST using CURL, the body shows the JSON all parsed out. But when I send it from Mealie, it shows it just as a string that looks like JSON.

dennyreiter commented 3 years ago

So I was able to make it work, in a roundabout way. I think it is something about how Pydantic serializes. I installed jsons and used jsons.dump to serialize it:

import requests
import jsons
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.user import GroupInDB
from mealie.services.meal_services import get_todays_meal
from sqlalchemy.orm.session import Session

def post_webhooks(group: int, session: Session = None):
    session = session or create_session()
    group_settings: GroupInDB = db.groups.get(session, group)

    if not group_settings.webhook_enable:
        return

    this_recipe = get_todays_meal(session, group)

    if not this_recipe:
        return

    todays_recipe = jsons.dump(this_recipe.dict(), strip_nulls=True)

    for url in group_settings.webhook_urls:
        requests.post(url, json=todays_recipe)

    session.close()

Now it shows up in Node Red and the automation succeeds in Home Assistant.

dennyreiter commented 3 years ago

OK, I found a cleaner way of doing it without installing another package:

import requests
import json
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.user import GroupInDB
from mealie.services.meal_services import get_todays_meal
from sqlalchemy.orm.session import Session

def post_webhooks(group: int, session: Session = None):
    session = session or create_session()
    group_settings: GroupInDB = db.groups.get(session, group)

    if not group_settings.webhook_enable:
        return

    todays_recipe = get_todays_meal(session, group)

    if not todays_recipe:
        return

    for url in group_settings.webhook_urls:
        requests.post(url, json=json.loads(todays_recipe.json()))

    session.close()

Basically wrap the todays_recipe.json() in a json.loads call, thanks to this comment: https://github.com/samuelcolvin/pydantic/issues/1409#issuecomment-642824336