beeminder bot: Add beeminder bot.

This commit is contained in:
Ricky 2018-01-18 23:40:39 +05:30 committed by showell
parent 60e02ed979
commit dad7eddcc6
12 changed files with 363 additions and 0 deletions

View file

@ -0,0 +1,4 @@
[beeminder]
auth_token=9sMQV47XXidu51UEDwgB
username=rr_0410
goalname=gainweight

View file

@ -0,0 +1,101 @@
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

View file

@ -0,0 +1,63 @@
# 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`.

View file

@ -0,0 +1,17 @@
{
"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"
}
}

View file

@ -0,0 +1,21 @@
{
"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"
}
}

View file

@ -0,0 +1,17 @@
{
"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"
}
}

View file

@ -0,0 +1,20 @@
{
"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"
}
}

View file

@ -0,0 +1,28 @@
{
"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"
}
}

View file

@ -0,0 +1,21 @@
{
"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"
}
}

View file

@ -0,0 +1 @@
requests

View file

@ -0,0 +1,70 @@
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)