adnanh / webhook

webhook is a lightweight incoming webhook server to run shell commands
MIT License
10.48k stars 836 forks source link

Could someone familiar with hook rules, help me figure out how to parse this body? #213

Closed psmith3 closed 6 years ago

psmith3 commented 6 years ago

I am trying to figure out how to parse the body of a webhook message sent from node-sonos-http-api and have a match to an ID in my hooks.json config that matches when my Kitchen Sonos player ("roomName":"Kitchen",) is PLAYING and another when it is STOPPED ("playbackState":"PLAYING"). Below is the raw body of the webhook message that I am needing to create two rules for under one ID in my hooks.json file because the node-sonos-http-api only can send to a single webhook server url. Ultimately, I need to have the execute command be variable and send a different url to IFTTT like https://maker.ifttt.com/trigger/Kitchen_Music_Off and another like https://maker.ifttt.com/trigger/Kitchen_Music_On. I am not sure how to pass variables from the hooks.json file to the scripts that executes a CURL to IFTTT.

RAW BODY {"type":"transport-state","data":{"uuid":"RINCON_000E583A2E2401400","coordinator":"RINCON_000E583A2E2401400","roomName":"Kitchen","state":{"volume":22,"mute":false,"equalizer":{"bass":9,"treble":10,"loudness":true},"currentTrack":{"artist":"Taylor Swift","title":"...Ready For It?","album":"Reputation","albumArtUri":"/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429750%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2","duration":208,"uri":"x-sonos-http:ondemand_track%3a%3atra.290429750%7cv1%7cALBUM%7calb.290429749.mp4?sid=202&flags=8224&sn=2","type":"track","stationName":"","absoluteAlbumArtUri":"http://172.30.17.220:1400/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429750%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2"},"nextTrack":{"artist":"Taylor Swift","title":"End Game","album":"Reputation","albumArtUri":"/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429751%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2","duration":245,"uri":"x-sonos-http:ondemand_track%3a%3atra.290429751%7cv1%7cALBUM%7calb.290429749.mp4?sid=202&flags=8224&sn=2","absoluteAlbumArtUri":"http://172.30.17.220:1400/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429751%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2"},"trackNo":1,"elapsedTime":26,"elapsedTimeFormatted":"00:00:26","playbackState":"PLAYING","playMode":{"repeat":"none","shuffle":false,"crossfade":true}},"groupState":{"volume":22,"mute":false},"avTransportUri":"x-rincon-queue:RINCON_000E583A2E2401400#0","avTransportUriMetadata":""}} {"type":"transport-state","data":{"uuid":"RINCON_000E583A2E2401400","coordinator":"RINCON_000E583A2E2401400","roomName":"Kitchen","state":{"volume":22,"mute":false,"equalizer":{"bass":9,"treble":10,"loudness":true},"currentTrack":{"artist":"Taylor Swift","title":"...Ready For It?","album":"Reputation","albumArtUri":"/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429750%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2","duration":208,"uri":"x-sonos-http:ondemand_track%3a%3atra.290429750%7cv1%7cALBUM%7calb.290429749.mp4?sid=202&flags=8224&sn=2","type":"track","stationName":"","absoluteAlbumArtUri":"http://172.30.17.220:1400/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429750%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2"},"nextTrack":{"artist":"Taylor Swift","title":"End Game","album":"Reputation","albumArtUri":"/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429751%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2","duration":245,"uri":"x-sonos-http:ondemand_track%3a%3atra.290429751%7cv1%7cALBUM%7calb.290429749.mp4?sid=202&flags=8224&sn=2","absoluteAlbumArtUri":"http://172.30.17.220:1400/getaa?s=1&u=x-sonos-http%3aondemand_track%253a%253atra.290429751%257cv1%257cALBUM%257calb.290429749.mp4%3fsid%3d202%26flags%3d8224%26sn%3d2"},"trackNo":1,"elapsedTime":26,"elapsedTimeFormatted":"00:00:26","playbackState":"PLAYING","playMode":{"repeat":"none","shuffle":false,"crossfade":true}},"groupState":{"volume":22,"mute":false},"avTransportUri":"x-rincon-queue:RINCON_000E583A2E2401400#0","avTransportUriMetadata":""}}

Just to get started, I have been tinkering with the following hooks.json file and cannot figure out how to get a simple match on the playingback state. Am I on the right track?

[
    {
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_on.sh",
        "response-message": "Ok ...",
        "trigger-rule": {
            "match": {
                "type": "value",
                "value": "PLAYING",
                "parameter": {
                    "source": "payload",
                    "name": "playbackState"
                }
            }

        }
    }
]

Any help would be much appreciated.

adnanh commented 6 years ago

Referencing values

You can read more about it here

The room name field is referenced like this:

{
    "source": "payload",
    "name": "data.roomName"
}

and the current playback state is referenced like this:

{
    "source": "payload",
    "name": "data.state.playbackState"
}

Trigger rule

You can read more about trigger rules here

Trigger rule controls when your script will be executed. You basically want to execute your script when room name field is equal to Kitchen and the playback state is either PLAYING or STOPPED.

Translated to the logic you want to define trigger rule: roomName == 'Kitchen' && (playbackState == 'PLAYING' || playbackState == 'STOPPED')

Which in webhook looks like this:

{
    "and": [
        {
            "match": {
                "type": "value",
                "value": "Kitchen",
                "parameter": {
                    "source": "payload",
                    "name": "data.roomName"
                }
            }
        },
        {
            "or": [
                {
                    "match": {
                        "type": "value",
                        "value": "PLAYING",
                        "parameter": {
                            "source": "payload",
                            "name": "data.state.playbackState"
                        }
                    }
                },
                {
                    "match": {
                        "type": "value",
                        "value": "STOPPED",
                        "parameter": {
                            "source": "payload",
                            "name": "data.state.playbackState"
                        }
                    }
                }
            ]
        }
    ]
}

Passing request context values to your script

There are two ways you can do this, via arguments or environment variables. You can read more about that here

Let's do it via environment variables: we want to pass the playbackState and roomName field values to the script as PLAYBACK_STATE and ROOM_NAME respectively. In order to do that, we define pass-environment-to-command property on the hook like so:

"pass-environment-to-command": [
    {
        "source": "payload",
        "envname": "ROOM_NAME",
        "name": "data.roomName"
    },
    {
        "source": "payload",
        "envname": "PLAYBACK_STATE",
        "name": "data.state.playbackState"
    }
]

Hook definition

Following all this, and starting with your example, the final hook definition looks like this:

[
    {
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_on.sh",
        "response-message": "Ok ...",
        "pass-environment-to-command": [
            {
                "source": "payload",
                "envname": "ROOM_NAME",
                "name": "data.roomName"
            },
            {
                "source": "payload",
                "envname": "PLAYBACK_STATE",
                "name": "data.state.playbackState"
            }
        ],
        "trigger-rule": {
            "and": [
                {
                    "match": {
                        "type": "value",
                        "value": "Kitchen",
                        "parameter": {
                            "source": "payload",
                            "name": "data.roomName"
                        }
                    }
                },
                {
                    "or": [
                        {
                            "match": {
                                "type": "value",
                                "value": "PLAYING",
                                "parameter": {
                                    "source": "payload",
                                    "name": "data.state.playbackState"
                                }
                            }
                        },
                        {
                            "match": {
                                "type": "value",
                                "value": "STOPPED",
                                "parameter": {
                                    "source": "payload",
                                    "name": "data.state.playbackState"
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
]

This hook will execute your script only when the room name is equal to Kitchen and playback state is either PLAYING or STOPPED. When your script gets executed, you will have ROOM_NAME and PLAYBACK_STATE environment variables available. Based on values of the PLAYBACK_STATE environment variable you can execute curl command with different parameters.

Hope this clears up some things :-)

psmith3 commented 6 years ago

@adnanh , thank you much for the detailed reply! This is great info and clarifies many of the questions that I came to. I did make much progress after opening the ticket and I closed it after I got it working, mostly. Your examples fill in the gaps and I will apply them shortly.

I did have a question about how I ended up handing multiple scenarios for the Playing vs Stopped. Rather than using pass-environment-to-command, I ended up creating 2 ids with the same name "sonos" and that seemed to work. However, I am wondering if that would cause an issue of some kind. I probably should rewrite to pass-environment-to-command like your last example. Here is the code that I had made up to the point that I closed the ticket. Again, thanks much for the detailed explanations.

[{
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_on.sh",
        "response-message": "Ok ...",
        "trigger-rule": {
            "and": [{
                    "match": {
                        "type": "value",
                        "value": "Kitchen",
                        "parameter": {
                            "source": "payload",
                            "name": "data.roomName"
                        }
                    }
                },
                {
                    "match": {
                        "type": "value",
                        "value": "PLAYING",
                        "parameter": {
                            "source": "payload",
                            "name": "data.state.playbackState"
                        }
                    }
                }
            ]
        }
    },
    {
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_off.sh",
        "response-message": "Ok ...",
        "trigger-rule": {
            "and": [{
                    "match": {
                        "type": "value",
                        "value": "Kitchen",
                        "parameter": {
                            "source": "payload",
                            "name": "data.roomName"
                        }
                    }
                },
                {
                    "match": {
                        "type": "value",
                        "value": "PAUSED_PLAYBACK",
                        "parameter": {
                            "source": "payload",
                            "name": "data.state.playbackState"
                        }
                    }
                }
            ]
        }
    }
]
adnanh commented 6 years ago

The multiple hooks with the same id has been deprecated and removed in latest versions of webhook. You are most likely running an older version of webhook, since it worked for you. I strongly suggest for you to upgrade to the latest version :-) (and rewrite the hook definitions to use one hook)

psmith3 commented 6 years ago

Will do. Thank you!

psmith3 commented 6 years ago

Digging further into converting this to send pass the environment to the CURL command, can you explain further how I pass the variables? Right now I have 2 different scripts on my server that get run and execute the CURL commands; kitchen_music_on.sh and kitchen_music_off.sh. I don't guess there is a way to run the CURL directly from Webhook server is there? Like I mentioned before, my scripts either issue a CURL with the ON name or another with the OFF. The script looks like this.

#!/bin/sh
curl "https://maker.ifttt.com/trigger/Music_On/with/key/secret"

and

#!/bin/sh
curl "https://maker.ifttt.com/trigger/Music_Off/with/key/secret"

Below is the hooks.json that I have working at the moment using the forked image of palmer/webhook:docker-1.10-git and like you say, it is not the latest.

[{
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_on.sh",
        "response-message": "Kitchen Music On ...",
        "trigger-rule": {
            "and": [{
                    "match": {
                        "type": "value",
                        "value": "Kitchen",
                        "parameter": {
                            "source": "payload",
                            "name": "data.roomName"
                        }
                    }
                },
                {
                    "match": {
                        "type": "value",
                        "value": "PLAYING",
                        "parameter": {
                            "source": "payload",
                            "name": "data.state.playbackState"
                        }
                    }
                }
            ]
        }
    },
    {
        "id": "sonos",
        "execute-command": "/etc/webhook/kitchen_music_off.sh",
        "response-message": "Kitchen Music Paused or Stopped ...",
        "trigger-rule": {
            "and": [{
                    "match": {
                        "type": "value",
                        "value": "Kitchen",
                        "parameter": {
                            "source": "payload",
                            "name": "data.roomName"
                        }
                    }
                },
                {
                    "or": [{
                            "match": {
                                "type": "value",
                                "value": "PAUSED_PLAYBACK",
                                "parameter": {
                                    "source": "payload",
                                    "name": "data.state.playbackState"
                                }
                            }
                        },
                        {
                            "match": {
                                "type": "value",
                                "value": "STOPPED",
                                "parameter": {
                                    "source": "payload",
                                    "name": "data.state.playbackState"
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
]
hassanbabaie commented 6 years ago

hi @psmith3 you need to to use the "pass-environment-to-command" to use as environment variables or ""pass-arguments-to-command". I'm sure you will get a more detialed response but below might help..

Below is an example of using as environment variables. you should see in the hook verbose logs that is grabs them. Then in your script use the normal environment variable e.g. $INTERNAL_IP to call it

"id": "email",
"execute-command": "/webhook/src/email.sh",
"command-working-directory": "/webhook/src",
"pass-environment-to-command":      [
                            {
                                    "envname": "INTERNAL_IP",
                                    "source": "payload",
                                    "name": "data.INTERNAL_IP"
hassanbabaie commented 6 years ago

Also in the Wiki you can see some examples but maybe not the one above

hassanbabaie commented 6 years ago

Just to expand on what I sent. Below is what my JSON payload looks like (what I'm sending over in the webhook call) hence how it looks within the webhook JSON (above) to extract the data. `

"data":{ "EXTERNAL_IP":"", "INTERNAL_IP":"172.18.4.38", `

psmith3 commented 6 years ago

Thanks. Making more sense. I am hoping someone can show me an example of passing the payload into a CURL script and inserting into the variables into the URL that is sent by the script.

hassanbabaie commented 6 years ago

So it's really just a scripting issue versus the webhook right? I think you can just use ${}

cURL "https://someurl/place/${ENV}/something"

However I'm a bit of a newbie but I'm sure after a few goes and using a print comment you can test how this translates

adnanh commented 6 years ago

Using the environment variables you can use Case statements to control the flow of your script, like so:

#!/bin/bash

case "$PLAYBACK_STATE" in
        PLAYING)
            curl playingurl
            ;;
        STOPPED)
            curl stoppedurl
            ;;
        *)
            echo "Invalid playback state $PLAYBACK_STATE"
            exit 1
esac
psmith3 commented 6 years ago

Thanks much for the reply and explanation. I will give this a try.

adnanh commented 6 years ago

P.S. You might want to check almir/docker-webhook for the up-to-date dockerized webhook :-)

Happy hacking!