Bump python-telegram-bot package to 21.0.1 (#110297)

* Bump python-telegram-bot package version to the latest.

* PySocks is no longer required as python-telegram-bot doesn't use urllib3 anymore.

* Fix moved ParseMode import

* Update filters import to new structure.

* Refactor removed Request objects to HTTPXRequest objects.

* Update to support asyncc functions

* Update timeout to new kwarg

connect_timeout is the most obvious option based on current param description, but this may need changing.

* Compatibility typo.

* Make methods async and use Bot client async natively

* Type needs to be Optional

That's what the source types are from the library
Also handle new possibility of None value

* Add socks support version of the library

* Refactor load_data function

Update to be async friendly
Refactor to use httpx instead of requests.

* Refactor Dispatcher references to Application

This is the newer model of the same class.

* Make more stuff async-friendly.

* Update tests to refactor Dispatcher usage out.

* Remove import and reference directly

* Refactor typing method

* Use async_fire now we have async support

* Fix some over complicate inheritance.

* Add the polling test telegram_text event fired back in.

* Add extra context to comment

* Handler should also be async

* Use underscores instead of camelCase

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Renamed kwarg.

* Refactor current timeout param to be read timeout

Reading the old version of the library code I believe this matches the existing functionality best

* Combine unload methods into one listener

* Fix test by stopping HA as part of fixture

* Add new fixture to mock stop_polling call

Use this in all polling tests.

* No longer need to check if application is running

It was to stop a test failing.

* Make sure the updater is started in tests

Mock external call methods
Remove stop_polling mock.

* Use cleaner references to patched methods

* Improve test by letting the library create the Update object

* Mock component tear down methods to be async

* Bump mypy cache version

* Update dependency to install from git

Allows use as a custom component in 2024.3
Allows us to track mypy issue resolution.

* Update manifest and requirements for new python-telegram-bot release.

* Remove pytest filterwarnings entry for old version of python-telegram-bot library.

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Jim 2024-03-08 07:56:26 +00:00 committed by GitHub
parent 15b59d310a
commit d2effd8693
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 240 additions and 192 deletions

View file

@ -35,7 +35,7 @@ on:
env: env:
CACHE_VERSION: 5 CACHE_VERSION: 5
PIP_CACHE_VERSION: 4 PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 7 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.4" HA_SHORT_VERSION: "2024.4"
DEFAULT_PYTHON: "3.11" DEFAULT_PYTHON: "3.11"
ALL_PYTHON_VERSIONS: "['3.11', '3.12']" ALL_PYTHON_VERSIONS: "['3.11', '3.12']"

View file

@ -1,15 +1,14 @@
"""Support to send and receive Telegram messages.""" """Support to send and receive Telegram messages."""
from __future__ import annotations from __future__ import annotations
from functools import partial import asyncio
import importlib import importlib
import io import io
from ipaddress import ip_network from ipaddress import ip_network
import logging import logging
from typing import Any from typing import Any
import requests import httpx
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from telegram import ( from telegram import (
Bot, Bot,
CallbackQuery, CallbackQuery,
@ -21,10 +20,10 @@ from telegram import (
Update, Update,
User, User,
) )
from telegram.constants import ParseMode
from telegram.error import TelegramError from telegram.error import TelegramError
from telegram.ext import CallbackContext, Filters from telegram.ext import CallbackContext, filters
from telegram.parsemode import ParseMode from telegram.request import HTTPXRequest
from telegram.utils.request import Request
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -283,7 +282,7 @@ SERVICE_MAP = {
} }
def load_data( async def load_data(
hass, hass,
url=None, url=None,
filepath=None, filepath=None,
@ -297,35 +296,48 @@ def load_data(
try: try:
if url is not None: if url is not None:
# Load data from URL # Load data from URL
params = {"timeout": 15} params = {}
headers = {}
if authentication == HTTP_BEARER_AUTHENTICATION and password is not None: if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
params["headers"] = {"Authorization": f"Bearer {password}"} headers = {"Authorization": f"Bearer {password}"}
elif username is not None and password is not None: elif username is not None and password is not None:
if authentication == HTTP_DIGEST_AUTHENTICATION: if authentication == HTTP_DIGEST_AUTHENTICATION:
params["auth"] = HTTPDigestAuth(username, password) params["auth"] = httpx.DigestAuth(username, password)
else: else:
params["auth"] = HTTPBasicAuth(username, password) params["auth"] = httpx.BasicAuth(username, password)
if verify_ssl is not None: if verify_ssl is not None:
params["verify"] = verify_ssl params["verify"] = verify_ssl
retry_num = 0 retry_num = 0
while retry_num < num_retries: async with httpx.AsyncClient(
req = requests.get(url, **params) timeout=15, headers=headers, **params
if not req.ok: ) as client:
_LOGGER.warning( while retry_num < num_retries:
"Status code %s (retry #%s) loading %s", req = await client.get(url)
req.status_code, if req.status_code != 200:
retry_num + 1, _LOGGER.warning(
url, "Status code %s (retry #%s) loading %s",
) req.status_code,
else: retry_num + 1,
data = io.BytesIO(req.content) url,
if data.read(): )
data.seek(0) else:
data.name = url data = io.BytesIO(req.content)
return data if data.read():
_LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) data.seek(0)
retry_num += 1 data.name = url
_LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) return data
_LOGGER.warning(
"Empty data (retry #%s) in %s)", retry_num + 1, url
)
retry_num += 1
if retry_num < num_retries:
await asyncio.sleep(
1
) # Add a sleep to allow other async operations to proceed
_LOGGER.warning(
"Can't load data in %s after %s retries", url, retry_num
)
elif filepath is not None: elif filepath is not None:
if hass.config.is_allowed_path(filepath): if hass.config.is_allowed_path(filepath):
return open(filepath, "rb") return open(filepath, "rb")
@ -406,9 +418,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER.debug("New telegram message %s: %s", msgtype, kwargs) _LOGGER.debug("New telegram message %s: %s", msgtype, kwargs)
if msgtype == SERVICE_SEND_MESSAGE: if msgtype == SERVICE_SEND_MESSAGE:
await hass.async_add_executor_job( await notify_service.send_message(**kwargs)
partial(notify_service.send_message, **kwargs)
)
elif msgtype in [ elif msgtype in [
SERVICE_SEND_PHOTO, SERVICE_SEND_PHOTO,
SERVICE_SEND_ANIMATION, SERVICE_SEND_ANIMATION,
@ -416,33 +426,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_SEND_VOICE, SERVICE_SEND_VOICE,
SERVICE_SEND_DOCUMENT, SERVICE_SEND_DOCUMENT,
]: ]:
await hass.async_add_executor_job( await notify_service.send_file(msgtype, **kwargs)
partial(notify_service.send_file, msgtype, **kwargs)
)
elif msgtype == SERVICE_SEND_STICKER: elif msgtype == SERVICE_SEND_STICKER:
await hass.async_add_executor_job( await notify_service.send_sticker(**kwargs)
partial(notify_service.send_sticker, **kwargs)
)
elif msgtype == SERVICE_SEND_LOCATION: elif msgtype == SERVICE_SEND_LOCATION:
await hass.async_add_executor_job( await notify_service.send_location(**kwargs)
partial(notify_service.send_location, **kwargs)
)
elif msgtype == SERVICE_SEND_POLL: elif msgtype == SERVICE_SEND_POLL:
await hass.async_add_executor_job( await notify_service.send_poll(**kwargs)
partial(notify_service.send_poll, **kwargs)
)
elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY:
await hass.async_add_executor_job( await notify_service.answer_callback_query(**kwargs)
partial(notify_service.answer_callback_query, **kwargs)
)
elif msgtype == SERVICE_DELETE_MESSAGE: elif msgtype == SERVICE_DELETE_MESSAGE:
await hass.async_add_executor_job( await notify_service.delete_message(**kwargs)
partial(notify_service.delete_message, **kwargs)
)
else: else:
await hass.async_add_executor_job( await notify_service.edit_message(msgtype, **kwargs)
partial(notify_service.edit_message, msgtype, **kwargs)
)
# Register notification services # Register notification services
for service_notif, schema in SERVICE_MAP.items(): for service_notif, schema in SERVICE_MAP.items():
@ -460,11 +456,13 @@ def initialize_bot(p_config):
proxy_params = p_config.get(CONF_PROXY_PARAMS) proxy_params = p_config.get(CONF_PROXY_PARAMS)
if proxy_url is not None: if proxy_url is not None:
request = Request( # These have been kept for backwards compatibility, they can actually be stuffed into the URL.
con_pool_size=8, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params # Side note: In the future we should deprecate these and raise a repair issue if we find them here.
) auth = proxy_params.pop("username"), proxy_params.pop("password")
proxy = httpx.Proxy(proxy_url, auth=auth, **proxy_params)
request = HTTPXRequest(connection_pool_size=8, proxy=proxy)
else: else:
request = Request(con_pool_size=8) request = HTTPXRequest(connection_pool_size=8)
return Bot(token=api_key, request=request) return Bot(token=api_key, request=request)
@ -616,10 +614,12 @@ class TelegramNotificationService:
) )
return params return params
def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg): async def _send_msg(
self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg
):
"""Send one message.""" """Send one message."""
try: try:
out = func_send(*args_msg, **kwargs_msg) out = await func_send(*args_msg, **kwargs_msg)
if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID):
chat_id = out.chat_id chat_id = out.chat_id
message_id = out[ATTR_MESSAGEID] message_id = out[ATTR_MESSAGEID]
@ -636,7 +636,7 @@ class TelegramNotificationService:
} }
if message_tag is not None: if message_tag is not None:
event_data[ATTR_MESSAGE_TAG] = message_tag event_data[ATTR_MESSAGE_TAG] = message_tag
self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data) self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data)
elif not isinstance(out, bool): elif not isinstance(out, bool):
_LOGGER.warning( _LOGGER.warning(
"Update last message: out_type:%s, out=%s", type(out), out "Update last message: out_type:%s, out=%s", type(out), out
@ -647,14 +647,14 @@ class TelegramNotificationService:
"%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg "%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg
) )
def send_message(self, message="", target=None, **kwargs): async def send_message(self, message="", target=None, **kwargs):
"""Send a message to one or multiple pre-allowed chat IDs.""" """Send a message to one or multiple pre-allowed chat IDs."""
title = kwargs.get(ATTR_TITLE) title = kwargs.get(ATTR_TITLE)
text = f"{title}\n{message}" if title else message text = f"{title}\n{message}" if title else message
params = self._get_msg_kwargs(kwargs) params = self._get_msg_kwargs(kwargs)
for chat_id in self._get_target_chat_ids(target): for chat_id in self._get_target_chat_ids(target):
_LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params)
self._send_msg( await self._send_msg(
self.bot.send_message, self.bot.send_message,
"Error sending message", "Error sending message",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -665,15 +665,15 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
def delete_message(self, chat_id=None, **kwargs): async def delete_message(self, chat_id=None, **kwargs):
"""Delete a previously sent message.""" """Delete a previously sent message."""
chat_id = self._get_target_chat_ids(chat_id)[0] chat_id = self._get_target_chat_ids(chat_id)[0]
message_id, _ = self._get_msg_ids(kwargs, chat_id) message_id, _ = self._get_msg_ids(kwargs, chat_id)
_LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id) _LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id)
deleted = self._send_msg( deleted = await self._send_msg(
self.bot.delete_message, "Error deleting message", None, chat_id, message_id self.bot.delete_message, "Error deleting message", None, chat_id, message_id
) )
# reduce message_id anyway: # reduce message_id anyway:
@ -682,7 +682,7 @@ class TelegramNotificationService:
self._last_message_id[chat_id] -= 1 self._last_message_id[chat_id] -= 1
return deleted return deleted
def edit_message(self, type_edit, chat_id=None, **kwargs): async def edit_message(self, type_edit, chat_id=None, **kwargs):
"""Edit a previously sent message.""" """Edit a previously sent message."""
chat_id = self._get_target_chat_ids(chat_id)[0] chat_id = self._get_target_chat_ids(chat_id)[0]
message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id) message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id)
@ -698,7 +698,7 @@ class TelegramNotificationService:
title = kwargs.get(ATTR_TITLE) title = kwargs.get(ATTR_TITLE)
text = f"{title}\n{message}" if title else message text = f"{title}\n{message}" if title else message
_LOGGER.debug("Editing message with ID %s", message_id or inline_message_id) _LOGGER.debug("Editing message with ID %s", message_id or inline_message_id)
return self._send_msg( return await self._send_msg(
self.bot.edit_message_text, self.bot.edit_message_text,
"Error editing text message", "Error editing text message",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -709,10 +709,10 @@ class TelegramNotificationService:
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
if type_edit == SERVICE_EDIT_CAPTION: if type_edit == SERVICE_EDIT_CAPTION:
return self._send_msg( return await self._send_msg(
self.bot.edit_message_caption, self.bot.edit_message_caption,
"Error editing message attributes", "Error editing message attributes",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -721,11 +721,11 @@ class TelegramNotificationService:
inline_message_id=inline_message_id, inline_message_id=inline_message_id,
caption=kwargs.get(ATTR_CAPTION), caption=kwargs.get(ATTR_CAPTION),
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
) )
return self._send_msg( return await self._send_msg(
self.bot.edit_message_reply_markup, self.bot.edit_message_reply_markup,
"Error editing message attributes", "Error editing message attributes",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -733,10 +733,10 @@ class TelegramNotificationService:
message_id=message_id, message_id=message_id,
inline_message_id=inline_message_id, inline_message_id=inline_message_id,
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
def answer_callback_query( async def answer_callback_query(
self, message, callback_query_id, show_alert=False, **kwargs self, message, callback_query_id, show_alert=False, **kwargs
): ):
"""Answer a callback originated with a press in an inline keyboard.""" """Answer a callback originated with a press in an inline keyboard."""
@ -747,20 +747,20 @@ class TelegramNotificationService:
message, message,
show_alert, show_alert,
) )
self._send_msg( await self._send_msg(
self.bot.answer_callback_query, self.bot.answer_callback_query,
"Error sending answer callback query", "Error sending answer callback query",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
callback_query_id, callback_query_id,
text=message, text=message,
show_alert=show_alert, show_alert=show_alert,
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): async def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs):
"""Send a photo, sticker, video, or document.""" """Send a photo, sticker, video, or document."""
params = self._get_msg_kwargs(kwargs) params = self._get_msg_kwargs(kwargs)
file_content = load_data( file_content = await load_data(
self.hass, self.hass,
url=kwargs.get(ATTR_URL), url=kwargs.get(ATTR_URL),
filepath=kwargs.get(ATTR_FILE), filepath=kwargs.get(ATTR_FILE),
@ -775,7 +775,7 @@ class TelegramNotificationService:
_LOGGER.debug("Sending file to chat ID %s", chat_id) _LOGGER.debug("Sending file to chat ID %s", chat_id)
if file_type == SERVICE_SEND_PHOTO: if file_type == SERVICE_SEND_PHOTO:
self._send_msg( await self._send_msg(
self.bot.send_photo, self.bot.send_photo,
"Error sending photo", "Error sending photo",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -785,12 +785,12 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
) )
elif file_type == SERVICE_SEND_STICKER: elif file_type == SERVICE_SEND_STICKER:
self._send_msg( await self._send_msg(
self.bot.send_sticker, self.bot.send_sticker,
"Error sending sticker", "Error sending sticker",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -799,11 +799,11 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
elif file_type == SERVICE_SEND_VIDEO: elif file_type == SERVICE_SEND_VIDEO:
self._send_msg( await self._send_msg(
self.bot.send_video, self.bot.send_video,
"Error sending video", "Error sending video",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -813,11 +813,11 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
) )
elif file_type == SERVICE_SEND_DOCUMENT: elif file_type == SERVICE_SEND_DOCUMENT:
self._send_msg( await self._send_msg(
self.bot.send_document, self.bot.send_document,
"Error sending document", "Error sending document",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -827,11 +827,11 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
) )
elif file_type == SERVICE_SEND_VOICE: elif file_type == SERVICE_SEND_VOICE:
self._send_msg( await self._send_msg(
self.bot.send_voice, self.bot.send_voice,
"Error sending voice", "Error sending voice",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -841,10 +841,10 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
elif file_type == SERVICE_SEND_ANIMATION: elif file_type == SERVICE_SEND_ANIMATION:
self._send_msg( await self._send_msg(
self.bot.send_animation, self.bot.send_animation,
"Error sending animation", "Error sending animation",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -854,7 +854,7 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
parse_mode=params[ATTR_PARSER], parse_mode=params[ATTR_PARSER],
) )
@ -862,13 +862,13 @@ class TelegramNotificationService:
else: else:
_LOGGER.error("Can't send file with kwargs: %s", kwargs) _LOGGER.error("Can't send file with kwargs: %s", kwargs)
def send_sticker(self, target=None, **kwargs): async def send_sticker(self, target=None, **kwargs):
"""Send a sticker from a telegram sticker pack.""" """Send a sticker from a telegram sticker pack."""
params = self._get_msg_kwargs(kwargs) params = self._get_msg_kwargs(kwargs)
stickerid = kwargs.get(ATTR_STICKER_ID) stickerid = kwargs.get(ATTR_STICKER_ID)
if stickerid: if stickerid:
for chat_id in self._get_target_chat_ids(target): for chat_id in self._get_target_chat_ids(target):
self._send_msg( await self._send_msg(
self.bot.send_sticker, self.bot.send_sticker,
"Error sending sticker", "Error sending sticker",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -877,12 +877,12 @@ class TelegramNotificationService:
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
reply_markup=params[ATTR_REPLYMARKUP], reply_markup=params[ATTR_REPLYMARKUP],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
else: else:
self.send_file(SERVICE_SEND_STICKER, target, **kwargs) await self.send_file(SERVICE_SEND_STICKER, target, **kwargs)
def send_location(self, latitude, longitude, target=None, **kwargs): async def send_location(self, latitude, longitude, target=None, **kwargs):
"""Send a location.""" """Send a location."""
latitude = float(latitude) latitude = float(latitude)
longitude = float(longitude) longitude = float(longitude)
@ -891,7 +891,7 @@ class TelegramNotificationService:
_LOGGER.debug( _LOGGER.debug(
"Send location %s/%s to chat ID %s", latitude, longitude, chat_id "Send location %s/%s to chat ID %s", latitude, longitude, chat_id
) )
self._send_msg( await self._send_msg(
self.bot.send_location, self.bot.send_location,
"Error sending location", "Error sending location",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -900,10 +900,10 @@ class TelegramNotificationService:
longitude=longitude, longitude=longitude,
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
def send_poll( async def send_poll(
self, self,
question, question,
options, options,
@ -917,7 +917,7 @@ class TelegramNotificationService:
openperiod = kwargs.get(ATTR_OPEN_PERIOD) openperiod = kwargs.get(ATTR_OPEN_PERIOD)
for chat_id in self._get_target_chat_ids(target): for chat_id in self._get_target_chat_ids(target):
_LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id) _LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id)
self._send_msg( await self._send_msg(
self.bot.send_poll, self.bot.send_poll,
"Error sending poll", "Error sending poll",
params[ATTR_MESSAGE_TAG], params[ATTR_MESSAGE_TAG],
@ -929,14 +929,14 @@ class TelegramNotificationService:
open_period=openperiod, open_period=openperiod,
disable_notification=params[ATTR_DISABLE_NOTIF], disable_notification=params[ATTR_DISABLE_NOTIF],
reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
timeout=params[ATTR_TIMEOUT], read_timeout=params[ATTR_TIMEOUT],
) )
def leave_chat(self, chat_id=None): async def leave_chat(self, chat_id=None):
"""Remove bot from chat.""" """Remove bot from chat."""
chat_id = self._get_target_chat_ids(chat_id)[0] chat_id = self._get_target_chat_ids(chat_id)[0]
_LOGGER.debug("Leave from chat ID %s", chat_id) _LOGGER.debug("Leave from chat ID %s", chat_id)
leaved = self._send_msg( leaved = await self._send_msg(
self.bot.leave_chat, "Error leaving chat", None, chat_id self.bot.leave_chat, "Error leaving chat", None, chat_id
) )
return leaved return leaved
@ -950,8 +950,8 @@ class BaseTelegramBotEntity:
self.allowed_chat_ids = config[CONF_ALLOWED_CHAT_IDS] self.allowed_chat_ids = config[CONF_ALLOWED_CHAT_IDS]
self.hass = hass self.hass = hass
def handle_update(self, update: Update, context: CallbackContext) -> bool: async def handle_update(self, update: Update, context: CallbackContext) -> bool:
"""Handle updates from bot dispatcher set up by the respective platform.""" """Handle updates from bot application set up by the respective platform."""
_LOGGER.debug("Handling update %s", update) _LOGGER.debug("Handling update %s", update)
if not self.authorize_update(update): if not self.authorize_update(update):
return False return False
@ -972,12 +972,12 @@ class BaseTelegramBotEntity:
return True return True
_LOGGER.debug("Firing event %s: %s", event_type, event_data) _LOGGER.debug("Firing event %s: %s", event_type, event_data)
self.hass.bus.fire(event_type, event_data) self.hass.bus.async_fire(event_type, event_data)
return True return True
@staticmethod @staticmethod
def _get_command_event_data(command_text: str) -> dict[str, str | list]: def _get_command_event_data(command_text: str | None) -> dict[str, str | list]:
if not command_text.startswith("/"): if not command_text or not command_text.startswith("/"):
return {} return {}
command_parts = command_text.split() command_parts = command_text.split()
command = command_parts[0] command = command_parts[0]
@ -990,7 +990,7 @@ class BaseTelegramBotEntity:
ATTR_CHAT_ID: message.chat.id, ATTR_CHAT_ID: message.chat.id,
ATTR_DATE: message.date, ATTR_DATE: message.date,
} }
if Filters.command.filter(message): if filters.COMMAND.filter(message):
# This is a command message - set event type to command and split data into command and args # This is a command message - set event type to command and split data into command and args
event_type = EVENT_TELEGRAM_COMMAND event_type = EVENT_TELEGRAM_COMMAND
event_data.update(self._get_command_event_data(message.text)) event_data.update(self._get_command_event_data(message.text))

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/telegram_bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["telegram"], "loggers": ["telegram"],
"requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"] "requirements": ["python-telegram-bot[socks]==21.0.1"]
} }

View file

@ -3,7 +3,7 @@ import logging
from telegram import Update from telegram import Update
from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut
from telegram.ext import CallbackContext, TypeHandler, Updater from telegram.ext import ApplicationBuilder, CallbackContext, TypeHandler
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
@ -22,7 +22,7 @@ async def async_setup_platform(hass, bot, config):
return True return True
def process_error(update: Update, context: CallbackContext) -> None: async def process_error(update: Update, context: CallbackContext) -> None:
"""Telegram bot error handler.""" """Telegram bot error handler."""
try: try:
if context.error: if context.error:
@ -35,26 +35,29 @@ def process_error(update: Update, context: CallbackContext) -> None:
class PollBot(BaseTelegramBotEntity): class PollBot(BaseTelegramBotEntity):
"""Controls the Updater object that holds the bot and a dispatcher. """Controls the Application object that holds the bot and an updater.
The dispatcher is set up by the super class to pass telegram updates to `self.handle_update` The application is set up to pass telegram updates to `self.handle_update`
""" """
def __init__(self, hass, bot, config): def __init__(self, hass, bot, config):
"""Create Updater and Dispatcher before calling super().""" """Create Application to poll for updates."""
self.bot = bot
self.updater = Updater(bot=bot, workers=4)
self.dispatcher = self.updater.dispatcher
self.dispatcher.add_handler(TypeHandler(Update, self.handle_update))
self.dispatcher.add_error_handler(process_error)
super().__init__(hass, config) super().__init__(hass, config)
self.bot = bot
self.application = ApplicationBuilder().bot(self.bot).build()
self.application.add_handler(TypeHandler(Update, self.handle_update))
self.application.add_error_handler(process_error)
def start_polling(self, event=None): async def start_polling(self, event=None):
"""Start the polling task.""" """Start the polling task."""
_LOGGER.debug("Starting polling") _LOGGER.debug("Starting polling")
self.updater.start_polling() await self.application.initialize()
await self.application.updater.start_polling()
await self.application.start()
def stop_polling(self, event=None): async def stop_polling(self, event=None):
"""Stop the polling task.""" """Stop the polling task."""
_LOGGER.debug("Stopping polling") _LOGGER.debug("Stopping polling")
self.updater.stop() await self.application.updater.stop()
await self.application.stop()
await self.application.shutdown()

View file

@ -29,8 +29,8 @@
"description": "Disables link previews for links in the message." "description": "Disables link previews for links in the message."
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send message. Will help with timeout errors (poor internet connection, etc)s." "description": "Read timeout for send message. Will help with timeout errors (poor internet connection, etc)s."
}, },
"keyboard": { "keyboard": {
"name": "Keyboard", "name": "Keyboard",
@ -95,8 +95,8 @@
"description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server."
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send photo."
}, },
"keyboard": { "keyboard": {
"name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]",
@ -157,8 +157,8 @@
"description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send sticker."
}, },
"keyboard": { "keyboard": {
"name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]",
@ -223,7 +223,7 @@
"description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "[%key:component::telegram_bot::services::send_sticker::fields::timeout::description%]" "description": "[%key:component::telegram_bot::services::send_sticker::fields::timeout::description%]"
}, },
"keyboard": { "keyboard": {
@ -289,8 +289,8 @@
"description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send video. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send video."
}, },
"keyboard": { "keyboard": {
"name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]",
@ -351,8 +351,8 @@
"description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send voice. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send voice."
}, },
"keyboard": { "keyboard": {
"name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]",
@ -417,8 +417,8 @@
"description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send document. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send document."
}, },
"keyboard": { "keyboard": {
"name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]",
@ -459,7 +459,7 @@
"description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "[%key:component::telegram_bot::services::send_photo::fields::timeout::description%]" "description": "[%key:component::telegram_bot::services::send_photo::fields::timeout::description%]"
}, },
"keyboard": { "keyboard": {
@ -513,8 +513,8 @@
"description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]"
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for send poll. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for send poll."
}, },
"message_tag": { "message_tag": {
"name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]",
@ -617,8 +617,8 @@
"description": "Show a permanent notification." "description": "Show a permanent notification."
}, },
"timeout": { "timeout": {
"name": "Timeout", "name": "Read timeout",
"description": "Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc)." "description": "Read timeout for sending the answer."
} }
} }
}, },

View file

@ -8,7 +8,7 @@ import string
from telegram import Update from telegram import Update
from telegram.error import TimedOut from telegram.error import TimedOut
from telegram.ext import Dispatcher, TypeHandler from telegram.ext import Application, TypeHandler
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
@ -36,16 +36,17 @@ async def async_setup_platform(hass, bot, config):
_LOGGER.error("Invalid telegram webhook %s must be https", pushbot.webhook_url) _LOGGER.error("Invalid telegram webhook %s must be https", pushbot.webhook_url)
return False return False
await pushbot.start_application()
webhook_registered = await pushbot.register_webhook() webhook_registered = await pushbot.register_webhook()
if not webhook_registered: if not webhook_registered:
return False return False
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pushbot.deregister_webhook) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pushbot.stop_application)
hass.http.register_view( hass.http.register_view(
PushBotView( PushBotView(
hass, hass,
bot, bot,
pushbot.dispatcher, pushbot.application,
config[CONF_TRUSTED_NETWORKS], config[CONF_TRUSTED_NETWORKS],
secret_token, secret_token,
) )
@ -57,13 +58,13 @@ class PushBot(BaseTelegramBotEntity):
"""Handles all the push/webhook logic and passes telegram updates to `self.handle_update`.""" """Handles all the push/webhook logic and passes telegram updates to `self.handle_update`."""
def __init__(self, hass, bot, config, secret_token): def __init__(self, hass, bot, config, secret_token):
"""Create Dispatcher before calling super().""" """Create Application before calling super()."""
self.bot = bot self.bot = bot
self.trusted_networks = config[CONF_TRUSTED_NETWORKS] self.trusted_networks = config[CONF_TRUSTED_NETWORKS]
self.secret_token = secret_token self.secret_token = secret_token
# Dumb dispatcher that just gets our updates to our handler callback (self.handle_update) # Dumb Application that just gets our updates to our handler callback (self.handle_update)
self.dispatcher = Dispatcher(bot, None) self.application = Application.builder().bot(bot).updater(None).build()
self.dispatcher.add_handler(TypeHandler(Update, self.handle_update)) self.application.add_handler(TypeHandler(Update, self.handle_update))
super().__init__(hass, config) super().__init__(hass, config)
self.base_url = config.get(CONF_URL) or get_url( self.base_url = config.get(CONF_URL) or get_url(
@ -71,15 +72,15 @@ class PushBot(BaseTelegramBotEntity):
) )
self.webhook_url = f"{self.base_url}{TELEGRAM_WEBHOOK_URL}" self.webhook_url = f"{self.base_url}{TELEGRAM_WEBHOOK_URL}"
def _try_to_set_webhook(self): async def _try_to_set_webhook(self):
_LOGGER.debug("Registering webhook URL: %s", self.webhook_url) _LOGGER.debug("Registering webhook URL: %s", self.webhook_url)
retry_num = 0 retry_num = 0
while retry_num < 3: while retry_num < 3:
try: try:
return self.bot.set_webhook( return await self.bot.set_webhook(
self.webhook_url, self.webhook_url,
api_kwargs={"secret_token": self.secret_token}, api_kwargs={"secret_token": self.secret_token},
timeout=5, connect_timeout=5,
) )
except TimedOut: except TimedOut:
retry_num += 1 retry_num += 1
@ -87,11 +88,14 @@ class PushBot(BaseTelegramBotEntity):
return False return False
async def start_application(self):
"""Handle starting the Application object."""
await self.application.initialize()
await self.application.start()
async def register_webhook(self): async def register_webhook(self):
"""Query telegram and register the URL for our webhook.""" """Query telegram and register the URL for our webhook."""
current_status = await self.hass.async_add_executor_job( current_status = await self.bot.get_webhook_info()
self.bot.get_webhook_info
)
# Some logging of Bot current status: # Some logging of Bot current status:
last_error_date = getattr(current_status, "last_error_date", None) last_error_date = getattr(current_status, "last_error_date", None)
if (last_error_date is not None) and (isinstance(last_error_date, int)): if (last_error_date is not None) and (isinstance(last_error_date, int)):
@ -105,7 +109,7 @@ class PushBot(BaseTelegramBotEntity):
_LOGGER.debug("telegram webhook status: %s", current_status) _LOGGER.debug("telegram webhook status: %s", current_status)
if current_status and current_status["url"] != self.webhook_url: if current_status and current_status["url"] != self.webhook_url:
result = await self.hass.async_add_executor_job(self._try_to_set_webhook) result = await self._try_to_set_webhook()
if result: if result:
_LOGGER.info("Set new telegram webhook %s", self.webhook_url) _LOGGER.info("Set new telegram webhook %s", self.webhook_url)
else: else:
@ -114,10 +118,16 @@ class PushBot(BaseTelegramBotEntity):
return True return True
def deregister_webhook(self, event=None): async def stop_application(self, event=None):
"""Handle gracefully stopping the Application object."""
await self.deregister_webhook()
await self.application.stop()
await self.application.shutdown()
async def deregister_webhook(self):
"""Query telegram and deregister the URL for our webhook.""" """Query telegram and deregister the URL for our webhook."""
_LOGGER.debug("Deregistering webhook URL") _LOGGER.debug("Deregistering webhook URL")
return self.bot.delete_webhook() await self.bot.delete_webhook()
class PushBotView(HomeAssistantView): class PushBotView(HomeAssistantView):
@ -127,11 +137,11 @@ class PushBotView(HomeAssistantView):
url = TELEGRAM_WEBHOOK_URL url = TELEGRAM_WEBHOOK_URL
name = "telegram_webhooks" name = "telegram_webhooks"
def __init__(self, hass, bot, dispatcher, trusted_networks, secret_token): def __init__(self, hass, bot, application, trusted_networks, secret_token):
"""Initialize by storing stuff needed for setting up our webhook endpoint.""" """Initialize by storing stuff needed for setting up our webhook endpoint."""
self.hass = hass self.hass = hass
self.bot = bot self.bot = bot
self.dispatcher = dispatcher self.application = application
self.trusted_networks = trusted_networks self.trusted_networks = trusted_networks
self.secret_token = secret_token self.secret_token = secret_token
@ -153,6 +163,6 @@ class PushBotView(HomeAssistantView):
update = Update.de_json(update_data, self.bot) update = Update.de_json(update_data, self.bot)
_LOGGER.debug("Received Update on %s: %s", self.url, update) _LOGGER.debug("Received Update on %s: %s", self.url, update)
await self.hass.async_add_executor_job(self.dispatcher.process_update, update) await self.application.process_update(update)
return None return None

View file

@ -508,8 +508,6 @@ filterwarnings = [
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol",
# https://github.com/hunterjm/python-onvif-zeep-async/pull/51 - >3.1.12 # https://github.com/hunterjm/python-onvif-zeep-async/pull/51 - >3.1.12
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client",
# Fixed upstream in python-telegram-bot - >=20.0
"ignore:python-telegram-bot is using upstream urllib3:UserWarning:telegram.utils.request",
# https://github.com/xeniter/romy/pull/1 - >0.0.7 # https://github.com/xeniter/romy/pull/1 - >0.0.7
"ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils",
# https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3

View file

@ -92,9 +92,6 @@ PyQRCode==1.2.1
# homeassistant.components.rmvtransport # homeassistant.components.rmvtransport
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.telegram_bot
PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.45.0 PySwitchbot==0.45.0
@ -2300,7 +2297,7 @@ python-tado==0.17.4
python-technove==1.2.2 python-technove==1.2.2
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot[socks]==21.0.1
# homeassistant.components.vlc # homeassistant.components.vlc
python-vlc==3.0.18122 python-vlc==3.0.18122

View file

@ -80,9 +80,6 @@ PyQRCode==1.2.1
# homeassistant.components.rmvtransport # homeassistant.components.rmvtransport
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.telegram_bot
PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.45.0 PySwitchbot==0.45.0
@ -1773,7 +1770,7 @@ python-tado==0.17.4
python-technove==1.2.2 python-technove==1.2.2
# homeassistant.components.telegram_bot # homeassistant.components.telegram_bot
python-telegram-bot==13.1 python-telegram-bot[socks]==21.0.1
# homeassistant.components.tile # homeassistant.components.tile
pytile==2023.04.0 pytile==2023.04.0

View file

@ -2,13 +2,19 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from telegram import User
from homeassistant.components.telegram_bot import ( from homeassistant.components.telegram_bot import (
CONF_ALLOWED_CHAT_IDS, CONF_ALLOWED_CHAT_IDS,
CONF_TRUSTED_NETWORKS, CONF_TRUSTED_NETWORKS,
DOMAIN, DOMAIN,
) )
from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, CONF_URL from homeassistant.const import (
CONF_API_KEY,
CONF_PLATFORM,
CONF_URL,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -65,6 +71,23 @@ def mock_register_webhook():
yield yield
@pytest.fixture
def mock_external_calls():
"""Mock calls that make calls to the live Telegram API."""
test_user = User(123456, "Testbot", True)
with patch(
"telegram.Bot.get_me",
return_value=test_user,
), patch(
"telegram.Bot._bot_user",
test_user,
), patch(
"telegram.Bot.bot",
test_user,
), patch("telegram.ext.Updater._bootstrap"):
yield
@pytest.fixture @pytest.fixture
def mock_generate_secret_token(): def mock_generate_secret_token():
"""Mock secret token generated for webhook.""" """Mock secret token generated for webhook."""
@ -174,7 +197,11 @@ def update_callback_query():
@pytest.fixture @pytest.fixture
async def webhook_platform( async def webhook_platform(
hass, config_webhooks, mock_register_webhook, mock_generate_secret_token hass,
config_webhooks,
mock_register_webhook,
mock_external_calls,
mock_generate_secret_token,
): ):
"""Fixture for setting up the webhooks platform using appropriate config and mocks.""" """Fixture for setting up the webhooks platform using appropriate config and mocks."""
await async_setup_component( await async_setup_component(
@ -183,14 +210,18 @@ async def webhook_platform(
config_webhooks, config_webhooks,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
yield
await hass.async_stop()
@pytest.fixture @pytest.fixture
async def polling_platform(hass, config_polling): async def polling_platform(hass, config_polling, mock_external_calls):
"""Fixture for setting up the polling platform using appropriate config and mocks.""" """Fixture for setting up the polling platform using appropriate config and mocks."""
await async_setup_component( await async_setup_component(
hass, hass,
DOMAIN, DOMAIN,
config_polling, config_polling,
) )
# Fire this event to start polling
hass.bus.fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -1,25 +1,17 @@
"""Tests for the telegram_bot component.""" """Tests for the telegram_bot component."""
import pytest from unittest.mock import AsyncMock, patch
from telegram import Update from telegram import Update
from telegram.ext.dispatcher import Dispatcher
from homeassistant.components.telegram_bot import DOMAIN, SERVICE_SEND_MESSAGE from homeassistant.components.telegram_bot import DOMAIN, SERVICE_SEND_MESSAGE
from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import async_capture_events from tests.common import async_capture_events
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@pytest.fixture(autouse=True)
def clear_dispatcher():
"""Clear the singleton that telegram.ext.dispatcher.Dispatcher sets on itself."""
yield
Dispatcher._set_singleton(None)
# This is how python-telegram-bot resets the dispatcher in their test suite
Dispatcher._Dispatcher__singleton_semaphore.release()
async def test_webhook_platform_init(hass: HomeAssistant, webhook_platform) -> None: async def test_webhook_platform_init(hass: HomeAssistant, webhook_platform) -> None:
"""Test initialization of the webhooks platform.""" """Test initialization of the webhooks platform."""
assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True
@ -109,18 +101,38 @@ async def test_webhook_endpoint_generates_telegram_callback_event(
async def test_polling_platform_message_text_update( async def test_polling_platform_message_text_update(
hass: HomeAssistant, polling_platform, update_message_text hass: HomeAssistant, config_polling, update_message_text
) -> None: ) -> None:
"""Provide the `PollBot`s `Dispatcher` with an `Update` and assert fired `telegram_text` event.""" """Provide the `BaseTelegramBotEntity.update_handler` with an `Update` and assert fired `telegram_text` event."""
events = async_capture_events(hass, "telegram_text") events = async_capture_events(hass, "telegram_text")
def telegram_dispatcher_callback(): with patch(
dispatcher = Dispatcher.get_instance() "homeassistant.components.telegram_bot.polling.ApplicationBuilder"
update = Update.de_json(update_message_text, dispatcher.bot) ) as application_builder_class:
dispatcher.process_update(update) await async_setup_component(
hass,
DOMAIN,
config_polling,
)
await hass.async_block_till_done()
# Set up the integration with the polling platform inside the patch context manager.
application = (
application_builder_class.return_value.bot.return_value.build.return_value
)
# Then call the callback and assert events fired.
handler = application.add_handler.call_args[0][0]
handle_update_callback = handler.callback
# python-telegram-bots `Updater` uses threading, so we need to schedule its callback in a sync context. # Create Update object using library API.
await hass.async_add_executor_job(telegram_dispatcher_callback) application.bot.defaults.tzinfo = None
update = Update.de_json(update_message_text, application.bot)
# handle_update_callback == BaseTelegramBotEntity.update_handler
await handle_update_callback(update, None)
application.updater.stop = AsyncMock()
application.stop = AsyncMock()
application.shutdown = AsyncMock()
# Make sure event has fired # Make sure event has fired
await hass.async_block_till_done() await hass.async_block_till_done()