├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── aiogram_broadcaster ├── __init__.py ├── base.py ├── exceptions.py ├── message_broadcaster.py ├── text_broadcaster.py └── types.py ├── examples ├── __init__.py ├── base_usage.py ├── broadcaster_args_example.py ├── broadcasting_handler.py ├── variables_example.py └── variables_example_2.py ├── poetry.lock └── pyproject.toml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install setuptools wheel poetry 22 | - name: Build and publish 23 | run: | 24 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} && 25 | poetry publish --build 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # Distribution / packaging 8 | .Python 9 | env/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | 27 | # virtualenv 28 | .venv 29 | venv/ 30 | ENV/ 31 | 32 | # IDE-specific files 33 | .idea/ 34 | .vscode/ 35 | 36 | # Development zone 37 | dev/ 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://img.shields.io/pypi/v/aiogram-broadcaster.svg)](https://pypi.org/project/aiogram-broadcaster/) 2 | [![Python](https://img.shields.io/badge/Python-3.7+-green)](https://www.python.org/downloads/) 3 | [![Aiogram](https://img.shields.io/badge/aiogram-2.11+-blue)](https://pypi.org/project/aiogram/) 4 | [![CI](https://github.com/F0rzend/aiogram_broadcaster/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/F0rzend/aiogram_broadcaster/actions/workflows/main.yml) 5 | 6 | # Aiogram Broadcaster 7 | 8 | A simple and straightforward broadcasting implementation for aiogram 9 | 10 | ## Installaiton 11 | 12 | $ pip install aiogram-broadcaster 13 | 14 | ## Examples 15 | 16 | **Few steps before getting started...** 17 | 18 | - First, you should obtain token for your bot from [BotFather](https://t.me/BotFather) 19 | and make sure you started a conversation with the bot. 20 | - Obtain your user id from [JSON Dump Bot](https://t.me/JsonDumpBot) in order to test out broadcaster. 21 | 22 | **Note:** These and even more examples can found in [`examples/`](https://github.com/fonco/aiogram_broadcaster/tree/main/examples) directory 23 | 24 | ### Base usage 25 | ```python 26 | from aiogram_broadcaster import TextBroadcaster 27 | 28 | import asyncio 29 | 30 | 31 | async def main(): 32 | 33 | # Initialize a text broadcaster (you can directly pass a token) 34 | broadcaster = TextBroadcaster('USERS IDS HERE', 'hello!', bot_token='BOT TOKEN HERE') 35 | 36 | # Run the broadcaster and close it afterwards 37 | try: 38 | await broadcaster.run() 39 | finally: 40 | await broadcaster.close_bot() 41 | 42 | 43 | if __name__ == '__main__': 44 | asyncio.run(main()) 45 | ``` 46 | 47 | ### Embed a broadcaster in a message handler 48 | ```python 49 | from aiogram import Bot, Dispatcher, types 50 | 51 | from aiogram_broadcaster import MessageBroadcaster 52 | 53 | import asyncio 54 | 55 | 56 | async def message_handler(msg: types.Message): 57 | """ 58 | The broadcaster will flood to a user whenever it receives a message 59 | """ 60 | 61 | users = [msg.from_user.id] * 5 # Your users list 62 | await MessageBroadcaster(users, msg).run() # Run the broadcaster 63 | 64 | 65 | async def main(): 66 | 67 | # Initialize a bot and a dispatcher 68 | bot = Bot(token='BOT TOKEN HERE') 69 | dp = Dispatcher(bot=bot) 70 | 71 | # Register a message handler 72 | dp.register_message_handler(message_handler, content_types=types.ContentTypes.ANY) 73 | 74 | # Run the bot and close it afterwards 75 | try: 76 | await dp.start_polling() 77 | finally: 78 | await bot.session.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | asyncio.run(main()) 83 | ``` 84 | 85 | -------------------------------------------------------------------------------- /aiogram_broadcaster/__init__.py: -------------------------------------------------------------------------------- 1 | from .message_broadcaster import MessageBroadcaster 2 | from .text_broadcaster import TextBroadcaster 3 | -------------------------------------------------------------------------------- /aiogram_broadcaster/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | import logging 4 | from typing import Dict, Optional, Tuple, List, Union 5 | 6 | from aiogram import Bot 7 | 8 | from .types import ChatsType, MarkupType, ChatIdType 9 | from .exceptions import RunningError 10 | 11 | 12 | class BaseBroadcaster(abc.ABC): 13 | running = [] 14 | 15 | def __init__( 16 | self, 17 | chats: ChatsType, 18 | kwargs: Optional[Dict] = None, 19 | disable_notification: Optional[bool] = None, 20 | disable_web_page_preview: Optional[bool] = None, 21 | reply_to_message_id: Optional[int] = None, 22 | allow_sending_without_reply: Optional[bool] = None, 23 | reply_markup: MarkupType = None, 24 | bot: Optional[Bot] = None, 25 | bot_token: Optional[str] = None, 26 | timeout: float = 0.05, 27 | logger=__name__, 28 | ): 29 | self._setup_chats(chats, kwargs) 30 | self.disable_notification = disable_notification 31 | self.disable_web_page_preview = disable_web_page_preview 32 | self.reply_to_message_id = reply_to_message_id 33 | self.allow_sending_without_reply = allow_sending_without_reply 34 | self.reply_markup = reply_markup 35 | self._setup_bot(bot, bot_token) 36 | self.timeout = timeout 37 | 38 | if not isinstance(logger, logging.Logger): 39 | logger = logging.getLogger(logger) 40 | 41 | self.logger = logger 42 | 43 | self._id: int = len(BaseBroadcaster.running) 44 | self._is_running: bool = False 45 | self._successful: List[Dict] = [] 46 | self._failure: List[Dict] = [] 47 | 48 | def __str__(self) -> str: 49 | attributes = [ 50 | ('id', self._id), 51 | ('is_running', self._is_running), 52 | ] 53 | if self._is_running: 54 | attributes.append(('progress', f'{len(self.successful)}/{len(self.chats)}')) 55 | attributes = '; '.join((f'{key}={str(value)}' for key, value in attributes)) 56 | return f'<{self.__class__.__name__}({attributes})>' 57 | 58 | @property 59 | def successful(self) -> List[Dict]: 60 | if not self._is_running: 61 | raise RunningError(self._is_running) 62 | else: 63 | return self._successful 64 | 65 | def get_successful(self, id_only: bool = False) -> Union[List[Dict], List[ChatIdType]]: 66 | if id_only: 67 | return [chat['chat_id'] for chat in self.successful] 68 | else: 69 | return self.successful 70 | 71 | @property 72 | def failure(self) -> List[Dict]: 73 | return self._failure 74 | 75 | def get_failure(self, id_only: bool = False) -> Union[List[Dict], List[ChatIdType]]: 76 | if id_only: 77 | return [chat['chat_id'] for chat in self.failure] 78 | else: 79 | return self.failure 80 | 81 | def _setup_bot( 82 | self, 83 | bot: Optional[Bot] = None, 84 | token: Optional[str] = None, 85 | ) -> Bot: 86 | if not (bot or token): 87 | bot = Bot.get_current() 88 | if bot: 89 | self.bot = bot 90 | else: 91 | raise AttributeError('You should either pass a bot instance or a token') 92 | if bot and token: 93 | raise AttributeError('You can’t pass both bot and token') 94 | if bot: 95 | self.bot = bot 96 | elif token: 97 | bot = Bot(token=token) 98 | self.bot = bot 99 | return bot 100 | 101 | def _setup_chats(self, chats: ChatsType, kwargs: Optional[Dict] = None) -> None: 102 | if not kwargs: 103 | kwargs = {} 104 | if isinstance(chats, int) or isinstance(chats, str): 105 | self.chats = [{'chat_id': chats, **kwargs}] 106 | elif isinstance(chats, list): 107 | if all([ 108 | isinstance(chat, int) or isinstance(chat, str) 109 | for chat in chats 110 | ]): 111 | self.chats = [ 112 | {'chat_id': chat, **kwargs} for chat in chats 113 | ] 114 | elif all([ 115 | isinstance(chat, dict) 116 | for chat in chats 117 | ]): 118 | if not all([chat.get('chat_id') for chat in chats]): 119 | raise ValueError('Not all dictionaries have the "chat_id" key') 120 | if not self._chek_identical_keys(dicts=chats): 121 | raise ValueError('Not all dictionaries have identical keys') 122 | self.chats = [ 123 | {'chat_id': chat.pop('chat_id'), **chat, **kwargs} 124 | for chat in chats if chat.get('chat_id', None) 125 | ] 126 | else: 127 | raise AttributeError(f'argument chats: expected {ChatsType}, got "{type(chats)}"') 128 | 129 | @staticmethod 130 | def _chek_identical_keys(dicts: List) -> bool: 131 | for d in dicts[1:]: 132 | if not sorted(d.keys()) == sorted(dicts[0].keys()): 133 | return False 134 | return True 135 | 136 | @staticmethod 137 | def _parse_args(chat: Dict) -> Tuple[ChatIdType, dict]: 138 | chat_id = chat.get('chat_id') 139 | text_args = chat 140 | return chat_id, text_args 141 | 142 | @abc.abstractmethod 143 | async def send(self, chat_id: ChatIdType, chat_args: dict) -> bool: 144 | pass 145 | 146 | def _change_running_status(self, run: bool) -> None: 147 | self._is_running = run 148 | if run: 149 | BaseBroadcaster.running.append(self) 150 | else: 151 | BaseBroadcaster.running.remove(self) 152 | 153 | async def _start_broadcast(self) -> None: 154 | for chat in self.chats: 155 | logging.info(str(self)) 156 | chat_id, chat_args = self._parse_args(chat) 157 | if await self.send(chat_id=chat_id, chat_args=chat_args): 158 | self._successful.append(chat) 159 | else: 160 | self._failure.append(chat) 161 | await asyncio.sleep(self.timeout) 162 | 163 | async def run(self) -> None: 164 | self._change_running_status(True) 165 | await self._start_broadcast() 166 | self._change_running_status(False) 167 | logging.info(f'{len(self._successful)}/{len(self.chats)} messages were sent out') 168 | 169 | async def close_bot(self) -> None: 170 | logging.warning('GOODBYE') 171 | await self.bot.session.close() 172 | -------------------------------------------------------------------------------- /aiogram_broadcaster/exceptions.py: -------------------------------------------------------------------------------- 1 | class RunningError(RuntimeError): 2 | def __init__(self, is_running: bool = False): 3 | if not is_running: 4 | message = 'The broadcast is not running' 5 | else: 6 | message = 'The broadcast is already running' 7 | 8 | super().__init__(message) 9 | -------------------------------------------------------------------------------- /aiogram_broadcaster/message_broadcaster.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from copy import deepcopy 3 | from string import Template 4 | from typing import Dict, Optional 5 | 6 | from aiogram import Bot 7 | from aiogram.types import Message, ParseMode 8 | from aiogram.utils import exceptions 9 | 10 | from .types import ChatsType, MarkupType, ChatIdType 11 | from .base import BaseBroadcaster 12 | 13 | 14 | class MessageBroadcaster(BaseBroadcaster): 15 | def __init__( 16 | self, 17 | chats: ChatsType, 18 | message: Message, 19 | kwargs: Optional[Dict] = None, 20 | disable_notification: Optional[bool] = None, 21 | reply_to_message_id: Optional[int] = None, 22 | allow_sending_without_reply: Optional[bool] = None, 23 | reply_markup: MarkupType = None, 24 | bot: Optional[Bot] = None, 25 | bot_token: Optional[str] = None, 26 | timeout: float = 0.02, 27 | logger=__name__ 28 | ): 29 | super().__init__( 30 | chats=chats, 31 | kwargs=kwargs, 32 | disable_notification=disable_notification, 33 | reply_to_message_id=reply_to_message_id, 34 | allow_sending_without_reply=allow_sending_without_reply, 35 | reply_markup=reply_markup, 36 | bot=bot, 37 | bot_token=bot_token, 38 | timeout=timeout, 39 | logger=logger, 40 | ) 41 | self.message = message 42 | 43 | @staticmethod 44 | async def send_copy( 45 | message: Message, 46 | chat_id: ChatIdType, 47 | disable_notification: Optional[bool] = None, 48 | disable_web_page_preview: Optional[bool] = None, 49 | reply_to_message_id: Optional[int] = None, 50 | allow_sending_without_reply: Optional[bool] = None, 51 | reply_markup: MarkupType = None, 52 | ) -> Message: 53 | kwargs = { 54 | "chat_id": chat_id, 55 | "allow_sending_without_reply": allow_sending_without_reply, 56 | "reply_markup": reply_markup or message.reply_markup, 57 | "parse_mode": ParseMode.HTML, 58 | "disable_notification": disable_notification, 59 | "reply_to_message_id": reply_to_message_id, 60 | } 61 | if message.caption: 62 | text = message.caption 63 | elif message.text: 64 | text = message.text 65 | else: 66 | text = None 67 | 68 | if message.text: 69 | kwargs["disable_web_page_preview"] = disable_web_page_preview 70 | return await message.bot.send_message(text=text, **kwargs) 71 | elif message.audio: 72 | return await message.bot.send_audio( 73 | audio=message.audio.file_id, 74 | caption=text, 75 | title=message.audio.title, 76 | performer=message.audio.performer, 77 | duration=message.audio.duration, 78 | **kwargs, 79 | ) 80 | elif message.animation: 81 | return await message.bot.send_animation( 82 | animation=message.animation.file_id, caption=text, **kwargs 83 | ) 84 | elif message.document: 85 | return await message.bot.send_document( 86 | document=message.document.file_id, caption=text, **kwargs 87 | ) 88 | elif message.photo: 89 | return await message.bot.send_photo( 90 | photo=message.photo[-1].file_id, caption=text, **kwargs 91 | ) 92 | elif message.sticker: 93 | kwargs.pop("parse_mode") 94 | return await message.bot.send_sticker(sticker=message.sticker.file_id, **kwargs) 95 | elif message.video: 96 | return await message.bot.send_video( 97 | video=message.video.file_id, caption=text, **kwargs 98 | ) 99 | elif message.video_note: 100 | kwargs.pop("parse_mode") 101 | return await message.bot.send_video_note( 102 | video_note=message.video_note.file_id, **kwargs 103 | ) 104 | elif message.voice: 105 | return await message.bot.send_voice( 106 | voice=message.voice.file_id, 107 | caption=text, 108 | **kwargs, 109 | ) 110 | elif message.contact: 111 | kwargs.pop("parse_mode") 112 | return await message.bot.send_contact( 113 | phone_number=message.contact.phone_number, 114 | first_name=message.contact.first_name, 115 | last_name=message.contact.last_name, 116 | vcard=message.contact.vcard, 117 | **kwargs, 118 | ) 119 | elif message.venue: 120 | kwargs.pop("parse_mode") 121 | return await message.bot.send_venue( 122 | latitude=message.venue.location.latitude, 123 | longitude=message.venue.location.longitude, 124 | title=message.venue.title, 125 | address=message.venue.address, 126 | foursquare_id=message.venue.foursquare_id, 127 | foursquare_type=message.venue.foursquare_type, 128 | **kwargs, 129 | ) 130 | elif message.location: 131 | kwargs.pop("parse_mode") 132 | return await message.bot.send_location( 133 | latitude=message.location.latitude, 134 | longitude=message.location.longitude, 135 | **kwargs, 136 | ) 137 | elif message.poll: 138 | kwargs.pop("parse_mode") 139 | return await message.bot.send_poll( 140 | question=message.poll.question, 141 | options=[option.text for option in message.poll.options], 142 | is_anonymous=message.poll.is_anonymous, 143 | allows_multiple_answers=message.poll.allows_multiple_answers, 144 | **kwargs, 145 | ) 146 | elif message.dice: 147 | kwargs.pop("parse_mode") 148 | return await message.bot.send_dice( 149 | emoji=message.dice.emoji, 150 | **kwargs, 151 | ) 152 | else: 153 | raise TypeError("This type of message can't be copied.") 154 | 155 | @staticmethod 156 | def get_updated_message(message: Message, text_args: dict) -> Message: 157 | msg = deepcopy(message) 158 | text = Template(msg.html_text).safe_substitute(text_args) if (msg.text or msg.caption) else None 159 | if msg.caption: 160 | msg.caption = text 161 | elif msg.text: 162 | msg.text = text 163 | else: 164 | return message 165 | 166 | return msg 167 | 168 | async def send( 169 | self, 170 | chat_id: ChatIdType, 171 | chat_args: Dict, 172 | ) -> bool: 173 | try: 174 | msg = self.get_updated_message(self.message, chat_args) 175 | await self.send_copy( 176 | message=msg, 177 | chat_id=chat_id, 178 | disable_notification=self.disable_notification, 179 | disable_web_page_preview=self.disable_web_page_preview, 180 | reply_to_message_id=self.reply_to_message_id, 181 | allow_sending_without_reply=self.allow_sending_without_reply, 182 | reply_markup=self.reply_markup, 183 | ) 184 | except exceptions.RetryAfter as e: 185 | self.logger.debug( 186 | f"Target [ID:{chat_id}]: Flood limit is exceeded. Sleep {e.timeout} seconds." 187 | ) 188 | await sleep(e.timeout) 189 | return await self.send(chat_id, chat_args) # Recursive call 190 | except ( 191 | exceptions.BotBlocked, 192 | exceptions.ChatNotFound, 193 | exceptions.UserDeactivated, 194 | exceptions.ChatNotFound 195 | ) as e: 196 | self.logger.debug(f"Target [ID:{chat_id}]: {e.match}") 197 | except exceptions.TelegramAPIError: 198 | self.logger.exception(f"Target [ID:{chat_id}]: failed") 199 | else: 200 | self.logger.debug(f"Target [ID:{chat_id}]: success") 201 | return True 202 | return False 203 | -------------------------------------------------------------------------------- /aiogram_broadcaster/text_broadcaster.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from string import Template 3 | from typing import Dict, Optional, Union 4 | 5 | from aiogram import Bot 6 | from aiogram.utils import exceptions 7 | 8 | from .types import ChatsType, MarkupType, TextType, ChatIdType 9 | from .base import BaseBroadcaster 10 | 11 | 12 | class TextBroadcaster(BaseBroadcaster): 13 | def __init__( 14 | self, 15 | chats: ChatsType, 16 | text: TextType, 17 | kwargs: Optional[Dict] = None, 18 | parse_mode: Optional[str] = None, 19 | disable_web_page_preview: Optional[bool] = None, 20 | disable_notification: Optional[bool] = None, 21 | reply_to_message_id: Optional[int] = None, 22 | allow_sending_without_reply: Optional[bool] = None, 23 | reply_markup: MarkupType = None, 24 | bot: Optional[Bot] = None, 25 | bot_token: Optional[str] = None, 26 | timeout: float = 0.02, 27 | logger=__name__, 28 | ): 29 | super().__init__( 30 | chats=chats, 31 | kwargs=kwargs, 32 | disable_notification=disable_notification, 33 | reply_to_message_id=reply_to_message_id, 34 | allow_sending_without_reply=allow_sending_without_reply, 35 | reply_markup=reply_markup, 36 | bot=bot, 37 | bot_token=bot_token, 38 | timeout=timeout, 39 | logger=logger, 40 | ) 41 | self.text = Template(text) if isinstance(text, str) else text 42 | self.parse_mode = parse_mode 43 | self.disable_web_page_preview = disable_web_page_preview 44 | 45 | def get_text(self, as_str: bool = True) -> Union[str, Template]: 46 | if as_str: 47 | return self.text if isinstance(self.text, str) else self.text.template 48 | else: 49 | return self.text 50 | 51 | async def send( 52 | self, 53 | chat_id: ChatIdType, 54 | chat_args: Dict, 55 | ) -> bool: 56 | try: 57 | await self.bot.send_message( 58 | chat_id=chat_id, 59 | text=self.text.safe_substitute(chat_args), 60 | parse_mode=self.parse_mode, 61 | disable_web_page_preview=self.disable_web_page_preview, 62 | disable_notification=self.disable_notification, 63 | reply_to_message_id=self.reply_to_message_id, 64 | allow_sending_without_reply=self.allow_sending_without_reply, 65 | reply_markup=self.reply_markup, 66 | ) 67 | except exceptions.RetryAfter as e: 68 | self.logger.debug( 69 | f"Target [ID:{chat_id}]: Flood limit is exceeded. Sleep {e.timeout} seconds." 70 | ) 71 | await sleep(e.timeout) 72 | return await self.send(chat_id, chat_args) # Recursive call 73 | except ( 74 | exceptions.BotBlocked, 75 | exceptions.ChatNotFound, 76 | exceptions.UserDeactivated, 77 | exceptions.ChatNotFound 78 | ) as e: 79 | self.logger.debug(f"Target [ID:{chat_id}]: {e.match}") 80 | except exceptions.TelegramAPIError: 81 | self.logger.exception(f"Target [ID:{chat_id}]: failed") 82 | else: 83 | self.logger.debug(f"Target [ID:{chat_id}]: success") 84 | return True 85 | return False 86 | -------------------------------------------------------------------------------- /aiogram_broadcaster/types.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | from typing import Dict, List, Union 3 | 4 | from aiogram.types import (ForceReply, InlineKeyboardMarkup, 5 | ReplyKeyboardMarkup, ReplyKeyboardRemove) 6 | 7 | ChatIdType = Union[int, str] 8 | ChatsType = Union[Union[List[ChatIdType], ChatIdType], List[Dict]] 9 | TextType = Union[Template, str] 10 | MarkupType = Union[ 11 | InlineKeyboardMarkup, 12 | ReplyKeyboardMarkup, 13 | ReplyKeyboardRemove, 14 | ForceReply, 15 | None, 16 | ] 17 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/aiogram_broadcaster/b84c3ec2ba267b1e38ef3956508f5288b7902379/examples/__init__.py -------------------------------------------------------------------------------- /examples/base_usage.py: -------------------------------------------------------------------------------- 1 | from aiogram_broadcaster import TextBroadcaster 2 | 3 | import asyncio 4 | import logging 5 | 6 | 7 | async def main(): 8 | broadcaster = TextBroadcaster('USERS IDS HERE', 'hello!', bot_token='BOT TOKEN HERE') 9 | try: 10 | await broadcaster.run() 11 | finally: 12 | await broadcaster.close_bot() 13 | 14 | 15 | if __name__ == '__main__': 16 | logging.basicConfig(level='DEBUG') 17 | asyncio.run(main()) 18 | -------------------------------------------------------------------------------- /examples/broadcaster_args_example.py: -------------------------------------------------------------------------------- 1 | from aiogram_broadcaster import TextBroadcaster 2 | 3 | import asyncio 4 | import logging 5 | 6 | 7 | async def main(): 8 | """ 9 | You can pass arguments to chats. 10 | Thus, you don’t need to generate a list on your own 11 | """ 12 | broadcaster = TextBroadcaster( 13 | 'USERS IDS HERE', 14 | 'Hello, we are $service\nYour id: $chat_id', 15 | kwargs=dict(service='fonco'), # You can pass arguments, that will be used in all chats 16 | bot_token='BOT TOKEN HERE' 17 | ) 18 | try: 19 | await broadcaster.run() 20 | finally: 21 | await broadcaster.close_bot() 22 | 23 | 24 | if __name__ == '__main__': 25 | logging.basicConfig(level='DEBUG') 26 | asyncio.run(main()) 27 | -------------------------------------------------------------------------------- /examples/broadcasting_handler.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | 3 | from aiogram_broadcaster import MessageBroadcaster 4 | 5 | import asyncio 6 | import logging 7 | 8 | 9 | async def message_handler(msg: types.Message): 10 | """ 11 | The broadcaster will flood to a user whenever it receives a message 12 | """ 13 | users = [msg.from_user.id] * 5 # Your users list 14 | await MessageBroadcaster(users, msg).run() # run mailing 15 | 16 | 17 | async def main(): 18 | bot = Bot(token='BOT TOKEN HERE') 19 | dp = Dispatcher(bot=bot) 20 | 21 | dp.register_message_handler(message_handler, content_types=types.ContentTypes.ANY) 22 | try: 23 | await dp.start_polling() 24 | finally: 25 | await bot.session.close() 26 | 27 | 28 | if __name__ == '__main__': 29 | logging.basicConfig(level='DEBUG') 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /examples/variables_example.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | 3 | from aiogram_broadcaster import TextBroadcaster 4 | 5 | import asyncio 6 | import logging 7 | 8 | 9 | async def message_handler(msg: types.Message): 10 | """ 11 | On any msg bot will be return chat_id to user 12 | """ 13 | users = [msg.from_user.id] 14 | await TextBroadcaster(users, text="U'r id: $chat_id").run() # run mailing 15 | 16 | 17 | async def main(): 18 | bot = Bot(token='BOT TOKEN HERE') 19 | dp = Dispatcher(bot=bot) 20 | 21 | dp.register_message_handler(message_handler, content_types=types.ContentTypes.ANY) 22 | try: 23 | await dp.start_polling() 24 | finally: 25 | await bot.session.close() 26 | 27 | 28 | if __name__ == '__main__': 29 | logging.basicConfig(level='DEBUG') 30 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/variables_example_2.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | 3 | from aiogram_broadcaster import MessageBroadcaster 4 | 5 | import asyncio 6 | import logging 7 | 8 | 9 | async def message_handler(msg: types.Message): 10 | """ 11 | On any msg bot will be return chat_id to user 12 | """ 13 | chats = [ 14 | dict( 15 | chat_id=msg.from_user.id, 16 | mention=msg.from_user.get_mention(as_html=True) 17 | ) 18 | 19 | ] 20 | await MessageBroadcaster(chats, message=msg).run() # run mailing 21 | 22 | 23 | async def main(): 24 | bot = Bot(token='BOT TOKEN HERE') 25 | dp = Dispatcher(bot=bot) 26 | 27 | dp.register_message_handler(message_handler, content_types=types.ContentTypes.ANY) 28 | try: 29 | await dp.start_polling() 30 | finally: 31 | await bot.session.close() 32 | 33 | 34 | if __name__ == '__main__': 35 | logging.basicConfig(level='DEBUG') 36 | asyncio.run(main()) 37 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiogram" 3 | version = "2.13" 4 | description = "Is a pretty simple and fully asynchronous framework for Telegram Bot API" 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [package.dependencies] 10 | aiohttp = ">=3.7.2,<4.0.0" 11 | Babel = ">=2.8.0" 12 | certifi = ">=2020.6.20" 13 | 14 | [package.extras] 15 | fast = ["uvloop (>=0.14.0,<0.15.0)", "ujson (>=1.35)"] 16 | proxy = ["aiohttp-socks (>=0.5.3,<0.6.0)"] 17 | 18 | [[package]] 19 | name = "aiohttp" 20 | version = "3.7.4.post0" 21 | description = "Async http client/server framework (asyncio)" 22 | category = "main" 23 | optional = false 24 | python-versions = ">=3.6" 25 | 26 | [package.dependencies] 27 | async-timeout = ">=3.0,<4.0" 28 | attrs = ">=17.3.0" 29 | chardet = ">=2.0,<5.0" 30 | multidict = ">=4.5,<7.0" 31 | typing-extensions = ">=3.6.5" 32 | yarl = ">=1.0,<2.0" 33 | 34 | [package.extras] 35 | speedups = ["aiodns", "brotlipy", "cchardet"] 36 | 37 | [[package]] 38 | name = "async-timeout" 39 | version = "3.0.1" 40 | description = "Timeout context manager for asyncio programs" 41 | category = "main" 42 | optional = false 43 | python-versions = ">=3.5.3" 44 | 45 | [[package]] 46 | name = "attrs" 47 | version = "21.2.0" 48 | description = "Classes Without Boilerplate" 49 | category = "main" 50 | optional = false 51 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 52 | 53 | [package.extras] 54 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 55 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 56 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 57 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 58 | 59 | [[package]] 60 | name = "babel" 61 | version = "2.9.1" 62 | description = "Internationalization utilities" 63 | category = "main" 64 | optional = false 65 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 66 | 67 | [package.dependencies] 68 | pytz = ">=2015.7" 69 | 70 | [[package]] 71 | name = "certifi" 72 | version = "2020.12.5" 73 | description = "Python package for providing Mozilla's CA Bundle." 74 | category = "main" 75 | optional = false 76 | python-versions = "*" 77 | 78 | [[package]] 79 | name = "chardet" 80 | version = "4.0.0" 81 | description = "Universal encoding detector for Python 2 and 3" 82 | category = "main" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [[package]] 87 | name = "idna" 88 | version = "3.1" 89 | description = "Internationalized Domain Names in Applications (IDNA)" 90 | category = "main" 91 | optional = false 92 | python-versions = ">=3.4" 93 | 94 | [[package]] 95 | name = "multidict" 96 | version = "5.1.0" 97 | description = "multidict implementation" 98 | category = "main" 99 | optional = false 100 | python-versions = ">=3.6" 101 | 102 | [[package]] 103 | name = "pytz" 104 | version = "2021.1" 105 | description = "World timezone definitions, modern and historical" 106 | category = "main" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [[package]] 111 | name = "typing-extensions" 112 | version = "3.10.0.0" 113 | description = "Backported and Experimental Type Hints for Python 3.5+" 114 | category = "main" 115 | optional = false 116 | python-versions = "*" 117 | 118 | [[package]] 119 | name = "yarl" 120 | version = "1.6.3" 121 | description = "Yet another URL library" 122 | category = "main" 123 | optional = false 124 | python-versions = ">=3.6" 125 | 126 | [package.dependencies] 127 | idna = ">=2.0" 128 | multidict = ">=4.0" 129 | typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 130 | 131 | [metadata] 132 | lock-version = "1.1" 133 | python-versions = "^3.7" 134 | content-hash = "f766d251b30eaa4c3f13da082a4589b390f9e61ecdbd3c9d164aa01470730d4c" 135 | 136 | [metadata.files] 137 | aiogram = [ 138 | {file = "aiogram-2.13-py3-none-any.whl", hash = "sha256:e1923ad789bb8d90a7b1edc5fce7dc33df982d1fedc6528c65cc38d2d88f1ae0"}, 139 | {file = "aiogram-2.13.tar.gz", hash = "sha256:f656f57580fd8e1ad0f8fe645f59cf3ad40e9899050dd6ac999a8f0cdbf1b116"}, 140 | ] 141 | aiohttp = [ 142 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, 143 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, 144 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, 145 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, 146 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, 147 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, 148 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, 149 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, 150 | {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, 151 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, 152 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, 153 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, 154 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, 155 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, 156 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, 157 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, 158 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, 159 | {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, 160 | {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, 161 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, 162 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, 163 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, 164 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, 165 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, 166 | {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, 167 | {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, 168 | {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, 169 | {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, 170 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, 171 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, 172 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, 173 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, 174 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, 175 | {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, 176 | {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, 177 | {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, 178 | {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, 179 | ] 180 | async-timeout = [ 181 | {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, 182 | {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, 183 | ] 184 | attrs = [ 185 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 186 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 187 | ] 188 | babel = [ 189 | {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, 190 | {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, 191 | ] 192 | certifi = [ 193 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 194 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 195 | ] 196 | chardet = [ 197 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 198 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 199 | ] 200 | idna = [ 201 | {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, 202 | {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"}, 203 | ] 204 | multidict = [ 205 | {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, 206 | {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, 207 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, 208 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, 209 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, 210 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, 211 | {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, 212 | {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, 213 | {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, 214 | {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, 215 | {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, 216 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, 217 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, 218 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, 219 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, 220 | {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, 221 | {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, 222 | {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, 223 | {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, 224 | {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, 225 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, 226 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, 227 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, 228 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, 229 | {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, 230 | {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, 231 | {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, 232 | {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, 233 | {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, 234 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, 235 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, 236 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, 237 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, 238 | {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, 239 | {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, 240 | {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, 241 | {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, 242 | ] 243 | pytz = [ 244 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 245 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 246 | ] 247 | typing-extensions = [ 248 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 249 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 250 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 251 | ] 252 | yarl = [ 253 | {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, 254 | {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, 255 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, 256 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, 257 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, 258 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, 259 | {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, 260 | {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, 261 | {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, 262 | {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, 263 | {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, 264 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, 265 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, 266 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, 267 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, 268 | {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, 269 | {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, 270 | {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, 271 | {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, 272 | {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, 273 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, 274 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, 275 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, 276 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, 277 | {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, 278 | {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, 279 | {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, 280 | {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, 281 | {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, 282 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, 283 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, 284 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, 285 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, 286 | {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, 287 | {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, 288 | {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, 289 | {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, 290 | ] 291 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aiogram_broadcaster" 3 | version = "0.0.7" 4 | description = "Simple and lightweight library based on aiogram for creating telegram mailings" 5 | authors = ["F0rzend"] 6 | readme = "README.md" 7 | repository = "https://github.com/fonco/aiogram-broadcaster/" 8 | keywords = [ 9 | "telegram", 10 | "bot", 11 | "api", 12 | "asyncio", 13 | "aiogram", 14 | "broadcaser", 15 | "broadcast", 16 | "mailing", 17 | ] 18 | 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.7" 22 | aiogram = "^2.11" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | --------------------------------------------------------------------------------