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