diff --git a/tools/run-mypy b/tools/run-mypy index e738e43..8a28c11 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -74,7 +74,9 @@ force_include = [ "zulip_bots/zulip_bots/bots/yoda/yoda.py", "zulip_bots/zulip_bots/bots/yoda/test_yoda.py", "zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py", - "zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py" + "zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py", + "zulip_bots/zulip_bots/bots/mention/mention.py", + "zulip_bots/zulip_bots/bots/mention/test_mention.py" ] parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") diff --git a/zulip_bots/zulip_bots/bots/mention/__init__.py b/zulip_bots/zulip_bots/bots/mention/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/mention/assets/mentions_demo.png b/zulip_bots/zulip_bots/bots/mention/assets/mentions_demo.png new file mode 100644 index 0000000..6eb83ab Binary files /dev/null and b/zulip_bots/zulip_bots/bots/mention/assets/mentions_demo.png differ diff --git a/zulip_bots/zulip_bots/bots/mention/doc.md b/zulip_bots/zulip_bots/bots/mention/doc.md new file mode 100644 index 0000000..599e507 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/doc.md @@ -0,0 +1,32 @@ +# Mention bot + +The Mention bot is a Zulip bot that can fetch Mentions associated with +a given keyword from the web using [Mention](https://mention.com/en/). + +To use the Mention bot, you can simply call it with `@` followed +by a keyword, like so: + +``` +@Mention Apple +``` + +## Setup + +Before you can proceed further, you'll need to go to the +[Mention Dev](https://dev.mention.com/login), and get a +Mention API Access Token. + +1. Login. +2. Enter the **App Name**, **Description**, **Website**, and **Redirect uris**. In this version, there +is no actual use of the Redirect Uri and Website. +3. After accepting the agreement, click on **Create New App**. +4. And you're done! You should now have an Access Token. +5. Open up `zulip_bots/bots/mention/mention.conf` in an editor and + change the value of the `` attribute to the Access Token + you generated above. + +## Usage +`@Mention ` - This command will fetch the most recent 20 +mentions of the keyword on the web (Limitations of a free account). +Example: +![](assets/mentions_demo.png) diff --git a/zulip_bots/zulip_bots/bots/mention/fixtures/create_alert.json b/zulip_bots/zulip_bots/bots/mention/fixtures/create_alert.json new file mode 100644 index 0000000..af4d6c9 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/fixtures/create_alert.json @@ -0,0 +1,125 @@ +{ + "alert": { + "id": "112233", + "name": "NASA and competitors", + "query": { + "type": "basic", + "included_keywords": [ + "NASA", + "Arianespace", + "SpaceX", + "Pockocmoc" + ], + "required_keywords": [ + "mars" + ], + "excluded_keywords": [ + "nose", + "fil d'ariane" + ] + }, + "languages": [ + "en", + "fr", + "ru" + ], + "countries": [], + "sources": [ + "twitter", + "news", + "web", + "blogs", + "videos", + "forums", + "images" + ], + "blocked_sites": [ + "www.spaceoflovemagazine.com/" + ], + "role": "admin", + "stats": { + "mention_sources": { + "total": "0" + }, + "mention_folders": { + "inbox": { + "total": "0" + }, + "archive": { + "total": "0" + }, + "spam": { + "total": "0" + }, + "trash": { + "total": "0" + } + }, + "unread_mentions": { + "total": "0" + }, + "unseen_mentions": { + "total": "0" + }, + "favorite_mentions": { + "total": "0" + }, + "important_mentions": { + "total": "0" + }, + "tasks": { + "total": "0" + }, + "todo_tasks": { + "total": "0" + }, + "done_tasks": { + "total": "0" + }, + "logs": { + "total": "0" + } + }, + "shares": [ + { + "id": "THE_ACCOUNT_ID", + "account": { + "id": "THE_ACCOUNT_ID", + "name": "Doe", + "email": "john.doe@nasa.com", + "language_code": "en", + "created_at": "2014-09-30T10:03:54.0+00:00", + "updated_at": "2016-01-14T14:55:57.0+00:00", + "avatar_url": "https:\/\/d39qsljf883l.cloudfront.net\/f6415b89ef2ljkca5c0c7d464f1b82-088f3dsqlj12lj4.jpg", + "timezone": "Europe\/Berlin", + "grouped_email_notification": true, + "default_email_notification_frequency": "daily", + "default_desktop_notification_frequency": "hourly", + "default_push_notification_frequency": "hourly" + }, + "role": "admin", + "permissions": { + "edit": true, + "delete": true + }, + "created_at": "2016-01-14T15:31:42.0+00:00", + "weight": 88674800 + } + ], + "noise_detection": true, + "created_at": "2016-01-14T15:31:42.0+00:00", + "updated_at": "2016-01-14T15:31:44.0+00:00", + "quota_used": 0, + "index_version": 2, + "permissions": { + "edit": true, + "share": true, + "list_tasks": true, + "list_logs": true + }, + "description": "Monitor NASA press release.", + "color": "#05e363", + "connection_type": "related", + "connection_id": "12121212" + } +} diff --git a/zulip_bots/zulip_bots/bots/mention/fixtures/get_mentions.json b/zulip_bots/zulip_bots/bots/mention/fixtures/get_mentions.json new file mode 100644 index 0000000..3ecf33c --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/fixtures/get_mentions.json @@ -0,0 +1,15 @@ +{ + "mentions": [ + {...mention...}, + {...mention...}, + {...mention...} + ], + "_links": { + "more": { + "href": "...url to get older mentions..." + }, + "pull": { + "href": "...url to get newer mentions..." + } + } +} diff --git a/zulip_bots/zulip_bots/bots/mention/fixtures/mention.json b/zulip_bots/zulip_bots/bots/mention/fixtures/mention.json new file mode 100644 index 0000000..47302cb --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/fixtures/mention.json @@ -0,0 +1,73 @@ +{ + "id": "527849933", + "alert_id": 112233, + "title": "NASA image shows 'starburst spider' pattern on Mars", + "description": "USA TODAY NASA image shows 'starburst spider' pattern on Mars USA TODAY NASA's Mars Reconnaissance Orbiter captured a stunning image of a \"starburst\" pattern on Mars ' surface. The \u201cstarbursts\u201d can be seen each spring when Mars ' seasonal cap of carbon \u2026", + "original_url": "http:\/\/www.usatoday.com\/story\/tech\/nation-now\/2016\/01\/14\/nasa-image-shows-starburst-spider-pattern-mars\/78781074\/", + "clickable_url": "https:\/\/web.mention.com\/api\/url?token=eyJ0eXAiOiJKV1QiLChbGciOiJIUzI1NiJ9.eyJ1cmwiOJodHRwOlwvXC93d3cudXNhdG9kYkuY29tXC9zdG9eVwvdGVjaFwvbmF0a9uLW5vd1wvMjAxNlwvMDFcLzE0XC9uYXNLWltYWdlLXNob3dzLXN0YJidXJzdC1zcGlZXItcGF0dGVyb1tYXJzXC83ODc4MTA3NFwvIiwiYWNjb3VudF9pZI6NDM4NDA0LCJhbGVydF9pZI6MTE2NTk5NSwic291cmNlXlkIjo0LCJtZW50a9uX2lkIjoiNzU2NzM5MjMzMIifQ.XH7WJlYkOYTDFysZELmouro__7QVoe5pT9c1qeZw", + "displayable_url": "usatoday.com\/story\/tech\/nation-now\/2016\/01\/14\/nasa-image-shows-starburst-spider-pattern-mars\/78781074\/", + "unique_id": "http:\/\/www.usatoday.com\/story\/tech\/nation-now\/2016\/01\/14\/nasa-image-shows-starburst-spider-pattern-mars\/78781074\/", + "published_at": "2016-01-14T17:16:27.10090700+00:00", + "created_at": "2016-01-18T16:05:44.0+00:00", + "country": "US", + "updated_at": "2016-01-18T16:05:45.0+00:00", + "favorite": false, + "folder": "inbox", + "folder_set_by_user": false, + "read": false, + "tone": 0, + "source_type": "news", + "source_name": "USA TODAY", + "source_url": "http:\/\/www.usatoday.com\/", + "language_code": "en", + "tasks": [], + "logs": [], + "children": { + "children": [ + {...mention...}, + {...mention...}, + {...mention...} + ], + "total": 42, + "_links": { + "more": { + "href": "/api/accounts/THE_ACCOUNT_ID/alerts/112233/mentions/527849937/children?limit=20&before_date=2014-03-20T18:10:37.53829200+00:00", + "params": { + "limit": 20, + "before_date": "2014-03-20T18:10:37.53829200+00:00" + } + } + } + }, + "picture_url": "\/\/t3.gstatic.com\/images?q=tbn:ANd9GcQlW3QxiNh2YxxacyF0gR636ViYH6YS_0ONIRj9pf4OhiRZ8hHHyCQqdOYVgMuToZ1Iixhy", + "tags": [], + "offsets": { + "title": [0, 0, 4, 4, 47, 47, 4, 4], + "description": [10,10, 4, 4, 57, 57, 4, 4, 72, 72, 4, 4, 79, 79, 4, 4, 161, 161, 4, 4, 223, 223, 4, 4], + "url": [], + "source_name": [], + "source_url": [] + }, + "permissions": { + "favorite": true, + "change_folder": true, + "create_task": true + }, + "author_influence": { + "id": "3153793048", + "alert_id": 112233, + "kind": "web", + "url": "http:\/\/www.usatoday.com", + "name": "usatoday.com", + "score": 83, + "scored_id": "usatoday.com" + }, + "metadata": { + "twitter": { + "id_str": "800747234652340224", + "user": { + "id_str": "188302352" + } + } + } +} diff --git a/zulip_bots/zulip_bots/bots/mention/mention.conf b/zulip_bots/zulip_bots/bots/mention/mention.conf new file mode 100644 index 0000000..e6af086 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/mention.conf @@ -0,0 +1,3 @@ +[mention] +access_token = +# create an app here and get the access token from it: https://dev.mention.com/login diff --git a/zulip_bots/zulip_bots/bots/mention/mention.py b/zulip_bots/zulip_bots/bots/mention/mention.py new file mode 100644 index 0000000..961fa27 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/mention.py @@ -0,0 +1,103 @@ +# See readme.md for instructions on running this code. + +import requests +from typing import Any, List + +class MentionHandler(object): + def initialize(self, bot_handler: Any) -> None: + self.config_info = bot_handler.get_config_info('mention') + self.access_token = self.config_info['access_token'] + self.account_id = '' + + def usage(self) -> str: + return ''' + This is a Mention API Bot which will find mentions + of the given keyword throughout the web. + Version 1.00 + ''' + + def handle_message(self, message: Any, bot_handler: Any) -> None: + message['content'] = message['content'].strip() + + if message['content'].lower() == 'help': + bot_handler.send_reply(message, self.usage()) + return + + if message['content'] == '': + bot_handler.send_reply(message, 'Empty Mention Query') + return + + keyword = message['content'] + content = self.generate_response(keyword) + bot_handler.send_reply(message, content) + + def get_account_id(self) -> str: + get_ac_id_header = { + 'Authorization': 'Bearer ' + self.access_token, + 'Accept-Version': '1.15', + } + response = requests.get( + 'https://api.mention.net/api/accounts/me', headers=get_ac_id_header) + data_json = response.json() + account_id = data_json['account']['id'] + return account_id + + def get_alert_id(self, keyword: str) -> str: + create_alert_header = { + 'Authorization': 'Bearer ' + self.access_token, + 'Content-Type': 'application/json', + 'Accept-Version': '1.15', + } + + create_alert_data = { + 'name': keyword, + 'query': { + 'type': 'basic', + 'included_keywords': [keyword] + }, + 'languages': ['en'], + 'sources': ['web'] + } # type: Any + + response = requests.post('https://api.mention.net/api/accounts/' + self.account_id + + '/alerts', data=create_alert_data, headers=create_alert_header) + data_json = response.json() + alert_id = data_json['alert']['id'] + return alert_id + + def get_mentions(self, alert_id: str) -> List[Any]: + get_mentions_header = { + 'Authorization': 'Bearer ' + self.access_token, + 'Accept-Version': '1.15', + } + response = requests.get('https://api.mention.net/api/accounts/' + self.account_id + + '/alerts/' + alert_id + '/mentions', headers=get_mentions_header) + data_json = response.json() + mentions = data_json['mentions'] + return mentions + + def generate_response(self, keyword: str) -> str: + if self.account_id == '': + self.account_id = self.get_account_id() + + try: + alert_id = self.get_alert_id(keyword) + except (TypeError, KeyError): + # Usually triggered by invalid token or json parse error when account quote is finished. + raise MentionNoResponseException() + + try: + mentions = self.get_mentions(alert_id) + except (TypeError, KeyError): + # Usually triggered by no response or json parse error when account quota is finished. + raise MentionNoResponseException() + + reply = 'The most recent mentions of `' + keyword + '` on the web are: \n' + for mention in mentions: + reply += "[{title}]({id})\n".format(title=mention['title'], id=mention['original_url']) + return reply + +handler_class = MentionHandler + +class MentionNoResponseException(Exception): + pass diff --git a/zulip_bots/zulip_bots/bots/mention/test_mention.py b/zulip_bots/zulip_bots/bots/mention/test_mention.py new file mode 100644 index 0000000..cbfbf9e --- /dev/null +++ b/zulip_bots/zulip_bots/bots/mention/test_mention.py @@ -0,0 +1,34 @@ +from zulip_bots.test_lib import BotTestCase + +class TestMentionBot(BotTestCase): + bot_name = "mention" + + def test_bot_responds_to_empty_message(self) -> None: + # Offline query. + with self.mock_config_info({'access_token': '12345'}): + self.verify_reply('', 'Empty Mention Query') + + def test_help_query(self) -> None: + # Offline query. + with self.mock_config_info({'access_token': '12345'}): + self.verify_reply('help', ''' + This is a Mention API Bot which will find mentions + of the given keyword throughout the web. + Version 1.00 + ''') + + # Offline query. + with self.mock_config_info({'access_token': '12345'}): + self.verify_reply('hElp', ''' + This is a Mention API Bot which will find mentions + of the given keyword throughout the web. + Version 1.00 + ''') + + # Offline query. + with self.mock_config_info({'access_token': '12345'}): + self.verify_reply('HELP', ''' + This is a Mention API Bot which will find mentions + of the given keyword throughout the web. + Version 1.00 + ''')