def test_webhook_remove(): bot = VKBot(token='12345', group_id=12345) with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json = lambda: {'response': {'items': [{'id': 17}, {'id': 18}]}} bot.remove_webhook() assert mock_get.call_count == 3 assert mock_get.call_args_list[0][1]['url'].endswith('groups.getCallbackServers') for call_args, item_id in zip(mock_get.call_args_list[1:], [17, 18]): assert call_args[1]['url'].endswith('groups.deleteCallbackServer') assert call_args[1]['params']['server_id'] == item_id
def test_send_message(mock_post: MagicMock): mock_post.return_value.status_code = 200 mock_post.return_value.json = lambda: {} bot = VKBot(token='12345', group_id=12345) bot.send_message(user_id=666, text='hello', keyboard={'buttons': [[{'action': {'type': 'text', 'label': 'yay'}}]]}) assert mock_post.called args, kwargs = mock_post.call_args assert kwargs['url'].endswith('messages.send') assert kwargs['data']['user_id'] == 666 assert kwargs['data']['message'] == 'hello' assert kwargs['data']['access_token'] == '12345' assert 'group_id' not in kwargs['data'] assert isinstance(kwargs['data']['keyboard'], str)
def test_webhook_processing(): bot = VKBot(token='12345', group_id=12345) bot.webhook_key = 'secret' assert bot.process_webhook_data({'type': 'confirmation', 'group_id': 12345}) == ('secret', 200) messages = [] @bot.message_handler() def handle(message): messages.append(message) new_message = {'type': 'message_new', 'object': {'message': {'text': 'hello bot'}}} assert bot.process_webhook_data(new_message) == ('ok', 200) assert messages[0].text == 'hello bot'
def test_webhook_set(): bot = VKBot(token='12345', group_id=12345) assert bot.webhook_key is None with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json = lambda: {'response': {'code': '777', 'server_id': 13}} bot.set_webhook(url='localhost:15777', remove_old=False) assert bot.webhook_key == '777' assert mock_get.call_count == 3 assert mock_get.call_args_list[0][1]['url'].endswith('groups.getCallbackConfirmationCode') assert mock_get.call_args_list[1][1]['url'].endswith('groups.addCallbackServer') assert mock_get.call_args_list[1][1]['params']['secret_key'] == '777' assert mock_get.call_args_list[1][1]['params']['url'] == 'localhost:15777' assert mock_get.call_args_list[2][1]['url'].endswith('groups.setCallbackSettings') assert mock_get.call_args_list[2][1]['params']['server_id'] == 13
def test_apply_handlers(): bot = VKBot(token='12345', group_id=12345) processed1 = [] processed2 = [] processed3 = [] processed4 = [] @bot.message_handler(regexp='hello') def handle1(message: VKMessage): processed1.append(message) @bot.message_handler(types=['strange_type']) def handle2(message: VKMessage): processed2.append(message) @bot.message_handler(func=lambda x: len(x['object']['message']['text']) == 3) def handle3(message: VKMessage): processed3.append(message) @bot.message_handler() def handle4(message: VKMessage): processed4.append(message) assert len(bot.message_handlers) == 4 bot.process_new_updates([{'type': 'message_new', 'object': {'message': {'text': 'hello bot'}}}]) assert len(processed1) == 1 assert len(processed2) == 0 assert processed1[0].text == 'hello bot' bot.process_new_updates([{'type': 'strange_type', 'object': {'message': {'text': 'hello bot'}}}]) assert len(processed1) == 1 assert len(processed2) == 1 bot.process_new_updates([{'type': 'message_new', 'object': {'message': {'text': 'wow'}}}]) assert len(processed3) == 1 bot.process_new_updates([{'type': 'message_new', 'object': {'message': {'text': 'fallback'}}}]) assert len(processed4) == 1
def test_polling(): bot = VKBot(token='12345', group_id=12345) # test setting polling server assert bot._polling_server is None with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json = lambda: {'response': {'server': 'abcd', 'key': 'xyz', 'ts': 23}} bot.set_polling_server() assert mock_get.called assert mock_get.call_args[1]['url'].endswith('groups.getLongPollServer') assert bot._polling_server == 'abcd' # test actually polling with patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json = lambda: {'updates': ['i am an update'], 'ts': 38} assert bot.retrieve_updates() == ['i am an update'] assert mock_get.called assert mock_get.call_args[1]['url'] == 'abcd' assert mock_get.call_args[1]['params']['key'] == 'xyz' assert mock_get.call_args[1]['params']['ts'] == 23 assert bot._polling_ts == 38
def __init__( self, connector: DialogConnector, telegram_token=None, facebook_access_token=None, facebook_verify_token=None, base_url=None, alice_url='alice/', telegram_url='tg/', facebook_url='fb/', vk_url='vk/', restart_webhook_url='restart_webhook', vk_token=None, vk_group_id=None, app=None, ): self.telegram_token = telegram_token or os.environ.get( 'TOKEN') or os.environ.get('TELEGRAM_TOKEN') self.facebook_access_token = facebook_access_token or os.environ.get( 'FACEBOOK_ACCESS_TOKEN') self.facebook_verify_token = facebook_verify_token or os.environ.get( 'FACEBOOK_VERIFY_TOKEN') self.vk_token = vk_token or os.environ.get('VK_TOKEN') self.vk_group_id = vk_group_id or os.environ.get('VK_GROUP_ID') if base_url is None: base_url = os.environ.get('BASE_URL') self.base_url = base_url self.alice_url = alice_url self.telegram_url = telegram_url self.facebook_url = facebook_url self.vk_url = vk_url self.restart_webhook_url = restart_webhook_url self.connector = connector self.app = app or Flask(__name__) logger.info('The Alice webhook is available on "{}"'.format( self.alice_webhook_url)) self.app.route(self.alice_webhook_url, methods=['POST'])(self.alice_response) if self.telegram_token is not None: self.bot = telebot.TeleBot(self.telegram_token) self.bot.message_handler(func=lambda message: True)( self.tg_response) if base_url is not None: logger.info( 'Running Telegram bot with token "{}" on "{}"'.format( self.telegram_token, self.telegram_webhook_url)) self.app.route(self.telegram_webhook_url, methods=['POST'])(self.get_tg_message) self.app.route("/" + self.restart_webhook_url)( self.telegram_web_hook) else: logger.info( 'Running Telegram bot with token "{}", but without BASE_URL it can work only locally' .format(self.telegram_token)) else: logger.info( 'Running no Telegram bot because TOKEN or BASE_URL was not provided' ) self.bot = None if self.facebook_verify_token and self.facebook_access_token: logger.info('Running Facebook bot on "{}"'.format( self.facebook_webhook_url)) self.app.route(self.facebook_webhook_url, methods=['GET' ])(self.receive_fb_verification_request) self.app.route(self.facebook_webhook_url, methods=['POST'])(self.facebook_response) self.facebook_bot = FacebookBot(self.facebook_access_token) else: logger.info( 'Running no Facebook bot because FACEBOOK_ACCESS_TOKEN or FACEBOOK_VERIFY_TOKEN was not provided' ) self.facebook_bot = None self.vk_bot = None if self.vk_token is None: logger.info('Skipping VK setup because vk_token is empty') elif self.vk_group_id is None: logger.info('Skipping VK setup because vk_group_id is empty') else: self.vk_bot = VKBot(token=self.vk_token, group_id=self.vk_group_id) self.vk_bot.message_handler()(self.vk_response) self.app.route("/" + self.vk_url, methods=[ 'POST' ])(lambda: self.vk_bot.process_webhook_data(request.json)) self._processed_telegram_ids = set()
class FlaskServer: def __init__( self, connector: DialogConnector, telegram_token=None, facebook_access_token=None, facebook_verify_token=None, base_url=None, alice_url='alice/', telegram_url='tg/', facebook_url='fb/', vk_url='vk/', restart_webhook_url='restart_webhook', vk_token=None, vk_group_id=None, app=None, ): self.telegram_token = telegram_token or os.environ.get( 'TOKEN') or os.environ.get('TELEGRAM_TOKEN') self.facebook_access_token = facebook_access_token or os.environ.get( 'FACEBOOK_ACCESS_TOKEN') self.facebook_verify_token = facebook_verify_token or os.environ.get( 'FACEBOOK_VERIFY_TOKEN') self.vk_token = vk_token or os.environ.get('VK_TOKEN') self.vk_group_id = vk_group_id or os.environ.get('VK_GROUP_ID') if base_url is None: base_url = os.environ.get('BASE_URL') self.base_url = base_url self.alice_url = alice_url self.telegram_url = telegram_url self.facebook_url = facebook_url self.vk_url = vk_url self.restart_webhook_url = restart_webhook_url self.connector = connector self.app = app or Flask(__name__) logger.info('The Alice webhook is available on "{}"'.format( self.alice_webhook_url)) self.app.route(self.alice_webhook_url, methods=['POST'])(self.alice_response) if self.telegram_token is not None: self.bot = telebot.TeleBot(self.telegram_token) self.bot.message_handler(func=lambda message: True)( self.tg_response) if base_url is not None: logger.info( 'Running Telegram bot with token "{}" on "{}"'.format( self.telegram_token, self.telegram_webhook_url)) self.app.route(self.telegram_webhook_url, methods=['POST'])(self.get_tg_message) self.app.route("/" + self.restart_webhook_url)( self.telegram_web_hook) else: logger.info( 'Running Telegram bot with token "{}", but without BASE_URL it can work only locally' .format(self.telegram_token)) else: logger.info( 'Running no Telegram bot because TOKEN or BASE_URL was not provided' ) self.bot = None if self.facebook_verify_token and self.facebook_access_token: logger.info('Running Facebook bot on "{}"'.format( self.facebook_webhook_url)) self.app.route(self.facebook_webhook_url, methods=['GET' ])(self.receive_fb_verification_request) self.app.route(self.facebook_webhook_url, methods=['POST'])(self.facebook_response) self.facebook_bot = FacebookBot(self.facebook_access_token) else: logger.info( 'Running no Facebook bot because FACEBOOK_ACCESS_TOKEN or FACEBOOK_VERIFY_TOKEN was not provided' ) self.facebook_bot = None self.vk_bot = None if self.vk_token is None: logger.info('Skipping VK setup because vk_token is empty') elif self.vk_group_id is None: logger.info('Skipping VK setup because vk_group_id is empty') else: self.vk_bot = VKBot(token=self.vk_token, group_id=self.vk_group_id) self.vk_bot.message_handler()(self.vk_response) self.app.route("/" + self.vk_url, methods=[ 'POST' ])(lambda: self.vk_bot.process_webhook_data(request.json)) self._processed_telegram_ids = set() @property def alice_webhook_url(self): return "/" + self.alice_url @property def facebook_webhook_url(self): return '/' + self.facebook_url @property def telegram_webhook_url(self): return '/' + self.telegram_url + self.telegram_token def alice_response(self): logger.info('Got message from Alice: {}'.format(request.json)) response = self.connector.respond(request.json, source=SOURCES.ALICE) logger.info('Sending message to Alice: {}'.format(response)) return json.dumps(response, ensure_ascii=False, indent=2) def tg_response(self, message): logger.info('Got message from Telegram: {}'.format(message)) if message.message_id in self._processed_telegram_ids: # avoid duplicate response after the bot starts logger.info( 'Telegram message id {} is duplicate, skipping it'.format( message.message_id)) return self._processed_telegram_ids.add(message.message_id) # todo: cleanup old ids from _processed_telegram_ids response = self.connector.respond(message, source=SOURCES.TELEGRAM) telegram_response = self.bot.reply_to(message, **response) multimedia = response.pop('multimedia', []) for item in multimedia: if item['type'] == 'photo': self.bot.send_photo(message.chat.id, photo=item['content'], **response) if item['type'] == 'document': self.bot.send_document(message.chat.id, data=item['content'], **response) elif item['type'] == 'audio': self.bot.send_audio(message.chat.id, audio=item['content'], **response) logger.info('Sent a response to Telegram: {}'.format(message)) def vk_response(self, message: VKMessage): response = self.connector.respond(message, source=SOURCES.VK) self.vk_bot.send_message(user_id=message.user_id, **response) def get_tg_message(self): self.bot.process_new_updates([ telebot.types.Update.de_json(request.stream.read().decode("utf-8")) ]) return "!", 200 def telegram_web_hook(self): self.bot.remove_webhook() self.bot.set_webhook(url=self.base_url + self.telegram_url + self.telegram_token) return "Telegram webhook restarted!", 200 def vk_web_hook(self): endpoint = self.base_url + '/' + self.vk_url logger.info('Setting vk webhook on {}'.format(endpoint)) self.vk_bot.set_postponed_webhook(url=endpoint, remove_old=True) def run_local_telegram(self): if self.bot is not None: self.bot.remove_webhook() self.bot.polling() else: raise ValueError( 'Cannot run Telegram bot, because Telegram token was not found.' ) def run_local_vk(self): if self.vk_bot is not None: self.vk_bot.remove_webhook() self.vk_bot.polling() else: raise ValueError('VK bot is not initialized and cannot run.') def receive_fb_verification_request(self): """Before allowing people to message your bot, Facebook has implemented a verify token that confirms all requests that your bot receives came from Facebook.""" token_sent = request.args.get("hub.verify_token") if token_sent == self.facebook_verify_token: return request.args.get("hub.challenge") return 'Invalid verification token' def facebook_response(self): output = request.get_json() logger.info('Got messages from Facebook: {}'.format(output)) for event in output['entry']: messaging = event['messaging'] for message in messaging: if message.get('message') or message.get('postback'): recipient_id = message['sender']['id'] response = self.connector.respond(message, source=SOURCES.FACEBOOK) logger.info( 'Sending message to Facebook: {}'.format(response)) self.facebook_bot.send_message(recipient_id, response) return "Message Processed" def run_server(self, host="0.0.0.0", port=None, use_ngrok=False): # todo: maybe, run a foreign app instead (attach own blueprint to it) if port is None: port = int(os.environ.get('PORT', 5000)) if use_ngrok: from tgalice.server.flask_ngrok import run_ngrok logger.info('starting ngrok...') ngrok_address = run_ngrok(port) logger.info('ngrok_address is {}'.format(ngrok_address)) self.base_url = ngrok_address if self.telegram_token is not None and self.base_url is not None: self.telegram_web_hook() else: warnings.warn( 'Either telegram token or base_url was not found; cannot run Telegram bot.' ) if self.vk_bot: self.vk_web_hook() else: logging.warning('VK bot has not been created; cannot run it') self.app.run(host=host, port=port) def run_command_line(self): input_sentence = '' while True: response, need_to_exit = self.connector.respond( input_sentence, source=SOURCES.TEXT) print(response) if need_to_exit: break input_sentence = input('> ') def parse_args_and_run(self): parser = argparse.ArgumentParser(description='Run the bot') parser.add_argument('--cli', action='store_true', help='Run the bot locally in command line mode') parser.add_argument( '--poll', action='store_true', help='Run the bot locally in polling mode (Telegram or VK)') parser.add_argument( '--ngrok', action='store_true', help='Run the bot locally with ngrok tunnel into the Internet') args = parser.parse_args() if args.cli: self.run_command_line() elif args.poll: if self.bot: self.run_local_telegram() elif self.vk_bot: self.run_local_vk() else: raise ValueError('Got no local bots to run in polling mode') else: self.run_server(use_ngrok=args.ngrok)
import logging import os from tgalice.interfaces.vk import VKBot, VKMessage logging.basicConfig(level=logging.DEBUG) bot = VKBot( token=os.environ['VK_TOKEN'], group_id=os.environ['VK_GROUP_ID'], polling_wait=3, # normally, timeout is about 20 seconds, but we make it shorter for quicker feedback ) @bot.message_handler() def respond(message: VKMessage): bot.send_message( user_id=message.user_id, text='Вы написали {}'.format(message.text), keyboard={ 'one_time': True, 'buttons': [[{ 'action': {'type': 'text', 'label': 'окей'}, 'color': 'secondary', }]] }, ) if __name__ == '__main__': bot.polling()
import logging import os from flask import Flask, request from tgalice.interfaces.vk import VKBot, VKMessage logging.basicConfig(level=logging.INFO) logging.getLogger('tgalice.interfaces.vk').setLevel(logging.DEBUG) app = Flask(__name__) bot = VKBot( token=os.environ['VK_TOKEN'], group_id=os.environ['VK_GROUP_ID'], ) @bot.message_handler() def respond(message: VKMessage): bot.send_message( user_id=message.user_id, text='Вы написали {}'.format(message.text), keyboard={'buttons': [[{ 'action': { 'type': 'text', 'label': 'ок' } }]]}, )