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

Add config flow to discord #61069

Merged
merged 17 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ omit =
homeassistant/components/digital_ocean/*
homeassistant/components/digitalloggers/switch.py
homeassistant/components/discogs/sensor.py
homeassistant/components/discord/__init__.py
homeassistant/components/discord/notify.py
homeassistant/components/dlib_face_detect/image_processing.py
homeassistant/components/dlib_face_identify/image_processing.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ homeassistant/components/diagnostics/* @home-assistant/core
tests/components/diagnostics/* @home-assistant/core
homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/discogs/* @thibmaek
homeassistant/components/discord/* @tkdrob
tests/components/discord/* @tkdrob
homeassistant/components/discovery/* @home-assistant/core
tests/components/discovery/* @home-assistant/core
homeassistant/components/dlna_dmr/* @StevenLooman @chishm
Expand Down
62 changes: 62 additions & 0 deletions homeassistant/components/discord/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
"""The discord integration."""
from aiohttp.client_exceptions import ClientConnectorError
import nextcord

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PLATFORM, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import discovery
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN

PLATFORMS = [Platform.NOTIFY]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Discord component."""
# Iterate all entries for notify to only get Discord
if Platform.NOTIFY in config:
for entry in config[Platform.NOTIFY]:
if entry[CONF_PLATFORM] == DOMAIN:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
)
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Discord from a config entry."""
nextcord.VoiceClient.warn_nacl = False
discord_bot = nextcord.Client()
try:
await discord_bot.login(entry.data[CONF_TOKEN])
except nextcord.LoginFailure as ex:
raise ConfigEntryAuthFailed("Invalid token given") from ex
except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound) as ex:
raise ConfigEntryNotReady("Failed to connect") from ex
await discord_bot.close()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data

hass.async_create_task(
discovery.async_load_platform(
hass,
Platform.NOTIFY,
DOMAIN,
hass.data[DOMAIN][entry.entry_id],
hass.data[DOMAIN],
Copy link
Member

Choose a reason for hiding this comment

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

Same comments as for the slack config flow PR. See the Tibber integration for the correct way to call this function.

)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
104 changes: 104 additions & 0 deletions homeassistant/components/discord/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Config flow for Discord integration."""
from __future__ import annotations

import logging

from aiohttp.client_exceptions import ClientConnectorError
import nextcord
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult

from .const import DEFAULT_NAME, DOMAIN

_LOGGER = logging.getLogger(__name__)


class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Discord."""

async def async_step_reauth(self, config: dict[str, str]) -> FlowResult:
"""Handle a reauthorization flow request."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Confirm reauth dialog."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_TOKEN): str}),
errors={},
)

return await self.async_step_user(user_input)

async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user."""
errors = {}

if user_input is not None:
token = user_input[CONF_TOKEN]
name = user_input.get(CONF_NAME, DEFAULT_NAME)

error, unique_id = await _async_try_connect(token)
entry = await self.async_set_unique_id(unique_id)
if entry and self.source == config_entries.SOURCE_REAUTH:
self.hass.config_entries.async_update_entry(entry, data=user_input)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful")
self._abort_if_unique_id_configured()
if error is None:
return self.async_create_entry(
title=name,
data={CONF_TOKEN: token, CONF_NAME: name},
)
errors["base"] = error

user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN)): str,
vol.Required(
CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
): str,
}
),
errors=errors,
)

async def async_step_import(self, import_config: dict[str, str]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
_LOGGER.warning(
"Discord yaml config with partial key %s has been imported. Please remove it",
import_config[CONF_TOKEN][0:4],
)
for entry in self._async_current_entries():
if entry.data[CONF_TOKEN] == import_config[CONF_TOKEN]:
return self.async_abort(reason="already_configured")
import_config[CONF_TOKEN] = import_config.pop(CONF_TOKEN)
return await self.async_step_user(import_config)


async def _async_try_connect(token: str) -> tuple[str | None, str | None]:
"""Try connecting to Discord."""
discord_bot = nextcord.Client()
try:
await discord_bot.login(token)
info = await discord_bot.application_info()
except nextcord.LoginFailure:
return "invalid_auth", None
except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound):
return "cannot_connect", None
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return "unknown", None
await discord_bot.close()
return None, str(info.id)
6 changes: 6 additions & 0 deletions homeassistant/components/discord/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants for the Discord integration."""

from typing import Final

DEFAULT_NAME = "Discord"
DOMAIN: Final = "discord"
3 changes: 2 additions & 1 deletion homeassistant/components/discord/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"domain": "discord",
"name": "Discord",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/discord",
"requirements": ["nextcord==2.0.0a8"],
"codeowners": [],
"codeowners": ["@tkdrob"],
"iot_class": "cloud_push",
"loggers": ["discord"]
}
32 changes: 22 additions & 10 deletions homeassistant/components/discord/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

import logging
import os.path
from typing import Any, cast

import nextcord
from nextcord.abc import Messageable
import voluptuous as vol

from homeassistant.components.notify import (
Expand All @@ -14,7 +16,9 @@
BaseNotificationService,
)
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

_LOGGER = logging.getLogger(__name__)

Expand All @@ -25,30 +29,36 @@
ATTR_EMBED_THUMBNAIL = "thumbnail"
ATTR_IMAGES = "images"

# Deprecated in Home Assistant 2022.4
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string})


def get_service(hass, config, discovery_info=None):
"""Get the Discord notification service."""
token = config[CONF_TOKEN]
return DiscordNotificationService(hass, token)
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> DiscordNotificationService:
"""Get the NFAndroidTV notification service."""
if discovery_info is not None:
return DiscordNotificationService(hass, discovery_info[CONF_TOKEN])
return DiscordNotificationService(hass, config[CONF_TOKEN])


class DiscordNotificationService(BaseNotificationService):
"""Implement the notification service for Discord."""

def __init__(self, hass, token):
def __init__(self, hass: HomeAssistant, token: str) -> None:
"""Initialize the service."""
self.token = token
self.hass = hass

def file_exists(self, filename):
def file_exists(self, filename: str) -> bool:
"""Check if a file exists on disk and is in authorized path."""
if not self.hass.config.is_allowed_path(filename):
return False
return os.path.isfile(filename)

async def async_send_message(self, message, **kwargs):
async def async_send_message(self, message: str, **kwargs: Any) -> None:
"""Login to Discord, send message to channel(s) and log out."""
nextcord.VoiceClient.warn_nacl = False
discord_bot = nextcord.Client()
Expand Down Expand Up @@ -96,16 +106,18 @@ async def async_send_message(self, message, **kwargs):
try:
for channelid in kwargs[ATTR_TARGET]:
channelid = int(channelid)
# Must create new instances of File for each channel.
files = [nextcord.File(image) for image in images] if images else []
try:
channel = await discord_bot.fetch_channel(channelid)
channel = cast(
Messageable, await discord_bot.fetch_channel(channelid)
)
except nextcord.NotFound:
try:
channel = await discord_bot.fetch_user(channelid)
except nextcord.NotFound:
_LOGGER.warning("Channel not found for ID: %s", channelid)
continue
# Must create new instances of File for each channel.
files = [nextcord.File(image) for image in images] if images else []
await channel.send(message, files=files, embeds=embeds)
except (nextcord.HTTPException, nextcord.NotFound) as error:
_LOGGER.warning("Communication error: %s", error)
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/discord/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"config": {
"step": {
"user": {
"title": "Discord Notifications",
"description": "Refer to the documentation on getting your Discord bot key.\n\nhttps://www.home-assistant.io/integrations/discord",
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]",
"name": "[%key:common::config_flow::data::name%]"
}
},
"reauth_confirm": {
"title": "Discord Notifications",
"description": "Refer to the documentation on getting your Discord bot key.\n\nhttps://www.home-assistant.io/integrations/discord",
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}

30 changes: 30 additions & 0 deletions homeassistant/components/discord/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"api_token": "Token",
"name": "Name"
},
"description": "Refer to the documentation on getting your Discord bot key.\n\nhttps://www.home-assistant.io/integrations/discord",
"title": "Discord Notifications"
},
"reauth_confirm": {
"data": {
"api_token": "Token"
},
"description": "Refer to the documentation on getting your Discord bot key.\n\nhttps://www.home-assistant.io/integrations/discord",
"title": "Discord Notifications"
}
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"dexcom",
"dialogflow",
"directv",
"discord",
"dlna_dmr",
"dlna_dms",
"dnsip",
Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,9 @@ nettigo-air-monitor==1.2.1
# homeassistant.components.nexia
nexia==0.9.13

# homeassistant.components.discord
nextcord==2.0.0a8

# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.3

Expand Down
Loading