Skip to content

Commit

Permalink
Migrate Mailgun to use the webhook component (#17464)
Browse files Browse the repository at this point in the history
* Switch mailgun to use webhook api

* Generalize webhook_config_entry_flow

* Add tests for webhook_config_entry_flow

* Add tests for mailgun

* Remove old mailgun file from .coveragerc

* Refactor WebhookFlowHandler into config_entry_flow

* Remove test of helper func from IFTTT

* Lint
  • Loading branch information
rohankapoorcom authored and balloob committed Oct 23, 2018
1 parent 277a9a3 commit d5a5695
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 122 deletions.
1 change: 0 additions & 1 deletion .coveragerc
Expand Up @@ -209,7 +209,6 @@ omit =
homeassistant/components/lutron_caseta.py
homeassistant/components/*/lutron_caseta.py

homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py

homeassistant/components/matrix.py
Expand Down
53 changes: 9 additions & 44 deletions homeassistant/components/ifttt/__init__.py
Expand Up @@ -4,18 +4,15 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ifttt/
"""
from ipaddress import ip_address
import json
import logging
from urllib.parse import urlparse

import requests
import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.util.network import is_local
from homeassistant.helpers import config_entry_flow

REQUIREMENTS = ['pyfttt==0.3']
DEPENDENCIES = ['webhook']
Expand Down Expand Up @@ -100,43 +97,11 @@ async def async_unload_entry(hass, entry):
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True


@config_entries.HANDLERS.register(DOMAIN)
class ConfigFlow(config_entries.ConfigFlow):
"""Handle an IFTTT config flow."""

async def async_step_user(self, user_input=None):
"""Handle a user initiated set up flow."""
if self._async_current_entries():
return self.async_abort(reason='one_instance_allowed')

try:
url_parts = urlparse(self.hass.config.api.base_url)

if is_local(ip_address(url_parts.hostname)):
return self.async_abort(reason='not_internet_accessible')
except ValueError:
# If it's not an IP address, it's very likely publicly accessible
pass

if user_input is None:
return self.async_show_form(
step_id='user',
)

webhook_id = self.hass.components.webhook.async_generate_id()
webhook_url = \
self.hass.components.webhook.async_generate_url(webhook_id)

return self.async_create_entry(
title='IFTTT Webhook',
data={
CONF_WEBHOOK_ID: webhook_id
},
description_placeholders={
'applet_url': 'https://ifttt.com/maker_webhooks',
'webhook_url': webhook_url,
'docs_url':
'https://www.home-assistant.io/components/ifttt/'
}
)
config_entry_flow.register_webhook_flow(
DOMAIN,
'IFTTT Webhook',
{
'applet_url': 'https://ifttt.com/maker_webhooks',
'docs_url': 'https://www.home-assistant.io/components/ifttt/'
}
)
50 changes: 0 additions & 50 deletions homeassistant/components/mailgun.py

This file was deleted.

18 changes: 18 additions & 0 deletions homeassistant/components/mailgun/.translations/en.json
@@ -0,0 +1,18 @@
{
"config": {
"title": "Mailgun",
"step": {
"user": {
"title": "Set up the Mailgun Webhook",
"description": "Are you sure you want to set up Mailgun?"
}
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
}
}
}
67 changes: 67 additions & 0 deletions homeassistant/components/mailgun/__init__.py
@@ -0,0 +1,67 @@
"""
Support for Mailgun.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mailgun/
"""

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID
from homeassistant.helpers import config_entry_flow

DOMAIN = 'mailgun'
API_PATH = '/api/{}'.format(DOMAIN)
DEPENDENCIES = ['webhook']
MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN)
CONF_SANDBOX = 'sandbox'
DEFAULT_SANDBOX = False

CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_DOMAIN): cv.string,
vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean,
vol.Optional(CONF_WEBHOOK_ID): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Set up the Mailgun component."""
if DOMAIN not in config:
return True

hass.data[DOMAIN] = config[DOMAIN]
return True


async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook with Mailgun inbound messages."""
data = dict(await request.post())
data['webhook_id'] = webhook_id
hass.bus.async_fire(MESSAGE_RECEIVED, data)


async def async_setup_entry(hass, entry):
"""Configure based on config entry."""
hass.components.webhook.async_register(
entry.data[CONF_WEBHOOK_ID], handle_webhook)
return True


async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True

config_entry_flow.register_webhook_flow(
DOMAIN,
'Mailgun Webhook',
{
'mailgun_url':
'https://www.mailgun.com/blog/a-guide-to-using-mailguns-webhooks',
'docs_url': 'https://www.home-assistant.io/components/mailgun/'
}
)
18 changes: 18 additions & 0 deletions homeassistant/components/mailgun/strings.json
@@ -0,0 +1,18 @@
{
"config": {
"title": "Mailgun",
"step": {
"user": {
"title": "Set up the Mailgun Webhook",
"description": "Are you sure you want to set up Mailgun?"
}
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
}
}
}
5 changes: 3 additions & 2 deletions homeassistant/components/notify/mailgun.py
Expand Up @@ -8,7 +8,8 @@

import voluptuous as vol

from homeassistant.components.mailgun import CONF_SANDBOX, DATA_MAILGUN
from homeassistant.components.mailgun import (
CONF_SANDBOX, DOMAIN as MAILGUN_DOMAIN)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService, ATTR_TITLE, ATTR_TITLE_DEFAULT,
ATTR_DATA)
Expand All @@ -35,7 +36,7 @@

def get_service(hass, config, discovery_info=None):
"""Get the Mailgun notification service."""
data = hass.data[DATA_MAILGUN]
data = hass.data[MAILGUN_DOMAIN]
mailgun_service = MailgunNotificationService(
data.get(CONF_DOMAIN), data.get(CONF_SANDBOX),
data.get(CONF_API_KEY), config.get(CONF_SENDER),
Expand Down
1 change: 1 addition & 0 deletions homeassistant/config_entries.py
Expand Up @@ -143,6 +143,7 @@ async def async_step_discovery(info):
'ifttt',
'ios',
'lifx',
'mailgun',
'mqtt',
'nest',
'openuv',
Expand Down
58 changes: 58 additions & 0 deletions homeassistant/helpers/config_entry_flow.py
@@ -1,7 +1,10 @@
"""Helpers for data entry flows for config entries."""
from functools import partial
from ipaddress import ip_address
from urllib.parse import urlparse

from homeassistant import config_entries
from homeassistant.util.network import is_local


def register_discovery_flow(domain, title, discovery_function,
Expand All @@ -12,6 +15,14 @@ def register_discovery_flow(domain, title, discovery_function,
connection_class))


def register_webhook_flow(domain, title, description_placeholder,
allow_multiple=False):
"""Register flow for webhook integrations."""
config_entries.HANDLERS.register(domain)(
partial(WebhookFlowHandler, domain, title, description_placeholder,
allow_multiple))


class DiscoveryFlowHandler(config_entries.ConfigFlow):
"""Handle a discovery config flow."""

Expand Down Expand Up @@ -84,3 +95,50 @@ async def async_step_import(self, _):
title=self._title,
data={},
)


class WebhookFlowHandler(config_entries.ConfigFlow):
"""Handle a webhook config flow."""

VERSION = 1

def __init__(self, domain, title, description_placeholder,
allow_multiple):
"""Initialize the discovery config flow."""
self._domain = domain
self._title = title
self._description_placeholder = description_placeholder
self._allow_multiple = allow_multiple

async def async_step_user(self, user_input=None):
"""Handle a user initiated set up flow to create a webhook."""
if not self._allow_multiple and self._async_current_entries():
return self.async_abort(reason='one_instance_allowed')

try:
url_parts = urlparse(self.hass.config.api.base_url)

if is_local(ip_address(url_parts.hostname)):
return self.async_abort(reason='not_internet_accessible')
except ValueError:
# If it's not an IP address, it's very likely publicly accessible
pass

if user_input is None:
return self.async_show_form(
step_id='user',
)

webhook_id = self.hass.components.webhook.async_generate_id()
webhook_url = \
self.hass.components.webhook.async_generate_url(webhook_id)

self._description_placeholder['webhook_url'] = webhook_url

return self.async_create_entry(
title=self._title,
data={
'webhook_id': webhook_id
},
description_placeholders=self._description_placeholder
)
12 changes: 1 addition & 11 deletions tests/components/ifttt/test_init.py
@@ -1,5 +1,5 @@
"""Test the init file of IFTTT."""
from unittest.mock import Mock, patch
from unittest.mock import patch

from homeassistant import data_entry_flow
from homeassistant.core import callback
Expand Down Expand Up @@ -36,13 +36,3 @@ def handle_event(event):
assert len(ifttt_events) == 1
assert ifttt_events[0].data['webhook_id'] == webhook_id
assert ifttt_events[0].data['hello'] == 'ifttt'


async def test_config_flow_aborts_external_url(hass, aiohttp_client):
"""Test setting up IFTTT and sending webhook."""
hass.config.api = Mock(base_url='http://192.168.1.10')
result = await hass.config_entries.flow.async_init('ifttt', context={
'source': 'user'
})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'not_internet_accessible'
1 change: 1 addition & 0 deletions tests/components/mailgun/__init__.py
@@ -0,0 +1 @@
"""Tests for the Mailgun component."""

0 comments on commit d5a5695

Please sign in to comment.