diff --git a/zulip_bots/zulip_bots/bots/googletranslate/__init__.py b/zulip_bots/zulip_bots/bots/googletranslate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/googletranslate/doc.md b/zulip_bots/zulip_bots/bots/googletranslate/doc.md new file mode 100644 index 0000000..2fac545 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/doc.md @@ -0,0 +1,26 @@ +# Google Translate bot + +The Google Translate bot uses Google Translate to translate +any text sent to it. + +## Setup + +This bot requires a google cloud API key. Create one +[here](https://support.google.com/cloud/answer/6158862?hl=en) + +You should add this key to `googletranslate.conf`. + +To run this bot, use: +`zulip-run-bots googletranslate -c +--bot-config-file ` + +## Usage + +To use this bot, @-mention it like this: + +`@-mention "" ` + +`text` must be in quotation marks, and `source language` +is optional. + +If `source language` is not given, it will automatically detect your language. diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_403.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_403.json new file mode 100644 index 0000000..0523002 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_403.json @@ -0,0 +1,31 @@ +{ + "request": { + "method": "POST", + "api_url": "https://translation.googleapis.com/language/translate/v2", + "params": { + "q": "hello", + "key": "abcdefg", + "target": "de", + "source": "en" + } + }, + "response": { + "error": { + "code": 403, + "message": "Invalid API Key.", + "errors": [ + { + "message": "Invalid API Key", + "domain": "usageLimits", + "reason": "accessNotConfigured", + "extendedHelp": "https://console.developers.google.com" + } + ], + "status": "PERMISSION_DENIED" + } + }, + "response-headers": { + "status": 403, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_conn_error.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_conn_error.json new file mode 100644 index 0000000..cffbbc3 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_conn_error.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "POST", + "api_url": "https://translation.googleapis.com/language/translate/v2", + "params": { + "q": "Hello", + "key": "abcdefg", + "target": "de", + "source": "en" + } + }, + "response": { + }, + "response-headers": { + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_language_403.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_language_403.json new file mode 100644 index 0000000..3114184 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_language_403.json @@ -0,0 +1,29 @@ +{ + "request": { + "method": "GET", + "api_url": "https://translation.googleapis.com/language/translate/v2/languages", + "params": { + "key": "abcdefg", + "target": "en" + } + }, + "response": { + "error": { + "code": 403, + "message": "Invalid API Key.", + "errors": [ + { + "message": "Invalid API Key", + "domain": "usageLimits", + "reason": "accessNotConfigured", + "extendedHelp": "https://console.developers.google.com" + } + ], + "status": "PERMISSION_DENIED" + } + }, + "response-headers": { + "status": 403, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_languages.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_languages.json new file mode 100644 index 0000000..22971f9 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_languages.json @@ -0,0 +1,32 @@ +{ + "request": { + "method": "GET", + "api_url": "https://translation.googleapis.com/language/translate/v2/languages", + "params": { + "key": "abcdefg", + "target": "en" + } + }, + "response": { + "data": { + "languages": [ + { + "language": "en", + "name": "English" + }, + { + "language": "fr", + "name": "French" + }, + { + "language": "de", + "name": "German" + } + ] + } + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_normal.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_normal.json new file mode 100644 index 0000000..5ae6b40 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_normal.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "api_url": "https://translation.googleapis.com/language/translate/v2", + "params": { + "q": "hello", + "key": "abcdefg", + "target": "de" + } + }, + "response": { + "data": { + "translations": [ + { + "translatedText": "Hallo", + "detectedSourceLanguage": "en" + } + ] + } + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_quotation.json b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_quotation.json new file mode 100644 index 0000000..7271dc4 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/fixtures/test_quotation.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "api_url": "https://translation.googleapis.com/language/translate/v2", + "params": { + "q": "this has \"quotation\" marks in", + "key": "abcdefg", + "target": "en" + } + }, + "response": { + "data": { + "translations": [ + { + "translatedText": "this has \"quotation\" marks in", + "detectedSourceLanguage": "en" + } + ] + } + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.conf b/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.conf new file mode 100644 index 0000000..c992ba6 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.conf @@ -0,0 +1,2 @@ +[googletranslate] +key=your_api_key_here diff --git a/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.py b/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.py new file mode 100644 index 0000000..a41b199 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/googletranslate.py @@ -0,0 +1,103 @@ +# To use this plugin, you need to set up the Google Cloud API key for this bot in +# googletranslate.conf in this (zulip_bots/bots/googletranslate/) directory. + +from __future__ import absolute_import +from __future__ import print_function + +import requests + +class GoogleTranslateHandler(object): + ''' + This bot will translate any messages sent to it using google translate. + Before using it, make sure you set up google api keys, and enable google + cloud translate from the google cloud console. + ''' + def usage(self): + return ''' + This plugin allows users translate messages + Users should @-mention the bot with the format + @-mention "" + ''' + + def initialize(self, bot_handler): + self.config_info = bot_handler.get_config_info('googletranslate') + self.supported_languages = get_supported_languages(self.config_info['key']) + + def handle_message(self, message, bot_handler): + bot_response = get_translate_bot_response(message['content'], + self.config_info, + message['sender_full_name'], + self.supported_languages) + bot_handler.send_reply(message, bot_response) + +api_url = 'https://translation.googleapis.com/language/translate/v2' + +help_text = ''' +Google translate bot +Please format your message like: +`@-mention "" ` +Visit [here](https://cloud.google.com/translate/docs/languages) for all languages +''' + +language_not_found_text = '{} language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages' + +def get_supported_languages(key): + parameters = {'key': key, 'target': 'en'} + response = requests.get(api_url + '/languages', params = parameters) + if response.status_code == requests.codes.ok: + languages = response.json()['data']['languages'] + return {lang['name'].lower(): lang['language'].lower() for lang in languages} + raise TranslateError(response.json()['error']['message']) + +class TranslateError(Exception): + pass + +def translate(text_to_translate, key, dest, src): + parameters = {'q': text_to_translate, 'target': dest, 'key': key} + if src != '': + parameters.update({'source': src}) + response = requests.post(api_url, params=parameters) + if response.status_code == requests.codes.ok: + return response.json()['data']['translations'][0]['translatedText'] + raise TranslateError(response.json()['error']['message']) + +def get_code_for_language(language, all_languages): + if language.lower() not in all_languages.values(): + if language.lower() not in all_languages.keys(): + return '' + language = all_languages[language.lower()] + return language + +def get_translate_bot_response(message_content, config_file, author, all_languages): + message_content = message_content.strip() + if message_content == 'help' or message_content is None or not message_content.startswith('"'): + return help_text + split_text = message_content.rsplit('" ', 1) + if len(split_text) == 1: + return help_text + split_text += split_text.pop(1).split(' ') + if len(split_text) == 2: + # There is no source language + split_text.append("") + if len(split_text) != 3: + return help_text + (text_to_translate, target_language, source_language) = split_text + text_to_translate = text_to_translate[1:] + target_language = get_code_for_language(target_language, all_languages) + if target_language == '': + return language_not_found_text.format("Target") + if source_language != '': + source_language = get_code_for_language(source_language, all_languages) + if source_language == '': + return language_not_found_text.format("Source") + try: + translated_text = translate(text_to_translate, config_file['key'], target_language, source_language) + except requests.exceptions.ConnectionError as conn_err: + return "Could not connect to Google Translate. {}.".format(conn_err) + except TranslateError as tr_err: + return "Translate Error. {}.".format(tr_err) + except Exception as err: + return "Error. {}.".format(err) + return "{} (from {})".format(translated_text, author) + +handler_class = GoogleTranslateHandler diff --git a/zulip_bots/zulip_bots/bots/googletranslate/test_googletranslate.py b/zulip_bots/zulip_bots/bots/googletranslate/test_googletranslate.py new file mode 100644 index 0000000..a3b5b66 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/googletranslate/test_googletranslate.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +from __future__ import absolute_import +from __future__ import print_function + +import json + +from unittest.mock import patch +from requests.exceptions import HTTPError, ConnectionError + +from zulip_bots.test_lib import BotTestCase +from zulip_bots.bots.googletranslate.googletranslate import TranslateError + +help_text = ''' +Google translate bot +Please format your message like: +`@-mention "" ` +Visit [here](https://cloud.google.com/translate/docs/languages) for all languages +''' + +class TestGoogleTranslateBot(BotTestCase): + bot_name = "googletranslate" + + def test_normal(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_normal'): + with self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"hello" de', 'sender_full_name': 'tester'}, + response = {'content': 'Hallo (from tester)'}, + expected_method = 'send_reply' + ) + + def test_source_language_not_found(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"hello" german foo', 'sender_full_name': 'tester'}, + response = {'content': 'Source language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages'}, + expected_method = 'send_reply' + ) + + def test_target_language_not_found(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"hello" bar english', 'sender_full_name': 'tester'}, + response = {'content': 'Target language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages'}, + expected_method = 'send_reply' + ) + + def test_403(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_403'): + with self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"hello" german english', 'sender_full_name': 'tester'}, + response = {'content': 'Translate Error. Invalid API Key..'}, + expected_method = 'send_reply' + ) + + def test_help_empty(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '', 'sender_full_name': 'tester'}, + response = {'content': help_text}, + expected_method = 'send_reply' + ) + + def test_help_command(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': 'help', 'sender_full_name': 'tester'}, + response = {'content': help_text}, + expected_method = 'send_reply' + ) + + def test_help_no_langs(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"hello"', 'sender_full_name': 'tester'}, + response = {'content': help_text}, + expected_method = 'send_reply' + ) + + def test_quotation_in_text(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_quotation'): + with self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"this has "quotation" marks in" english', 'sender_full_name': 'tester'}, + response = {'content': 'this has "quotation" marks in (from tester)'}, + expected_method = 'send_reply' + ) + + def test_exception(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + patch('zulip_bots.bots.googletranslate.googletranslate.translate', side_effect=Exception): + with self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assertRaises(Exception) + self.assert_bot_response( + message = {'content': '"hello" de', 'sender_full_name': 'tester'}, + response = {'content': 'Error. .'}, + expected_method = 'send_reply' + ) + + def test_get_language_403(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + self.mock_http_conversation('test_language_403'), \ + self.assertRaises(TranslateError): + self.initialize_bot() + + def test_connection_error(self): + with self.mock_config_info({'key': 'abcdefg'}), \ + patch('requests.post', side_effect=ConnectionError()), \ + patch('logging.warning'): + with self.mock_http_conversation('test_languages'): + self.initialize_bot() + self.assert_bot_response( + message = {'content': '"test" en', 'sender_full_name': 'tester'}, + response = {'content': 'Could not connect to Google Translate. .'}, + expected_method = 'send_reply' + )