beeminder bot: Add beeminder bot.
This commit is contained in:
parent
60e02ed979
commit
dad7eddcc6
0
zulip_bots/zulip_bots/bots/beeminder/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/beeminder/__init__.py
Normal file
4
zulip_bots/zulip_bots/bots/beeminder/beeminder.conf
Normal file
4
zulip_bots/zulip_bots/bots/beeminder/beeminder.conf
Normal file
|
@ -0,0 +1,4 @@
|
|||
[beeminder]
|
||||
auth_token=9sMQV47XXidu51UEDwgB
|
||||
username=rr_0410
|
||||
goalname=gainweight
|
101
zulip_bots/zulip_bots/bots/beeminder/beeminder.py
Normal file
101
zulip_bots/zulip_bots/bots/beeminder/beeminder.py
Normal 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
|
63
zulip_bots/zulip_bots/bots/beeminder/doc.md
Normal file
63
zulip_bots/zulip_bots/bots/beeminder/doc.md
Normal 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`.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
1
zulip_bots/zulip_bots/bots/beeminder/requirements.txt
Normal file
1
zulip_bots/zulip_bots/bots/beeminder/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
requests
|
70
zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py
Normal file
70
zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py
Normal 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)
|
Loading…
Reference in a new issue