├── .gitignore ├── LICENSE ├── README.md ├── TelegramBot ├── __init__.py ├── logger.py ├── plugin.py └── service.py ├── makefile ├── plugins ├── __init__.py ├── ping.py └── timer.py ├── setup.cfg ├── setup.py └── setup_env.sh /.gitignore: -------------------------------------------------------------------------------- 1 | tests/env.py 2 | *.pyc 3 | .idea 4 | .DS_Store 5 | .coverage 6 | _trial_temp* 7 | virtualenv/ 8 | htmlcov/* 9 | *.egg-info 10 | log 11 | twisted/plugins/dropin.cache 12 | */__pycache__/* 13 | dist/ 14 | TelegramBotAPI 15 | config.ini 16 | pyplugin 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Source Simian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot Service (unofficial) in asyncio Python 3 2 | 3 | A customisable bot for the [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3 4 | using [asyncio](https://docs.python.org/3/library/asyncio.html). 5 | 6 | Uses: 7 | * [TelegramBotAPI](https://github.com/sourcesimian/pyTelegramBotAPI): An implementation of the Telegram Bot API messages and some simple clients. 8 | * [pyPlugin](https://github.com/sourcesimian/pyPlugin): Simple framework-less plugin loader for Python. 9 | 10 | ## Installation 11 | ``` 12 | pip3 install aioTelegramBot 13 | ``` 14 | 15 | ## Usage 16 | * Create a ```config.ini``` as follows, and set it up with your 17 | [Telegram Bot API token](https://core.telegram.org/bots/api#authorizing-your-bot), etc: 18 | ``` 19 | [telegrambot] 20 | # Your Telegram Bot API token 21 | token = 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 22 | 23 | [proxy] 24 | # address = www.example.com:8080 25 | 26 | [message_plugins] 27 | 1 = plugins/*.py 28 | # 2 = other/plugins/foo.py 29 | 30 | [env] 31 | # Set any additional environment variables your plugins may need 32 | BOT_NAME = txTelegramBot 33 | # FOO = bar 34 | ``` 35 | 36 | * Write a plugin using the following template and add it in the \[message_plugins\] section above: 37 | ``` 38 | import asyncio 39 | 40 | from TelegramBotAPI.types import sendMessage 41 | from TelegramBot.plugin import BotPlugin 42 | 43 | 44 | class Ping(BotPlugin): 45 | 46 | @asyncio.coroutine 47 | def startPlugin(self): 48 | pass 49 | 50 | @asyncio.coroutine 51 | def stopPlugin(self): 52 | pass 53 | 54 | @asyncio.coroutine 55 | def on_message(self, msg): 56 | 57 | if hasattr(msg, 'text'): 58 | m = sendMessage() 59 | m.chat_id = msg.chat.id 60 | if msg.text == 'Hello': 61 | m.text = 'Hello my name is %s' % os.environ['BOT_NAME'] 62 | else: 63 | m.text = 'You said: "%s"' % msg.text 64 | 65 | rsp = yield from self.send_method(m) 66 | return True 67 | ``` 68 | 69 | * Run the bot: 70 | ``` 71 | $ python3 -m TelegramBot.service ./config.ini 72 | ``` 73 | -------------------------------------------------------------------------------- /TelegramBot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcesimian/aioTelegramBot/66b56a3286c57d00a3ac369de0bfbcadf177aece/TelegramBot/__init__.py -------------------------------------------------------------------------------- /TelegramBot/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def basic_logging(): 6 | root = logging.getLogger() 7 | root.setLevel(logging.DEBUG) 8 | 9 | ch = logging.StreamHandler(sys.stdout) 10 | ch.setLevel(logging.DEBUG) 11 | formatter = logging.Formatter('%(asctime)s [%(name)s] %(levelname)s: %(message)s') 12 | ch.setFormatter(formatter) 13 | root.addHandler(ch) 14 | -------------------------------------------------------------------------------- /TelegramBot/plugin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class BotPlugin(object): 5 | priority = 100 6 | 7 | def __init__(self, cb_send_method): 8 | self.__cb_send_method = cb_send_method 9 | 10 | @asyncio.coroutine 11 | def startPlugin(self): 12 | pass 13 | 14 | @asyncio.coroutine 15 | def stopPlugin(self): 16 | pass 17 | 18 | @asyncio.coroutine 19 | def on_message(self, msg): 20 | pass 21 | 22 | @asyncio.coroutine 23 | def send_method(self, method): 24 | result = yield from self.__cb_send_method(method) 25 | return result 26 | -------------------------------------------------------------------------------- /TelegramBot/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import logging 5 | import os 6 | 7 | from configparser import ConfigParser 8 | import argparse 9 | 10 | from pyplugin import PluginLoader 11 | 12 | from TelegramBotAPI.client.asyncioclient import AsyncioClient 13 | from TelegramBotAPI.types.methods import getUpdates 14 | 15 | from TelegramBot.logger import basic_logging 16 | from TelegramBot.plugin import BotPlugin 17 | 18 | basic_logging() 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class TelegramBotService(object): 23 | 24 | def __init__(self, token, plugin_filespec, proxy=None, debug=False): 25 | self._update_id = 0 26 | self._client = AsyncioClient(token, proxy=proxy, debug=debug) 27 | 28 | self._plugins = [p(self._send_method) for p in PluginLoader(BotPlugin, plugin_filespec)] 29 | 30 | self._plugins.sort(key=lambda p: p.priority) 31 | 32 | @asyncio.coroutine 33 | def run(self): 34 | for plugin in self._plugins: 35 | yield from plugin.startPlugin() 36 | 37 | try: 38 | while True: 39 | method = getUpdates() 40 | method.offset = self._update_id 41 | method.timeout = 30 42 | updates = yield from self._client.send_method(method) 43 | 44 | for update in updates: 45 | yield from self._on_update(update) 46 | self._update_id = update.update_id + 1 47 | # if not len(updates): 48 | # yield from asyncio.sleep(10) 49 | 50 | except KeyboardInterrupt: 51 | log.info('Stopping ...') 52 | 53 | for plugin in self._plugins: 54 | yield from plugin.stopPlugin() 55 | 56 | @asyncio.coroutine 57 | def _on_update(self, update): 58 | for plugin in self._plugins: 59 | handled = yield from plugin.on_message(update.message) 60 | if handled: 61 | break 62 | 63 | @asyncio.coroutine 64 | def _send_method(self, method): 65 | result = yield from self._client.send_method(method) 66 | return result 67 | 68 | 69 | def main(): 70 | parser = argparse.ArgumentParser() 71 | parser.add_argument('config', help='INI config file') 72 | args = parser.parse_args() 73 | 74 | config = ConfigParser() 75 | config.optionxform = str 76 | config.read(args.config) 77 | 78 | token = config['telegrambot']['token'] 79 | 80 | proxy = config['proxy'].get('address', None) 81 | if proxy: 82 | os.environ['http_proxy'] = 'http://%s' % proxy 83 | os.environ['https_proxy'] = 'https://%s' % proxy 84 | 85 | msg_plugins = [v for v in config['message_plugins'].values()] 86 | 87 | for key, value in config['env'].items(): 88 | os.environ[key] = value 89 | 90 | service = TelegramBotService(token, msg_plugins, proxy, debug=True) 91 | 92 | asyncio.get_event_loop().run_until_complete(service.run()) 93 | 94 | 95 | if __name__ == "__main__": 96 | exit(main()) 97 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | trial tests/test_*.py 3 | 4 | 5 | run: 6 | python3 -m TelegramBot.service ./config.ini 7 | 8 | 9 | coverage: 10 | coverage run tests/test_*.py 11 | coverage html 12 | open htmlcov/index.html 13 | 14 | 15 | develop: 16 | ./setup_env.sh 17 | 18 | 19 | clean: 20 | rm -rf build 21 | rm -rf _trial* 22 | rm -rf htmlcov 23 | rm -rf *.egg-info 24 | 25 | 26 | analyse: 27 | find TelegramBot -name '*.py' | xargs pep8 --ignore E501 28 | find TelegramBot -name '*.py' | xargs pyflakes 29 | find TelegramBot -name '*.py' | xargs pylint -d invalid-name -d locally-disabled -d missing-docstring -d too-few-public-methods -d protected-access 30 | 31 | 32 | to_pypi_test: 33 | python setup.py register -r pypitest 34 | python setup.py sdist upload -r pypitest 35 | 36 | 37 | to_pypi: 38 | python setup.py register -r pypi 39 | python setup.py sdist upload -r pypi 40 | 41 | 42 | from_pypi_test: 43 | pip install --extra-index-url https://testpypi.python.org/pypi txTelegramBot 44 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcesimian/aioTelegramBot/66b56a3286c57d00a3ac369de0bfbcadf177aece/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/ping.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from TelegramBotAPI.types import sendMessage, sendPhoto 6 | 7 | from TelegramBot.plugin import BotPlugin 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Ping(BotPlugin): 13 | 14 | @asyncio.coroutine 15 | def startPlugin(self): 16 | pass 17 | 18 | @asyncio.coroutine 19 | def on_message(self, msg): 20 | 21 | if hasattr(msg, 'text'): 22 | m = sendMessage() 23 | m.chat_id = msg.chat.id 24 | if msg.text == 'Hello': 25 | m.text = 'Hello my name is %s' % os.environ['BOT_NAME'] 26 | else: 27 | m.text = 'You said: "%s"' % msg.text 28 | elif hasattr(msg, 'photo'): 29 | m = sendPhoto() 30 | m.chat_id = msg.chat.id 31 | m.caption = "What a pong!" 32 | m.photo = msg.photo[0].file_id 33 | else: 34 | return False 35 | 36 | rsp = yield from self.send_method(m) 37 | return True 38 | -------------------------------------------------------------------------------- /plugins/timer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TelegramBotAPI.types import sendMessage, sendPhoto 4 | from TelegramBotAPI.types import Message 5 | 6 | from TelegramBot.plugin import BotPlugin 7 | 8 | from datetime import datetime 9 | 10 | import logging 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class Timer(BotPlugin): 16 | priority = 10 17 | _user_ids = None 18 | 19 | @asyncio.coroutine 20 | def startPlugin(self): 21 | self._user_ids = {} 22 | 23 | @asyncio.coroutine 24 | def stopPlugin(self): 25 | pass 26 | 27 | @asyncio.coroutine 28 | def on_tick(self, chat_id): 29 | log.info('Sending time to %s' % chat_id) 30 | m = sendMessage() 31 | m.chat_id = chat_id 32 | m.text = "Time now is: %s" % datetime.now() 33 | yield from self.send_method(m) 34 | 35 | @asyncio.coroutine 36 | def ticker(self, chat_id, period): 37 | while True: 38 | yield from self.on_tick(chat_id) 39 | yield from asyncio.sleep(period) 40 | 41 | @asyncio.coroutine 42 | def on_message(self, msg): 43 | if not hasattr(msg, 'text'): 44 | return False 45 | 46 | if msg.text.lower() == 'timer start': 47 | log.info('Timer started for %s' % msg.chat.id) 48 | 49 | loop = asyncio.get_event_loop() 50 | self._user_ids[msg.chat.id] = loop.create_task(self.ticker(msg.chat.id, 5)) 51 | 52 | m = sendMessage() 53 | m.chat_id = msg.chat.id 54 | self._chat_id = msg.chat.id 55 | m.text = 'Timer started' 56 | yield from self.send_method(m) 57 | return True 58 | 59 | if msg.text.lower() == 'timer stop': 60 | log.info('Timer started for %s' % msg.chat.id) 61 | 62 | if msg.chat.id in self._user_ids: 63 | self._user_ids[msg.chat.id].cancel() 64 | del self._user_ids[msg.chat.id] 65 | 66 | m = sendMessage() 67 | m.chat_id = msg.chat.id 68 | m.text = 'Timer stopped' 69 | yield from self.send_method(m) 70 | return True 71 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name="aioTelegramBot", 5 | version="0.1", 6 | description="TelegramBot Service Framework", 7 | author="Source Simian", 8 | author_email='sourcesimian@users.noreply.github.com', 9 | url='https://github.com/sourcesimian/aioTelegramBot', 10 | download_url="https://github.com/sourcesimian/aioTelegramBot/tarball/v0.1", 11 | license='MIT', 12 | packages=['TelegramBot'], 13 | install_requires=['python-dateutil', 14 | 'TelegramBotAPI==0.3.1', 15 | 'pyPlugin==0.1.2', 16 | 'aiohttp' 17 | ], 18 | entry_points={ 19 | "console_scripts": [ 20 | "telegrambot=TelegramBot.service:main", 21 | ] 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /setup_env.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | if declare -f deactivate >/dev/null; then 5 | echo "- deactivating current virtual env" 6 | deactivate 7 | fi 8 | 9 | if [ -e virtualenv/ ]; then 10 | echo "- existing virtualenv found" 11 | else 12 | python3 -m venv virtualenv 13 | fi 14 | 15 | exit 16 | if [ ! -e virtualenv/ ]; then 17 | echo "! virtualenv expected" 1>&2 18 | exit 1 19 | fi 20 | 21 | . ./virtualenv/bin/activate 22 | 23 | if ! declare -f deactivate >/dev/null; then 24 | echo "! virtualenv did not activate" 1>&2 25 | exit 1 26 | fi 27 | 28 | pip3 install pep8 pyflakes pylint 29 | 30 | python setup.py develop 31 | --------------------------------------------------------------------------------