beeminder bot: Add beeminder bot.
ADD
zulip_bots/zulip_bots/bots/beeminder/__init__.py
zulip_bots/zulip_bots/bots/beeminder/__init__.py
ADD
zulip_bots/zulip_bots/bots/beeminder/beeminder.conf
+[beeminder]
+auth_token=9sMQV47XXidu51UEDwgB
+username=rr_0410
+goalname=gainweight

ADD
zulip_bots/zulip_bots/bots/beeminder/beeminder.py
+import requests
+import logging
+import json
+from typing import Dict, Any, List
+from requests.exceptions import HTTPError, ConnectionError
+
+help_message = '''
+You can add datapoints towards your beeminder goals \
+following the syntax shown below :smile:.\n \
+\n**@mention-botname daystamp, value, comment**\
+\n* `daystamp`**:** *yyyymmdd* \
+[**NOTE:** Optional field, default is *current daystamp*],\
+\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
+\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
+'''
+
+def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str:
+ username = config_info['username']
+ goalname = config_info['goalname']
+ auth_token = config_info['auth_token']
+
+ message_content = message_content.strip()
+ if message_content == '' or message_content == 'help':
+ return help_message
+
+ url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(username, goalname)
+ message_pieces = message_content.split(',')
+ for i in range(len(message_pieces)):
+ message_pieces[i] = message_pieces[i].strip()
+
+ if (len(message_pieces) == 1):
+ payload = {
+ "value": message_pieces[0],
+ "auth_token": auth_token
+ }
+ elif (len(message_pieces) == 2):
+ if (message_pieces[1].isdigit()):
+ payload = {
+ "daystamp": message_pieces[0],
+ "value": message_pieces[1],
+ "auth_token": auth_token
+ }
+ else:
+ payload = {
+ "value": message_pieces[0],
+ "comment": message_pieces[1],
+ "auth_token": auth_token
+ }
+ elif (len(message_pieces) == 3):
+ payload = {
+ "daystamp": message_pieces[0],
+ "value": message_pieces[1],
+ "comment": message_pieces[2],
+ "auth_token": auth_token
+ }
+ elif (len(message_pieces) > 3):
+ return "Make sure you follow the syntax.\n You can take a look \
+at syntax by: @mention-botname help"
+
+ try:
+ r = requests.post(url, json=payload)
+
+ if r.status_code != 200:
+ if r.status_code == 401: # Handles case of invalid key and missing key
+ return "Error. Check your key!"
+ else:
+ return "Error occured : {}".format(r.status_code) # Occures in case of unprocessable entity
+ else:
+ datapoint_link = "https://www.beeminder.com/{}/{}".format(username, goalname)
+ return "[Datapoint]({}) created.".format(datapoint_link) # Handles the case of successful datapoint creation
+ except ConnectionError as e:
+ logging.exception(str(e))
+ return "Uh-Oh, couldn't process the request \
+right now.\nPlease try again later"
+
+
+class BeeminderHandler(object):
+ '''
+ This plugin allows users to easily add datapoints
+ towards their beeminder goals via zulip
+ '''
+
+ def initialize(self, bot_handler: Any) -> None:
+ self.config_info = bot_handler.get_config_info('beeminder')
+ # Check for valid auth_token
+ try:
+ result = get_beeminder_response('5', self.config_info)
+ if result == "Error. Check your key!":
+ bot_handler.quit('Invalid key!')
+ except ConnectionError:
+ logging.warning('Bad connection')
+ raise
+
+ def usage(self) -> str:
+ return "This plugin allows users to add datapoints towards their Beeminder goals"
+
+ def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None:
+ response = get_beeminder_response(message['content'], self.config_info)
+ bot_handler.send_reply(message, response)
+
+handler_class = BeeminderHandler

ADD
zulip_bots/zulip_bots/bots/beeminder/doc.md
+# Beeminder bot
+
+The Beeminder bot can help you adding datapoints towards
+your Beeminder goal from Zulip.
+
+To use the Beeminder bot, you can simply call it with `@beeminder`
+followed by a daystamp, value and an optional comment.
+
+Syntax is like:
+```
+@beeminder daystamp, value, comment
+```
+
+**NOTE** : **Commas** between inputs are a must, otherwise,
+you'll get an error.
+
+## Setup and Configuration
+
+Before running Beeminder bot you will need three things as follows :
+
+1. **auth_token**
+ - Go to your [Beeminder](https://www.beeminder.com/) **account settings**.
+Under **APPS & API** section you will find your **auth token**.
+
+2. **username**
+ - Your Beeminder username.
+
+3. **Goalname**
+ - The name of your Beeminder goal for which you want to
+add datapoints from [Zulip](https://zulipchat.com/)
+
+Once you have above information, you should supply
+them in `beeminder.conf` file.
+
+Run this bot as described in
+[here](https://zulipchat.com/api/running-bots#running-a-bot).
+
+## Usage
+
+You can give command to add datapoint in 4 ways:
+
+1. `@beeminder daystamp, value, comment`
+ - Example usage: `@beeminder 20180125, 15, Adding datapoint`.
+ - This will add a datapoint to your Beeminder goal having
+**daystamp**: `20180125`, **value**: `15` with
+**comment**: `Adding datapoint`.
+
+2. `@beeminder daystamp, value`
+ - Example usage: `@beeminder 20180125, 15`.
+ - This will add a datapoint in your Beeminder goal having
+**daystamp**: `20180125`, **value**: `15` and **comment**: `None`.
+
+3. `@beeminder value, comment`
+ - Example usage: `@beeminder 15, Adding datapoint`.
+ - This will add a datapoint in your Beeminder goal having
+**daystamp**: `current daystamp`, **value**: `15` and **comment**: `Adding datapoint`.
+
+4. `@beeminder value`
+ - Example usage: `@beeminder 15`.
+ - This will add a datapoint in your Beeminder goal having
+**daystamp**: `current daystamp`, **value**: `15` and **comment**: `None`.
+
+5. `@beeminder ` or `@beeminder help` will fetch you the `help message`.

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_blank_input.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "XXXXXX",
+ "value": "5"
+ }
+ },
+ "response": {
+ "help_message": "This is help message."
+ },
+ "response-headers": {
+ "status": 200,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_error.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "XXXXXX",
+ "value": "notNumber"
+ }
+ },
+ "response": {
+ "errors": {
+ "value": [
+ "is not a number"
+ ]
+ }
+ },
+ "response-headers": {
+ "status": 422,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_help_message.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "XXXXXX",
+ "value": "5"
+ }
+ },
+ "response": {
+ "help_message": "This is help message."
+ },
+ "response-headers": {
+ "status": 200,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_invalid.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "someInvalidKey",
+ "value": "5"
+ }
+ },
+ "response": {
+ "errors": {
+ "auth_token": "bad_token",
+ "message": "No such auth_token found. (Did you mix up auth_token and access_token?)"
+ }
+ },
+ "response-headers": {
+ "status": 401,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_normal.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "XXXXXX",
+ "value": "2",
+ "comment": "hi there!"
+ }
+ },
+ "response": {
+ "timestamp": 1517908892,
+ "value": 2,
+ "comment": "hi there!",
+ "id": "5a79739cbfec03345f011e6c",
+ "updated_at": 1517908892,
+ "requestid": null,
+ "canonical": "06 2 \"hi there!\"",
+ "fulltext": "2018-Feb-06 entered at 14:51 via api",
+ "origin": "api",
+ "daystamp": "20180206",
+ "status": "created"
+ },
+ "response-headers": {
+ "status": 200,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/fixtures/test_syntax_error.json
+{
+ "request": {
+ "api_url": "https://www.beeminder.com/api/v1/users/aaron/goals/goal/datapoints.json",
+ "method": "POST",
+ "json": {
+ "auth_token": "XXXXXX",
+ "value": "5"
+ }
+ },
+ "response": {
+ "errors": {
+ "value": [
+ "syntax error"
+ ]
+ }
+ },
+ "response-headers": {
+ "status": 422,
+ "content-type": "application/json; charset=utf-8"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/beeminder/requirements.txt
+requests

ADD
zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py
+from unittest.mock import patch, Mock
+from zulip_bots.test_lib import StubBotHandler, BotTestCase, get_bot_message_handler
+from requests.exceptions import ConnectionError
+
+class TestBeeminderBot(BotTestCase):
+ bot_name = "beeminder"
+ normal_config = {
+ "auth_token": "XXXXXX",
+ "username": "aaron",
+ "goalname": "goal"
+ }
+
+ help_message = '''
+You can add datapoints towards your beeminder goals \
+following the syntax shown below :smile:.\n \
+\n**@mention-botname daystamp, value, comment**\
+\n* `daystamp`**:** *yyyymmdd* \
+[**NOTE:** Optional field, default is *current daystamp*],\
+\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
+\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
+'''
+
+ def test_bot_responds_to_empty_message(self) -> None:
+ with self.mock_config_info(self.normal_config), \
+ self.mock_http_conversation('test_blank_input'):
+ self.verify_reply('', self.help_message)
+
+ def test_help_message(self) -> None:
+ with self.mock_config_info(self.normal_config), \
+ self.mock_http_conversation('test_help_message'):
+ self.verify_reply('help', self.help_message)
+
+ def test_normal(self) -> None:
+ bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
+ with self.mock_config_info(self.normal_config), \
+ self.mock_http_conversation('test_normal'):
+ self.verify_reply('2, hi there!', bot_response)
+
+ def test_syntax_error(self) -> None:
+ with self.mock_config_info(self.normal_config), \
+ self.mock_http_conversation('test_syntax_error'):
+ bot_response = "Make sure you follow the syntax.\n You can take a look \
+at syntax by: @mention-botname help"
+ self.verify_reply("20180303, 50, comment, redundant comment", bot_response)
+
+ def test_connection_error(self) -> None:
+ with self.mock_config_info(self.normal_config), \
+ patch('requests.post', side_effect=ConnectionError()), \
+ patch('logging.exception'):
+ self.verify_reply('?$!', 'Uh-Oh, couldn\'t process the request \
+right now.\nPlease try again later')
+
+ def test_error(self) -> None:
+ bot_request = 'notNumber'
+ bot_response = "Error occured : 422"
+ with self.mock_config_info(self.normal_config), \
+ self.mock_http_conversation('test_error'):
+ self.verify_reply(bot_request, bot_response)
+
+ def test_invalid(self) -> None:
+ bot = get_bot_message_handler(self.bot_name)
+ bot_handler = StubBotHandler()
+
+ with self.mock_config_info({'auth_token': 'someInvalidKey',
+ 'username': 'aaron',
+ 'goalname': 'goal',
+ "value": "5"}), \
+ self.mock_http_conversation('test_invalid'), \
+ self.assertRaises(bot_handler.BotQuitException):
+ bot.initialize(bot_handler)

beeminder bot: Add beeminder bot.