beeminder bot: Add beeminder bot.
This commit is contained in:
		
							parent
							
								
									60e02ed979
								
							
						
					
					
						commit
						dad7eddcc6
					
				
					 12 changed files with 363 additions and 0 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue