diff --git a/docs/source/conf.py b/docs/source/conf.py index 73508d36..13751948 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -231,6 +231,8 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'PyOBEX', 'todoist', 'trello', + 'telegram', + 'telegram.ext', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/backend/chat/__init__.py b/platypush/backend/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/platypush/backend/chat/telegram.py b/platypush/backend/chat/telegram.py new file mode 100644 index 00000000..0f441600 --- /dev/null +++ b/platypush/backend/chat/telegram.py @@ -0,0 +1,69 @@ +from typing import List, Optional + +from platypush.backend import Backend +from platypush.context import get_plugin +from platypush.message.event.chat.telegram import NewMessageEvent, NewCommandMessageEvent +from platypush.plugins.chat.telegram import ChatTelegramPlugin + + +class ChatTelegramBackend(Backend): + """ + Telegram bot that listens for messages and updates. + + Triggers: + + * :class:`platypush.message.event.chat.telegram.NewMessageEvent` when a new message is received. + * :class:`platypush.message.event.chat.telegram.NewCommandMessageEvent` when a new command message is received. + + Requires: + + * The :class:`platypush.plugins.chat.telegram.ChatTelegramPlugin` plugin configured + + """ + + def __init__(self, commands: Optional[List[str]] = None, **kwargs): + """ + :param commands: Optional list of commands to be registered on the bot (e.g. 'start', 'stop', 'help' etc.). + When you send e.g. '/start' to the bot conversation then a + :class:`platypush.message.event.chat.telegram.NewCommandMessageEvent` will be triggered instead of a + :class:`platypush.message.event.chat.telegram.NewMessageEvent` event. + """ + super().__init__(**kwargs) + self.commands = commands or [] + self._plugin: ChatTelegramPlugin = get_plugin('chat.telegram') + + def _msg_hook(self): + # noinspection PyUnusedLocal + def hook(update, context): + self.bus.post(NewMessageEvent(chat_id=update.effective_chat.id, + message=self._plugin.parse_msg(update.effective_message).output, + user=self._plugin.parse_user(update.effective_user).output)) + return hook + + def _command_hook(self, cmd): + # noinspection PyUnusedLocal + def hook(update, context): + self.bus.post(NewCommandMessageEvent(command=cmd, + chat_id=update.effective_chat.id, + message=self._plugin.parse_msg(update.effective_message).output, + user=self._plugin.parse_user(update.effective_user).output)) + + return hook + + def run(self): + # noinspection PyPackageRequirements + from telegram.ext import CommandHandler, MessageHandler, Filters + + super().run() + telegram = self._plugin.get_telegram() + dispatcher = telegram.dispatcher + dispatcher.add_handler(MessageHandler(Filters.text, self._msg_hook())) + + for cmd in self.commands: + dispatcher.add_handler(CommandHandler(cmd, self._command_hook(cmd))) + + self.logger.info('Initialized Telegram backend') + telegram.start_polling() + + +# vim:sw=4:ts=4:et: diff --git a/platypush/message/event/chat/__init__.py b/platypush/message/event/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/platypush/message/event/chat/telegram.py b/platypush/message/event/chat/telegram.py new file mode 100644 index 00000000..9aa82133 --- /dev/null +++ b/platypush/message/event/chat/telegram.py @@ -0,0 +1,25 @@ +from platypush.message.event import Event + + +class TelegramEvent(Event): + def __init__(self, *args, chat_id: int, **kwargs): + super().__init__(*args, chat_id=chat_id, **kwargs) + + +class NewMessageEvent(TelegramEvent): + """ + Event triggered when a new message is received by the Telegram bot. + """ + def __init__(self, *args, message, user, **kwargs): + super().__init__(*args, message=message, user=user, **kwargs) + + +class NewCommandMessageEvent(NewMessageEvent): + """ + Event triggered when a new message is received by the Telegram bot. + """ + def __init__(self, command: str, *args, **kwargs): + super().__init__(*args, command=command, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/message/response/chat/__init__.py b/platypush/message/response/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/platypush/message/response/chat/telegram.py b/platypush/message/response/chat/telegram.py new file mode 100644 index 00000000..dc3f8330 --- /dev/null +++ b/platypush/message/response/chat/telegram.py @@ -0,0 +1,164 @@ +import datetime + +from typing import Optional + +from platypush.message.response import Response + + +class TelegramMessageResponse(Response): + def __init__(self, + message_id: int, + chat_id: int, + creation_date: Optional[datetime.datetime], + chat_username: Optional[str] = None, + chat_firstname: Optional[str] = None, + chat_lastname: Optional[str] = None, + from_user_id: Optional[int] = None, + from_username: Optional[str] = None, + from_firstname: Optional[str] = None, + from_lastname: Optional[str] = None, + text: Optional[str] = None, + caption: Optional[str] = None, + edit_date: Optional[datetime.datetime] = None, + forward_date: Optional[datetime.datetime] = None, + forward_from_message_id: Optional[int] = None, + photo_file_id: Optional[str] = None, + photo_file_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + document_file_id: Optional[str] = None, + document_file_name: Optional[str] = None, + document_file_size: Optional[str] = None, + document_mime_type: Optional[str] = None, + audio_file_id: Optional[str] = None, + audio_file_size: Optional[str] = None, + audio_mime_type: Optional[str] = None, + audio_performer: Optional[str] = None, + audio_title: Optional[str] = None, + audio_duration: Optional[str] = None, + location_latitude: Optional[float] = None, + location_longitude: Optional[float] = None, + contact_phone_number: Optional[str] = None, + contact_first_name: Optional[str] = None, + contact_last_name: Optional[str] = None, + contact_user_id: Optional[int] = None, + contact_vcard: Optional[str] = None, + video_file_id: Optional[str] = None, + video_file_size: Optional[int] = None, + video_width: Optional[int] = None, + video_height: Optional[int] = None, + video_mime_type: Optional[str] = None, + video_duration: Optional[str] = None, + link: Optional[str] = None, + media_group_id: Optional[int] = None, + *args, **kwargs): + super().__init__(*args, output={ + 'message_id': message_id, + 'chat_id': chat_id, + 'chat_username': chat_username, + 'chat_firstname': chat_firstname, + 'chat_lastname': chat_lastname, + 'from_user_id': from_user_id, + 'from_username': from_username, + 'from_firstname': from_firstname, + 'from_lastname': from_lastname, + 'text': text, + 'caption': caption, + 'creation_date': creation_date, + 'edit_date': edit_date, + 'forward_from_message_id': forward_from_message_id, + 'forward_date': forward_date, + 'photo_file_id': photo_file_id, + 'photo_file_size': photo_file_size, + 'photo_width': photo_width, + 'photo_height': photo_height, + 'document_file_id': document_file_id, + 'document_file_name': document_file_name, + 'document_file_size': document_file_size, + 'document_mime_type': document_mime_type, + 'audio_file_id': audio_file_id, + 'audio_file_size': audio_file_size, + 'audio_performer': audio_performer, + 'audio_title': audio_title, + 'audio_duration': audio_duration, + 'audio_mime_type': audio_mime_type, + 'video_file_id': video_file_id, + 'video_file_size': video_file_size, + 'video_width': video_width, + 'video_height': video_height, + 'video_duration': video_duration, + 'video_mime_type': video_mime_type, + 'link': link, + 'location_latitude': location_latitude, + 'location_longitude': location_longitude, + 'contact_phone_number': contact_phone_number, + 'contact_first_name': contact_first_name, + 'contact_last_name': contact_last_name, + 'contact_user_id': contact_user_id, + 'contact_vcard': contact_vcard, + 'media_group_id': media_group_id, + }, **kwargs) + + +class TelegramFileResponse(Response): + def __init__(self, + file_id: str, + file_path: str, + file_size: int, + *args, **kwargs): + super().__init__(*args, output={ + 'file_id': file_id, + 'file_path': file_path, + 'file_size': file_size, + }, **kwargs) + + +class TelegramChatResponse(Response): + # noinspection PyShadowingBuiltins + def __init__(self, + chat_id: int, + link: str, + username: str, + invite_link: Optional[str], + title: Optional[str] = None, + description: Optional[str] = None, + type: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + *args, **kwargs): + super().__init__(*args, output={ + 'chat_id': chat_id, + 'link': link, + 'invite_link': invite_link, + 'username': username, + 'title': title, + 'description': description, + 'type': type, + 'first_name': first_name, + 'last_name': last_name, + }, **kwargs) + + +class TelegramUserResponse(Response): + # noinspection PyShadowingBuiltins + def __init__(self, + user_id: int, + username: str, + is_bot: bool, + first_name: str, + last_name: Optional[str] = None, + language_code: Optional[str] = None, + link: Optional[str] = None, + *args, **kwargs): + super().__init__(*args, output={ + 'user_id': user_id, + 'username': username, + 'is_bot': is_bot, + 'link': link, + 'language_code': language_code, + 'first_name': first_name, + 'last_name': last_name, + }, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/chat/__init__.py b/platypush/plugins/chat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/platypush/plugins/chat/telegram.py b/platypush/plugins/chat/telegram.py new file mode 100644 index 00000000..bb1ee531 --- /dev/null +++ b/platypush/plugins/chat/telegram.py @@ -0,0 +1,914 @@ +import datetime +import os + +from threading import RLock +from typing import Optional, Union + +# noinspection PyPackageRequirements +from telegram.ext import Updater +# noinspection PyPackageRequirements +from telegram.message import Message as TelegramMessage +# noinspection PyPackageRequirements +from telegram.user import User as TelegramUser + +from platypush.message.response.chat.telegram import TelegramMessageResponse, TelegramFileResponse, \ + TelegramChatResponse, TelegramUserResponse +from platypush.plugins import Plugin, action + + +class Resource: + def __init__(self, file_id: Optional[int] = None, url: Optional[str] = None, path: Optional[str] = None): + assert file_id or url or path, 'You need to specify either file_id, url or path' + self.file_id = file_id + self.url = url + self.path = path + self._file = None + + def __enter__(self): + if self.path: + self._file = open(os.path.abspath(os.path.expanduser(self.path)), 'rb') + return self._file + + return self.file_id or self.url + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._file: + self._file.close() + + +class ChatTelegramPlugin(Plugin): + """ + Plugin to programmatically send Telegram messages through a Telegram bot. In order to send messages to contacts, + groups or channels you'll first need to register a bot. To do so: + + 1. Open a Telegram conversation with the `@BotFather `_. + 2. Send ``/start`` followed by ``/newbot``. Choose a display name and a username for your bot. + 3. Copy the provided API token in the configuration of this plugin. + 4. Open a conversation with your newly created bot. + + Requires: + + * **python-telegram-bot** (``pip install python-telegram-bot``) + + """ + + def __init__(self, api_token: str, **kwargs): + """ + :param api_token: API token as returned by the `@BotFather `_ + """ + super().__init__(**kwargs) + + self._api_token = api_token + self._telegram_lock = RLock() + self._telegram: Optional[Updater] = None + + def get_telegram(self) -> Updater: + with self._telegram_lock: + if self._telegram: + return self._telegram + + self._telegram = Updater(self._api_token, use_context=True) + return self._telegram + + @staticmethod + def parse_msg(msg: TelegramMessage) -> TelegramMessageResponse: + return TelegramMessageResponse( + message_id=msg.message_id, + chat_id=msg.chat_id, + chat_username=msg.chat.username, + chat_firstname=msg.chat.first_name, + chat_lastname=msg.chat.last_name, + from_user_id=msg.from_user.id if msg.from_user else None, + from_username=msg.from_user.username if msg.from_user else None, + from_firstname=msg.from_user.first_name if msg.from_user else None, + from_lastname=msg.from_user.last_name if msg.from_user else None, + text=msg.text, + caption=msg.caption, + creation_date=msg.date, + edit_date=msg.edit_date, + forward_date=msg.forward_date, + forward_from_message_id=msg.forward_from_message_id, + photo_file_id=msg.photo[0].file_id if msg.photo else None, + photo_file_size=msg.photo[0].file_size if msg.photo else None, + photo_width=msg.photo[0].width if msg.photo else None, + photo_height=msg.photo[0].height if msg.photo else None, + document_file_id=msg.document.file_id if msg.document else None, + document_file_name=msg.document.file_name if msg.document else None, + document_file_size=msg.document.file_size if msg.document else None, + document_mime_type=msg.document.mime_type if msg.document else None, + audio_file_id=msg.audio.file_id if msg.audio else None, + audio_file_size=msg.audio.file_size if msg.audio else None, + audio_performer=msg.audio.performer if msg.audio else None, + audio_title=msg.audio.title if msg.audio else None, + audio_duration=msg.audio.duration if msg.audio else None, + audio_mime_type=msg.audio.mime_type if msg.audio else None, + video_file_id=msg.video.file_id if msg.video else None, + video_file_size=msg.video.file_size if msg.video else None, + video_duration=msg.video.duration if msg.video else None, + video_width=msg.video.width if msg.video else None, + video_height=msg.video.height if msg.video else None, + video_mime_type=msg.video.mime_type if msg.video else None, + location_latitude=msg.location.latitude if msg.location else None, + location_longitude=msg.location.longitude if msg.location else None, + contact_phone_number=msg.contact.phone_number if msg.contact else None, + contact_first_name=msg.contact.first_name if msg.contact else None, + contact_last_name=msg.contact.last_name if msg.contact else None, + contact_user_id=msg.contact.user_id if msg.contact else None, + contact_vcard=msg.contact.vcard if msg.contact else None, + link=msg.link, + media_group_id=msg.media_group_id + ) + + @staticmethod + def parse_user(user: TelegramUser) -> TelegramUserResponse: + return TelegramUserResponse( + user_id=user.id, + username=user.username, + is_bot=user.is_bot, + first_name=user.first_name, + last_name=user.last_name, + language_code=user.language_code, + link=user.link + ) + + @action + def send_message(self, chat_id: Union[str, int], text: str, parse_mode: Optional[str] = None, + disable_web_page_preview: bool = False, disable_notification: bool = False, + reply_to_message_id: Optional[int] = None) -> TelegramMessageResponse: + """ + Send a message to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param text: Text to be sent. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_web_page_preview: If True then web previews for URLs will be disabled. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + """ + + telegram = self.get_telegram() + msg = telegram.bot.send_message(chat_id=chat_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id) + + return self.parse_msg(msg) + + @action + def send_photo(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + caption: Optional[str] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a picture to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param caption: Optional caption for the picture. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_photo(chat_id=chat_id, + photo=resource, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_audio(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + caption: Optional[str] = None, + performer: Optional[str] = None, + title: Optional[str] = None, + duration: Optional[float] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send audio to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param caption: Optional caption for the picture. + :param performer: Optional audio performer. + :param title: Optional audio title. + :param duration: Duration of the audio in seconds. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_audio(chat_id=chat_id, + audio=resource, + caption=caption, + disable_notification=disable_notification, + performer=performer, + title=title, + duration=duration, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_document(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + filename: Optional[str] = None, + caption: Optional[str] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a document to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param filename: Name of the file as it will be shown in Telegram. + :param caption: Optional caption for the picture. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_document(chat_id=chat_id, + document=resource, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_video(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + duration: Optional[int] = None, + caption: Optional[str] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a video to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param duration: Duration in seconds. + :param caption: Optional caption for the picture. + :param width: Video width. + :param height: Video height. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_video(chat_id=chat_id, + video=resource, + duration=duration, + caption=caption, + width=width, + height=height, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_animation(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + duration: Optional[int] = None, + caption: Optional[str] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send an animation (GIF or H.264/MPEG-4 AVC video without sound) to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param duration: Duration in seconds. + :param caption: Optional caption for the picture. + :param width: Video width. + :param height: Video height. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_animation(chat_id=chat_id, + animation=resource, + duration=duration, + caption=caption, + width=width, + height=height, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout, + parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_voice(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + caption: Optional[str] = None, + duration: Optional[float] = None, + parse_mode: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send audio to a chat as a voice file. For this to work, your audio must be in an .ogg file encoded with OPUS + (other formats may be sent as Audio or Document). + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param caption: Optional caption for the picture. + :param duration: Duration of the voice in seconds. + :param parse_mode: Set to 'Markdown' or 'HTML' to send either Markdown or HTML content. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_voice(chat_id=chat_id, + voice=resource, + caption=caption, + disable_notification=disable_notification, + duration=duration, + reply_to_message_id=reply_to_message_id, + timeout=timeout, parse_mode=parse_mode) + + return self.parse_msg(msg) + + @action + def send_video_note(self, chat_id: Union[str, int], + file_id: Optional[int] = None, + url: Optional[str] = None, + path: Optional[str] = None, + duration: Optional[int] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a video note to a chat. As of v.4.0, Telegram clients support rounded square mp4 videos of up to + 1 minute long. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param file_id: Set it if the file already exists on Telegram servers and has a file_id. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param url: Set it if you want to send a file from a remote URL. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param path: Set it if you want to send a file from the local filesystem. + Note that you'll have to specify either ``file_id``, ``url`` or ``path``. + + :param duration: Duration in seconds. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(file_id=file_id, url=url, path=path) as resource: + msg = telegram.bot.send_video_note(chat_id=chat_id, + video=resource, + duration=duration, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout) + + return self.parse_msg(msg) + + @action + def send_location(self, chat_id: Union[str, int], + latitude: float, + longitude: float, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a location to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param latitude: Latitude + :param longitude: Longitude + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + msg = telegram.bot.send_location(chat_id=chat_id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout) + + return self.parse_msg(msg) + + @action + def send_venue(self, chat_id: Union[str, int], + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: Optional[str] = None, + foursquare_type: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send the address of a venue to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param latitude: Latitude + :param longitude: Longitude + :param title: Venue name. + :param address: Venue address. + :param foursquare_id: Foursquare ID. + :param foursquare_type: Foursquare type. + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + msg = telegram.bot.send_venue(chat_id=chat_id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + foursquare_type=foursquare_type, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout) + + return self.parse_msg(msg) + + @action + def send_contact(self, chat_id: Union[str, int], + phone_number: str, + first_name: str, + last_name: Optional[str] = None, + vcard: Optional[str] = None, + disable_notification: bool = False, + reply_to_message_id: Optional[int] = None, + timeout: int = 20) -> TelegramMessageResponse: + """ + Send a contact to a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param phone_number: Phone number. + :param first_name: First name. + :param last_name: Last name. + :param vcard: Additional contact info in vCard format (0-2048 bytes). + :param disable_notification: If True then no notification will be sent to the users. + :param reply_to_message_id: If set then the message will be sent as a response to the specified message. + :param timeout: Upload timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + msg = telegram.bot.send_contact(chat_id=chat_id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + vcard=vcard, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + timeout=timeout) + + return self.parse_msg(msg) + + @action + def get_file(self, file_id: str, timeout: int = 20) -> TelegramFileResponse: + """ + Get the info and URL of an uploaded file by file_id. + + :param file_id: File ID. + :param timeout: Upload timeout (default: 20 seconds). + """ + + telegram = self.get_telegram() + file = telegram.bot.get_file(file_id, timeout=timeout) + return TelegramFileResponse(file_id=file.file_id, file_path=file.file_path, file_size=file.file_size) + + @action + def get_chat(self, chat_id: Union[int, str], timeout: int = 20) -> TelegramChatResponse: + """ + Get the info about a Telegram chat. + + :param chat_id: Chat ID. + :param timeout: Upload timeout (default: 20 seconds). + """ + + telegram = self.get_telegram() + chat = telegram.bot.get_chat(chat_id, timeout=timeout) + return TelegramChatResponse(chat_id=chat.id, + link=chat.link, + username=chat.username, + invite_link=chat.invite_link, + title=chat.title, + description=chat.description, + type=chat.type, + first_name=chat.first_name, + last_name=chat.last_name) + + @action + def kick_chat_member(self, chat_id: Union[str, int], + user_id: int, + until_date: Optional[datetime.datetime] = None, + timeout: int = 20): + """ + Kick a user from a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param user_id: Unique user ID. + :param until_date: End date for the ban. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.kick_chat_member( + chat_id=chat_id, + user_id=user_id, + until_date=until_date, + timeout=timeout) + + @action + def unban_chat_member(self, chat_id: Union[str, int], + user_id: int, + timeout: int = 20): + """ + Lift the ban from a chat member. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param user_id: Unique user ID. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.unban_chat_member( + chat_id=chat_id, + user_id=user_id, + timeout=timeout) + + @action + def promote_chat_member(self, chat_id: Union[str, int], + user_id: int, + can_change_info: Optional[bool] = None, + can_post_messages: Optional[bool] = None, + can_edit_messages: Optional[bool] = None, + can_delete_messages: Optional[bool] = None, + can_invite_users: Optional[bool] = None, + can_restrict_members: Optional[bool] = None, + can_promote_members: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + timeout: int = 20): + """ + Promote or demote a member. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param user_id: Unique user ID. + :param can_change_info: Pass True if the user can change channel info. + :param can_post_messages: Pass True if the user can post messages. + :param can_edit_messages: Pass True if the user can edit messages. + :param can_delete_messages: Pass True if the user can delete messages. + :param can_invite_users: Pass True if the user can invite other users to the channel/group. + :param can_restrict_members: Pass True if the user can restrict the permissions of other users. + :param can_promote_members: Pass True if the user can promote mebmers. + :param can_pin_messages: Pass True if the user can pin messages. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.promote_chat_member( + chat_id=chat_id, + user_id=user_id, + can_change_info=can_change_info, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_invite_users=can_invite_users, + can_restrict_members=can_restrict_members, + can_promote_members=can_promote_members, + can_pin_messages=can_pin_messages, + timeout=timeout) + + @action + def set_chat_title(self, chat_id: Union[str, int], + title: str, + timeout: int = 20): + """ + Set the title of a channel/group. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param title: New chat title. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.set_chat_title( + chat_id=chat_id, + description=title, + timeout=timeout) + + @action + def set_chat_description(self, chat_id: Union[str, int], + description: str, + timeout: int = 20): + """ + Set the description of a channel/group. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param description: New chat description. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.set_chat_description( + chat_id=chat_id, + description=description, + timeout=timeout) + + @action + def set_chat_photo(self, chat_id: Union[str, int], + path: str, + timeout: int = 20): + """ + Set the photo of a channel/group. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param path: Path of the new image. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + + with Resource(path=path) as resource: + telegram.bot.set_chat_photo( + chat_id=chat_id, + photo=resource, + timeout=timeout) + + @action + def delete_chat_photo(self, chat_id: Union[str, int], + timeout: int = 20): + """ + Delete the photo of a channel/group. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.delete_chat_photo( + chat_id=chat_id, + timeout=timeout) + + @action + def pin_chat_message(self, chat_id: Union[str, int], + message_id: int, + disable_notification: Optional[bool] = None, + timeout: int = 20): + """ + Pin a message in a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param message_id: Message ID. + :param disable_notification: If True then no notification will be sent to the users. + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.pin_chat_message( + chat_id=chat_id, + message_id=message_id, + disable_notification=disable_notification, + timeout=timeout) + + @action + def unpin_chat_message(self, chat_id: Union[str, int], + timeout: int = 20): + """ + Unpin the message of a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.unpin_chat_message( + chat_id=chat_id, + timeout=timeout) + + @action + def leave_chat(self, chat_id: Union[str, int], + timeout: int = 20): + """ + Leave a chat. + + :param chat_id: Chat ID. Can be either a numerical ID or a unique identifier in the format ``@channelname``. + In order to get your own Telegram chat_id open a conversation with + `@IDBot `_ and type ``/start`` followed by ``/getid``. Similar procedures + also exist to get a group or channel chat_id - just Google for "Telegram get channel/group chat_id". + + :param timeout: Request timeout (default: 20 seconds) + """ + + telegram = self.get_telegram() + telegram.bot.leave_chat( + chat_id=chat_id, + timeout=timeout) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/http/request/__init__.py b/platypush/plugins/http/request/__init__.py index 3ee1bc75..b29b9840 100644 --- a/platypush/plugins/http/request/__init__.py +++ b/platypush/plugins/http/request/__init__.py @@ -1,3 +1,4 @@ +import os import requests from platypush.message import Message @@ -167,4 +168,19 @@ class HttpRequestPlugin(Plugin): return self._exec(method='options', url=url, **kwargs) + @action + def download(self, url: str, path: str, **kwargs): + """ + Locally download the content of a remote URL. + + :param url: URL to be downloaded. + :param path: Path where the content will be downloaded on the local filesystem - must be a file name. + """ + path = os.path.abspath(os.path.expanduser(path)) + content = self._exec(method='get', url=url, output='binary', **kwargs) + + with open(path, 'wb') as f: + f.write(content) + + # vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index 5a968e1d..c96a2c53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -201,3 +201,8 @@ croniter # Support for keyboard/mouse plugin # pyuserinput +# Support for Buienradar weather forecast +# buienradar + +# Support for Telegram integration +# python-telegram-bot diff --git a/setup.py b/setup.py index f00c6d23..c4fb7ae5 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ class WebBuildCommand(distutils.cmd.Command): @classmethod def generate_css_files(cls): try: + # noinspection PyPackageRequirements from scss import Compiler except ImportError: print('pyScss module not found: {}. You will have to generate ' + @@ -26,9 +27,9 @@ class WebBuildCommand(distutils.cmd.Command): return print('Building CSS files') - base_path = path(os.path.join('platypush','backend','http','static','css')) - input_path = path(os.path.join(base_path,'source')) - output_path = path(os.path.join(base_path,'dist')) + base_path = path(os.path.join('platypush', 'backend', 'http', 'static', 'css')) + input_path = path(os.path.join(base_path, 'source')) + output_path = path(os.path.join(base_path, 'dist')) for root, dirs, files in os.walk(input_path, followlinks=True): scss_file = os.path.join(root, 'index.scss') @@ -59,7 +60,6 @@ class WebBuildCommand(distutils.cmd.Command): self.generate_css_files() - class BuildCommand(build): def run(self): build.run(self) @@ -75,8 +75,10 @@ def readfile(fname): return f.read() +# noinspection PyShadowingBuiltins def pkg_files(dir): paths = [] + # noinspection PyShadowingNames for (path, dirs, files) in os.walk(dir): for file in files: paths.append(os.path.join('..', path, file)) @@ -84,6 +86,7 @@ def pkg_files(dir): def create_etc_dir(): + # noinspection PyShadowingNames path = '/etc/platypush' try: os.makedirs(path) @@ -101,40 +104,40 @@ backend = pkg_files('platypush/backend') # create_etc_dir() setup( - name = "platypush", - version = "0.11.2", - author = "Fabio Manganiello", - author_email = "info@fabiomanganiello.com", - description = ("Platypush service"), - license = "MIT", - python_requires = '>= 3.5', - keywords = "home-automation iot mqtt websockets redis dashboard notificaions", - url = "https://github.com/BlackLight/platypush", - packages = find_packages(), - include_package_data = True, - entry_points = { + name="platypush", + version="0.11.2", + author="Fabio Manganiello", + author_email="info@fabiomanganiello.com", + description="Platypush service", + license="MIT", + python_requires='>= 3.5', + keywords="home-automation iot mqtt websockets redis dashboard notificaions", + url="https://github.com/BlackLight/platypush", + packages=find_packages(), + include_package_data=True, + entry_points={ 'console_scripts': [ 'platypush=platypush:main', 'pusher=platypush.pusher:main', 'platydock=platypush.platydock:main', ], }, - scripts = ['bin/platyvenv'], - cmdclass = { + scripts=['bin/platyvenv'], + cmdclass={ 'web_build': WebBuildCommand, 'build': BuildCommand, }, # data_files = [ # ('/etc/platypush', ['platypush/config.example.yaml']) # ], - long_description = readfile('README.md'), - long_description_content_type = 'text/markdown', - classifiers = [ + long_description=readfile('README.md'), + long_description_content_type='text/markdown', + classifiers=[ "Topic :: Utilities", "License :: OSI Approved :: MIT License", "Development Status :: 3 - Alpha", ], - install_requires = [ + install_requires=[ 'pyyaml', 'redis', 'requests', @@ -143,7 +146,7 @@ setup( 'sqlalchemy', ], - extras_require = { + extras_require={ # Support for thread custom name 'threadname': ['python-prctl'], # Support for Kafka backend and plugin @@ -213,8 +216,8 @@ setup( 'sound': ['sounddevice', 'soundfile', 'numpy'], # Support for web media subtitles 'subtitles': [ - 'webvtt-py', - 'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master'], + 'webvtt-py', + 'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master'], # Support for mpv player plugin 'mpv': ['python-mpv'], # Support for NFC tags @@ -228,7 +231,7 @@ setup( # Support for LTR559 light/proximity sensor 'ltr559': ['ltr559'], # Support for VL53L1X laser ranger/distance sensor - 'vl53l1x': ['smbus2','vl53l1x'], + 'vl53l1x': ['smbus2', 'vl53l1x'], # Support for Dropbox integration 'dropbox': ['dropbox'], # Support for Leap Motion backend @@ -239,7 +242,7 @@ setup( 'alexa': ['avs @ https://github.com:BlackLight/avs/tarball/master'], # Support for bluetooth devices 'bluetooth': ['pybluez', 'gattlib', - 'pyobex @ https://github.com/BlackLight/PyOBEX'], + 'pyobex @ https://github.com/BlackLight/PyOBEX'], # Support for TP-Link devices 'tplink': ['pyHS100'], # Support for PWM3901 2-Dimensional Optical Flow Sensor @@ -260,6 +263,9 @@ setup( 'google-pubsub': ['google-cloud-pubsub'], # Support for keyboard/mouse plugin 'inputs': ['pyuserinput'], + # Support for Buienradar weather forecast + 'buienradar': ['buienradar'], + # Support for Telegram integration + 'telegram': ['python-telegram-bot'], }, ) -