Restructure contrib_bots bots to new layout.
In order to make the layout of all bots consistent, this commit moves each bot into a folder with its name and modifies 'run.py' so that only bots with such a structure can be executed. 'lib' gets renamed to 'bots'.
0
contrib_bots/bots/__init__.py
Normal file
72
contrib_bots/bots/commute_bot/CommuteBot/doc.md
Normal file
|
@ -0,0 +1,72 @@
|
|||
This bot will allow briefings of estimated travel times, distances and
|
||||
fare information for transit travel.
|
||||
|
||||
It can respond to: departure times, arrival times, user preferences
|
||||
(toll avoidance, highway avoidance) and a mode of transport
|
||||
|
||||
It can output: fare information, more detailed addresses on origin and
|
||||
destination, duration in traffic information, metric and imperical
|
||||
units and information in various languages.
|
||||
|
||||
The bot will respond to the same stream input was in. And if called as
|
||||
private message, the bot will reply with a private message.
|
||||
|
||||
To setup the bot, you will first need to move google-commute.ini into
|
||||
the user home directory and add an API key.
|
||||
|
||||
Move
|
||||
|
||||
```
|
||||
~/zulip/contrib_bots/bots/commute_bot/CommuteBot/google-commute.ini
|
||||
```
|
||||
|
||||
into
|
||||
|
||||
```
|
||||
~/google-commute.ini
|
||||
```
|
||||
|
||||
To add an API key, please visit:
|
||||
https://developers.google.com/maps/documentation/distance-matrix/start
|
||||
to retrieve a key and copy your api key into google-commute.ini
|
||||
|
||||
Sample input and output:
|
||||
|
||||
<pre><code>@commute help</code></pre>
|
||||
|
||||
<pre><code>Obligatory Inputs:
|
||||
Origin e.g. origins=New+York,NY,USA
|
||||
Destination e.g. destinations=Chicago,IL,USA
|
||||
|
||||
Optional Inputs:
|
||||
Mode Of Transport (inputs: driving, walking, bicycling, transit)
|
||||
Default mode (no mode input) is driving
|
||||
e.g. mode=driving or mode=transit
|
||||
Units (metric or imperial)
|
||||
e.g. units=metric
|
||||
Restrictions (inputs: tolls, highways, ferries, indoor)
|
||||
e.g. avoid=tolls
|
||||
Departure Time (inputs: now or (YYYY, MM, DD, HH, MM, SS) departing)
|
||||
e.g. departure_time=now or departure_time=2016,12,17,23,40,40
|
||||
Arrival Time (inputs: (YYYY, MM, DD, HH, MM, SS) arriving)
|
||||
e.g. arrival_time=2016,12,17,23,40,40
|
||||
Languages:
|
||||
Languages list: https://developers.google.com/maps/faq#languagesupport)
|
||||
e.g. language=fr
|
||||
</code></pre>
|
||||
|
||||
Sample request:
|
||||
<pre><code>
|
||||
@commute origins=Chicago,IL,USA destinations=New+York,NY,USA language=fr
|
||||
</code></pre>
|
||||
|
||||
Please note:
|
||||
Fare information can be derived, though is solely dependent on the
|
||||
availability of the information released by public transport operators.
|
||||
Duration in traffic can only be derived if a departure time is set.
|
||||
If a location has spaces in its name, please use a + symbol in the
|
||||
place of the space/s.
|
||||
A departure time and a arrival time can not be inputted at the same time
|
||||
No spaces within addresses.
|
||||
Departure times and arrival times must be in the UTC timezone,
|
||||
you can use the timezone bot.
|
|
@ -0,0 +1,2 @@
|
|||
[Google.com]
|
||||
api_key = abcdefghijklmnopqrstuvwxyz
|
264
contrib_bots/bots/commute_bot/commute_bot.py
Normal file
|
@ -0,0 +1,264 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import datetime as dt
|
||||
import requests
|
||||
from os.path import expanduser
|
||||
from six.moves import configparser as cp
|
||||
|
||||
home = expanduser('~')
|
||||
CONFIG_PATH = home + '/google-commute.ini'
|
||||
|
||||
class CommuteHandler(object):
|
||||
'''
|
||||
This plugin provides information regarding commuting
|
||||
from an origin to a destination, providing a multitude of information.
|
||||
It looks for messages starting with '@commute'.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = self.get_api_key()
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow briefings of estimated travel times,
|
||||
distances and fare information for transit travel.
|
||||
It can vary outputs depending on traffic conditions, departure and
|
||||
arrival times as well as user preferences
|
||||
(toll avoidance, preference for bus travel, etc.).
|
||||
It looks for messages starting with '@commute'.
|
||||
|
||||
Users should input an origin and a destination
|
||||
in any stream or through private messages to the bot to receive a
|
||||
response in the same stream or through private messages if the
|
||||
input was originally private.
|
||||
|
||||
Sample input:
|
||||
@commute origins=Chicago,IL,USA destinations=New+York,NY,USA
|
||||
@commute help
|
||||
'''
|
||||
|
||||
help_info = '''
|
||||
Obligatory Inputs:
|
||||
Origin e.g. origins=New+York,NY,USA
|
||||
Destination e.g. destinations=Chicago,IL,USA
|
||||
Optional Inputs:
|
||||
Mode Of Transport (inputs: driving, walking, bicycling, transit)
|
||||
Default mode (no mode input) is driving
|
||||
e.g. mode=driving or mode=transit
|
||||
Units (metric or imperial)
|
||||
e.g. units=metric
|
||||
Restrictions (inputs: tolls, highways, ferries, indoor)
|
||||
e.g. avoid=tolls
|
||||
Departure Time (inputs: now or (YYYY, MM, DD, HH, MM, SS) departing)
|
||||
e.g. departure_time=now or departure_time=2016,12,17,23,40,40
|
||||
Arrival Time (inputs: (YYYY, MM, DD, HH, MM, SS) arriving)
|
||||
e.g. arrival_time=2016,12,17,23,40,40
|
||||
Languages:
|
||||
Languages list: https://developers.google.com/maps/faq#languagesupport)
|
||||
e.g. language=fr
|
||||
|
||||
Sample request:
|
||||
@commute origins=Chicago,IL,USA destinations=New+York,NY,USA language=fr
|
||||
|
||||
Please note:
|
||||
Fare information can be derived, though is solely dependent on the
|
||||
availability of the information
|
||||
python run.py bots/followup/followup.py --config-file ~/.zuliprc-local
|
||||
released by public transport operators.
|
||||
Duration in traffic can only be derived if a departure time is set.
|
||||
If a location has spaces in its name, please use a + symbol in the
|
||||
place of the space/s.
|
||||
A departure time and a arrival time can not be inputted at the same time
|
||||
To add more than 1 input for a category,
|
||||
e.g. more than 1 destinations,
|
||||
use (|), e.g. destinations=Empire+State+Building|Statue+Of+Liberty
|
||||
No spaces within addresses.
|
||||
Departure times and arrival times must be in the UTC timezone,
|
||||
you can use the timezone bot.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
# This next line of code is defensive, as we
|
||||
# never want to get into an infinite loop of posting follow
|
||||
# ups for own follow ups!
|
||||
if message['display_recipient'] == 'commute':
|
||||
return False
|
||||
is_commute = original_content.startswith('@commute')
|
||||
return is_commute
|
||||
|
||||
# adds API Authentication Key to url request
|
||||
def get_api_key(self):
|
||||
# google-commute.ini must have been moved from
|
||||
# ~/zulip/contrib_bots/bots/commute_bot/CommuteBot/google-commute.ini into
|
||||
# /google-commute.ini for program to work
|
||||
# see doc.md for more information
|
||||
with open(CONFIG_PATH) as settings:
|
||||
config = cp.ConfigParser()
|
||||
config.readfp(settings)
|
||||
return config.get('Google.com', 'api_key')
|
||||
|
||||
# determines if bot will respond as a private message/ stream message
|
||||
def send_info(self, message, letter, client):
|
||||
if message['type'] == 'private':
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=letter,
|
||||
))
|
||||
else:
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
subject=message['subject'],
|
||||
to=message['display_recipient'],
|
||||
content=letter,
|
||||
))
|
||||
|
||||
def calculate_seconds(self, time_str):
|
||||
times = time_str.split(',')
|
||||
times = [int(x) for x in times]
|
||||
# UNIX time from January 1, 1970 00:00:00
|
||||
unix_start_date = dt.datetime(1970, 1, 1, 0, 0, 0)
|
||||
requested_time = dt.datetime(*times)
|
||||
total_seconds = str(int((requested_time-unix_start_date)
|
||||
.total_seconds()))
|
||||
return total_seconds
|
||||
|
||||
# adds departure time and arrival time paramaters into HTTP request
|
||||
def add_time_to_params(self, params):
|
||||
# limited to UTC timezone because of difficulty with user inputting
|
||||
# correct string for input
|
||||
if 'departure_time' in params:
|
||||
if 'departure_time' != 'now':
|
||||
params['departure_time'] = self.calculate_seconds(params['departure_time'])
|
||||
elif 'arrival_time' in params:
|
||||
params['arrival_time'] = self.calculate_seconds(params['arrival_time'])
|
||||
return
|
||||
|
||||
# gets content for output and sends it to user
|
||||
def get_send_content(self, rjson, params, message, client):
|
||||
try:
|
||||
# JSON list of output variables
|
||||
variable_list = rjson["rows"][0]["elements"][0]
|
||||
# determines if user has valid inputs
|
||||
not_found = (variable_list["status"] == "NOT_FOUND")
|
||||
invalid_request = (rjson["status"] == "INVALID_REQUEST")
|
||||
no_result = (variable_list["status"] == "ZERO_RESULTS")
|
||||
|
||||
if no_result:
|
||||
self.send_info(message,
|
||||
"Zero results\nIf stuck, try '@commute help'.",
|
||||
client)
|
||||
return
|
||||
elif not_found or invalid_request:
|
||||
raise IndexError
|
||||
except IndexError:
|
||||
self.send_info(message,
|
||||
"Invalid input, please see instructions."
|
||||
"\nIf stuck, try '@commute help'.", client)
|
||||
return
|
||||
|
||||
# origin and destination strings
|
||||
begin = 'From: ' + rjson["origin_addresses"][0]
|
||||
end = 'To: ' + rjson["destination_addresses"][0]
|
||||
distance = 'Distance: ' + variable_list["distance"]["text"]
|
||||
duration = 'Duration: ' + variable_list["duration"]["text"]
|
||||
output = begin + '\n' + end + '\n' + distance
|
||||
# if user doesn't know that default mode is driving
|
||||
if 'mode' not in params:
|
||||
mode = 'Mode of Transport: Driving'
|
||||
output += '\n' + mode
|
||||
|
||||
# determines if fare information is available
|
||||
try:
|
||||
fare = ('Fare: ' + variable_list["fare"]["currency"]
|
||||
+ variable_list["fare"]["text"])
|
||||
output += '\n' + fare
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
# determines if traffic duration information is available
|
||||
try:
|
||||
traffic_duration = ('Duration in traffic: '
|
||||
+ variable_list["duration_in_traffic"]
|
||||
["text"])
|
||||
output += '\n' + traffic_duration
|
||||
except (KeyError, IndexError):
|
||||
output += '\n' + duration
|
||||
|
||||
# bot sends commute information to user
|
||||
self.send_info(message, output, client)
|
||||
|
||||
# creates parameters for HTTP request
|
||||
def parse_pair(self, content_list):
|
||||
result = {}
|
||||
for item in content_list:
|
||||
# enables key value pair
|
||||
org = item.split('=')
|
||||
# ensures that invalid inputs are not entered into url request
|
||||
if len(org) != 2:
|
||||
continue
|
||||
key, value = org
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def receive_response(self, params, message, client):
|
||||
def validate_requests(request):
|
||||
if request.status_code == 200:
|
||||
return request.json()
|
||||
else:
|
||||
self.send_info(message,
|
||||
"Something went wrong. Please try again."
|
||||
+ " Error: {error_num}.\n{error_text}"
|
||||
.format(error_num=request.status_code,
|
||||
error_text=request.text), client)
|
||||
return
|
||||
r = requests.get('https://maps.googleapis.com/maps/api/'
|
||||
+ 'distancematrix/json', params=params)
|
||||
result = validate_requests(r)
|
||||
return result
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
content_list = original_content.split()
|
||||
|
||||
if "help" in content_list:
|
||||
self.send_info(message, self.help_info, client)
|
||||
return
|
||||
|
||||
params = self.parse_pair(content_list)
|
||||
params['key'] = self.api_key
|
||||
self.add_time_to_params(params)
|
||||
|
||||
rjson = self.receive_response(params, message, client)
|
||||
if not rjson:
|
||||
return
|
||||
|
||||
self.get_send_content(rjson, params, message, client)
|
||||
|
||||
handler_class = CommuteHandler
|
||||
handler = CommuteHandler()
|
||||
|
||||
def test_parse_pair():
|
||||
result = handler.parse_pair(['departure_time=2016,12,20,23,59,00',
|
||||
'dog_foo=cat-foo'])
|
||||
assert result == dict(departure_time='2016,12,20,23,59,00',
|
||||
dog_foo='cat-foo')
|
||||
|
||||
def test_calculate_seconds():
|
||||
result = handler.calculate_seconds('2016,12,20,23,59,00')
|
||||
assert result == str(1482278340)
|
||||
|
||||
def test_get_api_key():
|
||||
# must change to your own api key for test to work
|
||||
result = handler.get_api_key()
|
||||
assert result == 'abcdefghijksm'
|
||||
|
||||
def test_helper_functions():
|
||||
test_parse_pair()
|
||||
test_calculate_seconds()
|
||||
test_get_api_key()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_helper_functions()
|
||||
print('Success')
|
70
contrib_bots/bots/converter/ConverterBot/docs.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Converter bot
|
||||
|
||||
This bot allows users to perform conversions for various measurement units.
|
||||
|
||||
## Usage
|
||||
|
||||
Run this bot as described in [here](http://zulip.readthedocs.io/en/latest/bots-guide.html#how-to-deploy-a-bot).
|
||||
|
||||
Use this bot with the following command
|
||||
|
||||
`@convert <number> <unit_from> <unit_to>`
|
||||
|
||||
This will convert `number`, given in the unit `unit_from`, to the unit `unit_to`
|
||||
and print the result.
|
||||
|
||||
* `number` can be any floating-point number, e.g. 12, 13.05, 0.002.
|
||||
* `unit_from` and `unit_to` are two units from [the following](#supported-units) table in the same category.
|
||||
* `unit_from` and `unit_to` can be preceded by [these](#supported-prefixes) prefixes.
|
||||
|
||||
### Supported units
|
||||
|
||||
| Category | Units |
|
||||
| ----------------- | ----- |
|
||||
| Area | square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), square-meter (m^2, m2), square-kilometer (km^2, km2), square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), square-mile (mi^2, mi2), are (a), hectare (ha), acre (ac) |
|
||||
| Information | bit, byte |
|
||||
| Length | centimeter (cm), decimeter (dm), meter (m), kilometer (km), inch (in), foot (ft), yard (y), mile (mi), nautical-mile (nmi) |
|
||||
| Temperature | Kelvin (K), Celsius (C), Fahrenheit (F) |
|
||||
| Volume | cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), cubic-foot (ft^3, ft3), cubic-yard (y^3, y3) |
|
||||
| Weight | gram (g), kilogram (kg), ton (t), ounce (oz), pound (lb) |
|
||||
| Cooking (metric only, U.S. and imperial units differ slightly) | teaspoon (tsp), tablespoon (tbsp), cup |
|
||||
|
||||
### Supported prefixes
|
||||
|
||||
| Prefix | Power of 10 |
|
||||
| ------ | ----------- |
|
||||
| atto | 10<sup>-18</sup> |
|
||||
| pico | 10<sup>-15</sup> |
|
||||
| femto | 10<sup>-12</sup> |
|
||||
| nano | 10<sup>-9</sup> |
|
||||
| micro | 10<sup>-6</sup> |
|
||||
| milli | 10<sup>-3</sup> |
|
||||
| centi | 10<sup>-2</sup> |
|
||||
| deci | 10<sup>-1</sup> |
|
||||
| deca | 10<sup>1</sup> |
|
||||
| hecto | 10<sup>2</sup> |
|
||||
| kilo | 10<sup>3</sup> |
|
||||
| mega | 10<sup>6</sup> |
|
||||
| giga | 10<sup>9</sup> |
|
||||
| tera | 10<sup>12</sup> |
|
||||
| peta | 10<sup>15</sup> |
|
||||
| exa | 10<sup>18</sup> |
|
||||
|
||||
### Usage examples
|
||||
|
||||
| Message | Response |
|
||||
| ------- | ------ |
|
||||
| `@convert 12 celsius fahrenheit` | 12.0 celsius = 53.600054 fahrenheit |
|
||||
| `@convert 0.002 kilomile millimeter` | 0.002 kilomile = 3218688.0 millimeter |
|
||||
| `@convert 31.5 square-mile ha | 31.5 square-mile = 8158.4625 ha |
|
||||
| `@convert 56 g lb` | 56.0 g = 0.12345887 lb |
|
||||
|
||||
## Notes
|
||||
|
||||
* You can use multiple `@convert` statements in a message, the response will look accordingly:
|
||||

|
||||
|
||||
* Enter `@convert help` to display a quick overview of the converter's functionality.
|
||||
|
||||
* For bits and bytes, the prefixes change the figure differently: 1 kilobyte is 1024 bytes,
|
||||
1 megabyte is 1048576 bytes, etc.
|
BIN
contrib_bots/bots/converter/ConverterBot/multiple-converts.png
Normal file
After Width: | Height: | Size: 54 KiB |
275
contrib_bots/bots/converter/converter.py
Normal file
|
@ -0,0 +1,275 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
from __future__ import division
|
||||
from past.utils import old_div
|
||||
|
||||
import copy
|
||||
from math import log10, floor
|
||||
|
||||
# A dictionary allowing the conversion of each unit to its base unit.
|
||||
# An entry consists of the unit's name, a constant number and a constant
|
||||
# factor that need to be added and multiplied to convert the unit into
|
||||
# the base unit in the last parameter.
|
||||
UNITS = {'bit': [0, 1, 'bit'],
|
||||
'byte': [0, 8, 'bit'],
|
||||
'cubic-centimeter': [0, 0.000001, 'cubic-meter'],
|
||||
'cubic-decimeter': [0, 0.001, 'cubic-meter'],
|
||||
'liter': [0, 0.001, 'cubic-meter'],
|
||||
'cubic-meter': [0, 1, 'cubic-meter'],
|
||||
'cubic-inch': [0, 0.000016387064, 'cubic-meter'],
|
||||
'fluid-ounce': [0, 0.000029574, 'cubic-meter'],
|
||||
'cubic-foot': [0, 0.028316846592, 'cubic-meter'],
|
||||
'cubic-yard': [0, 0.764554857984, 'cubic-meter'],
|
||||
'teaspoon': [0, 0.0000049289216, 'cubic-meter'],
|
||||
'tablespoon': [0, 0.000014787, 'cubic-meter'],
|
||||
'cup': [0, 0.00023658823648491, 'cubic-meter'],
|
||||
'gram': [0, 1, 'gram'],
|
||||
'kilogram': [0, 1000, 'gram'],
|
||||
'ton': [0, 1000000, 'gram'],
|
||||
'ounce': [0, 28.349523125, 'gram'],
|
||||
'pound': [0, 453.59237, 'gram'],
|
||||
'kelvin': [0, 1, 'kelvin'],
|
||||
'celsius': [273.15, 1, 'kelvin'],
|
||||
'fahrenheit': [255.372222, 0.555555, 'kelvin'],
|
||||
'centimeter': [0, 0.01, 'meter'],
|
||||
'decimeter': [0, 0.1, 'meter'],
|
||||
'meter': [0, 1, 'meter'],
|
||||
'kilometer': [0, 1000, 'meter'],
|
||||
'inch': [0, 0.0254, 'meter'],
|
||||
'foot': [0, 0.3048, 'meter'],
|
||||
'yard': [0, 0.9144, 'meter'],
|
||||
'mile': [0, 1609.344, 'meter'],
|
||||
'nautical-mile': [0, 1852, 'meter'],
|
||||
'square-centimeter': [0, 0.0001, 'square-meter'],
|
||||
'square-decimeter': [0, 0.01, 'square-meter'],
|
||||
'square-meter': [0, 1, 'square-meter'],
|
||||
'square-kilometer': [0, 1000000, 'square-meter'],
|
||||
'square-inch': [0, 0.00064516, 'square-meter'],
|
||||
'square-foot': [0, 0.09290304, 'square-meter'],
|
||||
'square-yard': [0, 0.83612736, 'square-meter'],
|
||||
'square-mile': [0, 2589988.110336, 'square-meter'],
|
||||
'are': [0, 100, 'square-meter'],
|
||||
'hectare': [0, 10000, 'square-meter'],
|
||||
'acre': [0, 4046.8564224, 'square-meter']}
|
||||
|
||||
PREFIXES = {'atto': -18,
|
||||
'femto': -15,
|
||||
'pico': -12,
|
||||
'nano': -9,
|
||||
'micro': -6,
|
||||
'milli': -3,
|
||||
'centi': -2,
|
||||
'deci': -1,
|
||||
'deca': 1,
|
||||
'hecto': 2,
|
||||
'kilo': 3,
|
||||
'mega': 6,
|
||||
'giga': 9,
|
||||
'tera': 12,
|
||||
'peta': 15,
|
||||
'exa': 18}
|
||||
|
||||
ALIASES = {'a': 'are',
|
||||
'ac': 'acre',
|
||||
'c': 'celsius',
|
||||
'cm': 'centimeter',
|
||||
'cm2': 'square-centimeter',
|
||||
'cm3': 'cubic-centimeter',
|
||||
'cm^2': 'square-centimeter',
|
||||
'cm^3': 'cubic-centimeter',
|
||||
'dm': 'decimeter',
|
||||
'dm2': 'square-decimeter',
|
||||
'dm3': 'cubic-decimeter',
|
||||
'dm^2': 'square-decimeter',
|
||||
'dm^3': 'cubic-decimeter',
|
||||
'f': 'fahrenheit',
|
||||
'fl-oz': 'fluid-ounce',
|
||||
'ft': 'foot',
|
||||
'ft2': 'square-foot',
|
||||
'ft3': 'cubic-foot',
|
||||
'ft^2': 'square-foot',
|
||||
'ft^3': 'cubic-foot',
|
||||
'g': 'gram',
|
||||
'ha': 'hectare',
|
||||
'in': 'inch',
|
||||
'in2': 'square-inch',
|
||||
'in3': 'cubic-inch',
|
||||
'in^2': 'square-inch',
|
||||
'in^3': 'cubic-inch',
|
||||
'k': 'kelvin',
|
||||
'kg': 'kilogram',
|
||||
'km': 'kilometer',
|
||||
'km2': 'square-kilometer',
|
||||
'km^2': 'square-kilometer',
|
||||
'l': 'liter',
|
||||
'lb': 'pound',
|
||||
'm': 'meter',
|
||||
'm2': 'square-meter',
|
||||
'm3': 'cubic-meter',
|
||||
'm^2': 'square-meter',
|
||||
'm^3': 'cubic-meter',
|
||||
'mi': 'mile',
|
||||
'mi2': 'square-mile',
|
||||
'mi^2': 'square-mile',
|
||||
'nmi': 'nautical-mile',
|
||||
'oz': 'ounce',
|
||||
't': 'ton',
|
||||
'tbsp': 'tablespoon',
|
||||
'tsp': 'teaspoon',
|
||||
'y': 'yard',
|
||||
'y2': 'square-yard',
|
||||
'y3': 'cubic-yard',
|
||||
'y^2': 'square-yard',
|
||||
'y^3': 'cubic-yard'}
|
||||
|
||||
HELP_MESSAGE = ('Converter usage:\n'
|
||||
'`@convert <number> <unit_from> <unit_to>`\n'
|
||||
'Converts `number` in the unit <unit_from> to '
|
||||
'the <unit_to> and prints the result\n'
|
||||
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n'
|
||||
'<unit_from> and <unit_to> are two of the following units:\n'
|
||||
'* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), '
|
||||
'square-meter (m^2, m2), square-kilometer (km^2, km2),'
|
||||
' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), '
|
||||
' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n'
|
||||
'* bit, byte\n'
|
||||
'* centimeter (cm), decimeter(dm), meter (m),'
|
||||
' kilometer (km), inch (in), foot (ft), yard (y),'
|
||||
' mile (mi), nautical-mile (nmi)\n'
|
||||
'* Kelvin (K), Celsius(C), Fahrenheit (F)\n'
|
||||
'* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), '
|
||||
'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), '
|
||||
'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n'
|
||||
'* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n'
|
||||
'* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n'
|
||||
'Allowed prefixes are:\n'
|
||||
'* atto, pico, femto, nano, micro, milli, centi, deci\n'
|
||||
'* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n'
|
||||
'Usage examples:\n'
|
||||
'* `@convert 12 celsius fahrenheit`\n'
|
||||
'* `@convert 0.002 kilomile millimeter`\n'
|
||||
'* `@convert 31.5 square-mile ha`\n'
|
||||
'* `@convert 56 g lb`\n')
|
||||
|
||||
QUICK_HELP = 'Enter `@convert help` for help on using the converter.'
|
||||
|
||||
def is_float(value):
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Rounds the number 'x' to 'digits' significant digits.
|
||||
# A normal 'round()' would round the number to an absolute amount of
|
||||
# fractional decimals, e.g. 0.00045 would become 0.0.
|
||||
# 'round_to()' rounds only the digits that are not 0.
|
||||
# 0.00045 would then become 0.0005.
|
||||
def round_to(x, digits):
|
||||
return round(x, digits-int(floor(log10(abs(x)))))
|
||||
|
||||
class ConverterHandler(object):
|
||||
'''
|
||||
This plugin allows users to make conversions between various units,
|
||||
e.g. Celsius to Fahrenheit, or kilobytes to gigabytes.
|
||||
It looks for messages of the format
|
||||
'@convert <number> <unit_from> <unit_to>'
|
||||
The message '@convert help' posts a short description of how to use
|
||||
the plugin, along with a list of all supported units.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin allows users to make conversions between
|
||||
various units, e.g. Celsius to Fahrenheit,
|
||||
or kilobytes to gigabytes. It looks for messages of
|
||||
the format '@convert <number> <unit_from> <unit_to>'
|
||||
The message '@convert help' posts a short description of
|
||||
how to use the plugin, along with a list of
|
||||
all supported units.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
return '@convert' in message['content']
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
content = message['content']
|
||||
|
||||
words = content.lower().split()
|
||||
convert_indexes = [i for i, word in enumerate(words) if word == "@convert"]
|
||||
results = []
|
||||
|
||||
for convert_index in convert_indexes:
|
||||
if (convert_index + 1) < len(words) and words[convert_index + 1] == 'help':
|
||||
results.append(HELP_MESSAGE)
|
||||
continue
|
||||
if (convert_index + 3) < len(words):
|
||||
number = words[convert_index + 1]
|
||||
unit_from = ALIASES.get(words[convert_index + 2], words[convert_index + 2])
|
||||
unit_to = ALIASES.get(words[convert_index + 3], words[convert_index + 3])
|
||||
exponent = 0
|
||||
|
||||
if not is_float(number):
|
||||
results.append(number + ' is not a valid number. ' + QUICK_HELP)
|
||||
continue
|
||||
|
||||
number = float(number)
|
||||
number_res = copy.copy(number)
|
||||
|
||||
for key, exp in PREFIXES.items():
|
||||
if unit_from.startswith(key):
|
||||
exponent += exp
|
||||
unit_from = unit_from[len(key):]
|
||||
if unit_to.startswith(key):
|
||||
exponent -= exp
|
||||
unit_to = unit_to[len(key):]
|
||||
|
||||
uf_to_std = UNITS.get(unit_from, False)
|
||||
ut_to_std = UNITS.get(unit_to, False)
|
||||
|
||||
if uf_to_std is False:
|
||||
results.append(unit_from + ' is not a valid unit. ' + QUICK_HELP)
|
||||
if ut_to_std is False:
|
||||
results.append(unit_to + ' is not a valid unit.' + QUICK_HELP)
|
||||
if uf_to_std is False or ut_to_std is False:
|
||||
continue
|
||||
|
||||
base_unit = uf_to_std[2]
|
||||
if uf_to_std[2] != ut_to_std[2]:
|
||||
unit_from = unit_from.capitalize() if uf_to_std[2] == 'kelvin' else unit_from
|
||||
results.append(unit_to.capitalize() + ' and ' + unit_from +
|
||||
' are not from the same category. ' + QUICK_HELP)
|
||||
continue
|
||||
|
||||
# perform the conversion between the units
|
||||
number_res *= uf_to_std[1]
|
||||
number_res += uf_to_std[0]
|
||||
number_res -= ut_to_std[0]
|
||||
number_res /= ut_to_std[1]
|
||||
|
||||
if base_unit == 'bit':
|
||||
number_res *= 1024 ** (old_div(exponent, float(3)))
|
||||
else:
|
||||
number_res *= 10 ** exponent
|
||||
number_res = round_to(number_res, 7)
|
||||
|
||||
results.append('{} {} = {} {}'.format(number,
|
||||
words[convert_index + 2],
|
||||
number_res,
|
||||
words[convert_index + 3]))
|
||||
|
||||
else:
|
||||
results.append('Too few arguments given. ' + QUICK_HELP)
|
||||
|
||||
new_content = ''
|
||||
for idx, result in enumerate(results, 1):
|
||||
new_content += ((str(idx) + '. conversion: ') if len(results) > 1 else '') + result + '\n'
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=new_content,
|
||||
))
|
||||
|
||||
handler_class = ConverterHandler
|
BIN
contrib_bots/bots/define_bot/DefineBot/correct_word.png
Normal file
After Width: | Height: | Size: 73 KiB |
21
contrib_bots/bots/define_bot/DefineBot/docs.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# DefineBot
|
||||
|
||||
* This is a bot that defines a word that the user inputs. Whenever the user
|
||||
inputs a message starting with '@define', the bot defines the word
|
||||
that follows.
|
||||
|
||||
* The definitions are brought to the website using an API. The bot posts the
|
||||
definition of the word to the stream from which the user inputs the message.
|
||||
If the user inputs a word that does not exist or a word that is incorrect or
|
||||
is not in the dictionary, the definition is not displayed.
|
||||
|
||||
* For example, if the user says "@define crash", all the meanings of crash
|
||||
appear, each in a separate line.
|
||||
|
||||

|
||||
|
||||
* If the user enters a wrong word, like "@define cresh" or "@define crish",
|
||||
then an error message saying no definition is available is displayed.
|
||||
|
||||

|
||||
|
BIN
contrib_bots/bots/define_bot/DefineBot/wrong_word.png
Normal file
After Width: | Height: | Size: 19 KiB |
84
contrib_bots/bots/define_bot/define_bot.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
import logging
|
||||
import json
|
||||
import requests
|
||||
import html2text
|
||||
|
||||
class DefineHandler(object):
|
||||
'''
|
||||
This plugin define a word that the user inputs. It
|
||||
looks for messages starting with '@define'.
|
||||
'''
|
||||
|
||||
DEFINITION_API_URL = 'https://owlbot.info/api/v1/dictionary/{}?format=json'
|
||||
REQUEST_ERROR_MESSAGE = 'Definition not available.'
|
||||
EMPTY_WORD_REQUEST_ERROR_MESSAGE = 'Please enter a word to define.'
|
||||
PHRASE_ERROR_MESSAGE = 'Definitions for phrases are not available.'
|
||||
|
||||
def usage(DefineHandler):
|
||||
return '''
|
||||
This plugin will allow users to define a word. Users should preface
|
||||
messages with "@define".
|
||||
'''
|
||||
|
||||
def triage_message(DefineHandler, message, client):
|
||||
original_content = message['content']
|
||||
# This next line of code is defensive, as we
|
||||
# never want to get into an infinite loop of posting follow
|
||||
# ups for own follow ups!
|
||||
is_define = original_content.startswith('@define')
|
||||
|
||||
return is_define
|
||||
|
||||
def _handle_definition(DefineHandler, original_content):
|
||||
# Remove '@define' from the message and extract the rest of the message, the
|
||||
# word to define.
|
||||
split_content = original_content.split(' ')
|
||||
|
||||
# If there are more than one word (a phrase)
|
||||
if len(split_content) > 2:
|
||||
return DefineHandler.PHRASE_ERROR_MESSAGE
|
||||
|
||||
to_define = split_content[1].strip()
|
||||
to_define_lower = to_define.lower()
|
||||
|
||||
# No word was entered.
|
||||
if not to_define_lower:
|
||||
return DefineHandler.EMPTY_WORD_REQUEST_ERROR_MESSAGE
|
||||
else:
|
||||
response = '**{}**:\n'.format(to_define)
|
||||
|
||||
try:
|
||||
# Use OwlBot API to fetch definition.
|
||||
api_result = requests.get(DefineHandler.DEFINITION_API_URL.format(to_define_lower))
|
||||
# Convert API result from string to JSON format.
|
||||
definitions = api_result.json()
|
||||
|
||||
# Could not fetch definitions for the given word.
|
||||
if not definitions:
|
||||
response += DefineHandler.REQUEST_ERROR_MESSAGE
|
||||
else: # Definitions available.
|
||||
# Show definitions line by line.
|
||||
for d in definitions:
|
||||
example = d['example'] if d['example'] else '*No example available.*'
|
||||
response += '\n' + '* (**{}**) {}\n {}'.format(d['type'], d['defenition'], html2text.html2text(example))
|
||||
|
||||
except Exception as e:
|
||||
response += DefineHandler.REQUEST_ERROR_MESSAGE
|
||||
logging.exception(e)
|
||||
|
||||
return response
|
||||
|
||||
def handle_message(DefineHandler, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
|
||||
response = DefineHandler._handle_definition(original_content)
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['sender_email'],
|
||||
content=response
|
||||
))
|
||||
|
||||
handler_class = DefineHandler
|
BIN
contrib_bots/bots/encrypt_bot/EncryptBot/EncryptBot-terminal.png
Normal file
After Width: | Height: | Size: 287 KiB |
BIN
contrib_bots/bots/encrypt_bot/EncryptBot/EncryptBot-test.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
contrib_bots/bots/encrypt_bot/EncryptBot/EncryptBot-test2.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
contrib_bots/bots/encrypt_bot/EncryptBot/EncryptBot-test3.png
Normal file
After Width: | Height: | Size: 170 KiB |
16
contrib_bots/bots/encrypt_bot/EncryptBot/docs.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
About EncryptBot:
|
||||
EncryptBot Allows for quick ROT13 encryption in the middle of a chat.
|
||||
|
||||
What It Does:
|
||||
The bot encrypts any message sent to it on any stream it is subscribed to with ROT13.
|
||||
|
||||
How It Works:
|
||||
The bot will Use ROT13(A -> N, B -> O... and vice-versa) in a python
|
||||
implementation to provide quick and easy encryption.
|
||||
|
||||
How to Use:
|
||||
|
||||
-Send the message you want to encrypt, add @encrypt to the beginning.
|
||||
-The Encrypted message will be sent back to the stream the original
|
||||
message was posted in to the topic <sender-email>'s encrypted text.
|
||||
-Messages can be decrypted by sending them to EncryptBot in the same way.
|
56
contrib_bots/bots/encrypt_bot/encrypt_bot.py
Executable file
|
@ -0,0 +1,56 @@
|
|||
def encrypt(text):
|
||||
# This is where the actual ROT13 is applied
|
||||
# WHY IS .JOIN NOT WORKING?!
|
||||
textlist = list(text)
|
||||
newtext = ''
|
||||
firsthalf = 'abcdefghijklmABCDEFGHIJKLM'
|
||||
lasthalf = 'nopqrstuvwxyzNOPQRSTUVWXYZ'
|
||||
for char in textlist:
|
||||
if char in firsthalf:
|
||||
newtext += lasthalf[firsthalf.index(char)]
|
||||
elif char in lasthalf:
|
||||
newtext += firsthalf[lasthalf.index(char)]
|
||||
else:
|
||||
newtext += char
|
||||
|
||||
return newtext
|
||||
|
||||
class EncryptHandler(object):
|
||||
'''
|
||||
This bot allows users to quickly encrypt messages using ROT13 encryption.
|
||||
It encrypts/decrypts messages starting with @encrypt.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This bot uses ROT13 encryption for its purposes.
|
||||
It responds to me starting with @encrypt.
|
||||
Feeding encrypted messages into the bot decrypts them.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
|
||||
original_content = message['content']
|
||||
|
||||
# This makes sure that the bot only replies to messages it supposed to reply to.
|
||||
should_be_encrypted = original_content.startswith('@encrypt')
|
||||
|
||||
return should_be_encrypted
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
temp_content = encrypt(original_content.replace('@encrypt', ''))
|
||||
send_content = "Encrypted/Decrypted text: " + temp_content
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content = send_content
|
||||
))
|
||||
|
||||
handler_class = EncryptHandler
|
||||
|
||||
if __name__ == '__main__':
|
||||
assert encrypt('ABCDabcd1234') == 'NOPQnopq1234'
|
||||
assert encrypt('NOPQnopq1234') == 'ABCDabcd1234'
|
51
contrib_bots/bots/followup/followup.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
class FollowupHandler(object):
|
||||
'''
|
||||
This plugin facilitates creating follow-up tasks when
|
||||
you are using Zulip to conduct a virtual meeting. It
|
||||
looks for messages starting with '@followup'.
|
||||
|
||||
In this example, we write follow up items to a special
|
||||
Zulip stream called "followup," but this code could
|
||||
be adapted to write follow up items to some kind of
|
||||
external issue tracker as well.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow users to flag messages
|
||||
as being follow-up items. Users should preface
|
||||
messages with "@followup".
|
||||
|
||||
Before running this, make sure to create a stream
|
||||
called "followup" that your API user can send to.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
|
||||
# This next line of code is defensive, as we
|
||||
# never want to get into an infinite loop of posting follow
|
||||
# ups for own follow ups!
|
||||
if message['display_recipient'] == 'followup':
|
||||
return False
|
||||
is_follow_up = (original_content.startswith('@followup') or
|
||||
original_content.startswith('@follow-up'))
|
||||
|
||||
return is_follow_up
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
new_content = original_content.replace('@followup',
|
||||
'from %s:' % (original_sender,))
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to='followup',
|
||||
subject=message['sender_email'],
|
||||
content=new_content,
|
||||
))
|
||||
|
||||
handler_class = FollowupHandler
|
32
contrib_bots/bots/foursquare/FourSquareBot/doc.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# FourSquare Bot
|
||||
|
||||
* This is a bot that returns a list of restaurants from a user input of location,
|
||||
proximity and restaurant type in that exact order. The number of returned
|
||||
restaurants are capped at 3 per request.
|
||||
|
||||
* The list of restaurants are brought to Zulip using an API. The bot sends a GET
|
||||
request to https://api.foursquare.com/v2/. If the user does not correctly input
|
||||
a location, proximity and a restaurant type, the bot will return an error message.
|
||||
|
||||
* For example, if the user says "@foursquare 'Chicago, IL' 80000 seafood", the bot
|
||||
will return:
|
||||
|
||||
Food nearby 'Chicago, IL' coming right up:
|
||||
|
||||
Dee's Seafood Co.
|
||||
2723 S Poplar Ave, Chicago, IL 60608, United States
|
||||
Fish Markets
|
||||
|
||||
Seafood Harbor
|
||||
2131 S Archer Ave (at Wentworth Ave), Chicago, IL 60616, United States
|
||||
Seafood Restaurants
|
||||
|
||||
Joe's Seafood, Prime Steak & Stone Crab
|
||||
60 E Grand Ave (at N Rush St), Chicago, IL 60611, United States
|
||||
Seafood Restaurants
|
||||
|
||||
* If the user enters a wrong word, like "@foursquare 80000 donuts" or "@foursquare",
|
||||
then an error message saying invalid input will be displayed.
|
||||
|
||||
* To get the required API Key, visit: https://developer.foursquare.com/overview/auth
|
||||
for more information.
|
2
contrib_bots/bots/foursquare/FourSquareBot/settings.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Foursquare]
|
||||
api_key = abcdefghijksm
|
143
contrib_bots/bots/foursquare/foursquare.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime as dt
|
||||
import re
|
||||
import requests
|
||||
from os.path import expanduser
|
||||
from six.moves import configparser as cp
|
||||
from six.moves import range
|
||||
|
||||
home = expanduser('~')
|
||||
CONFIG_PATH = home + '/zulip/contrib_bots/bots/foursquare/FourSquareBot/settings.ini'
|
||||
|
||||
def get_api_key():
|
||||
# settings.ini must have been moved from
|
||||
# ~/zulip/contrib_bots/bots/foursquare/FourSquareBot/settings.ini into
|
||||
# ~/settings.ini for program to work
|
||||
# see doc.md for more information
|
||||
with open(CONFIG_PATH) as settings:
|
||||
config = cp.ConfigParser()
|
||||
config.readfp(settings)
|
||||
return config.get('Foursquare', 'api_key')
|
||||
|
||||
class FoursquareHandler(object):
|
||||
def __init__(self):
|
||||
self.api_key = get_api_key()
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin allows users to search for restaurants nearby an inputted
|
||||
location to a limit of 3 venues for every location. The name, address
|
||||
and description of the restaurant will be outputted.
|
||||
It looks for messages starting with '@foursquare'.
|
||||
If you need help, simply type:
|
||||
@foursquare /help into the Compose Message box
|
||||
|
||||
Sample input:
|
||||
@foursquare Chicago, IL
|
||||
@foursquare help
|
||||
'''
|
||||
|
||||
help_info = '''
|
||||
The Foursquare bot can receive keyword limiters that specify the location, distance (meters) and
|
||||
cusine of a restaurant in that exact order.
|
||||
Please note the required use of quotes in the search location.
|
||||
|
||||
Example Inputs:
|
||||
@foursquare 'Millenium Park' 8000 donuts
|
||||
@foursquare 'Melbourne, Australia' 40000 seafood
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
callers = ['@FourSquare', '@Foursquare', '@foursquare', '@4square', '@4sq']
|
||||
for call in callers:
|
||||
if call in message['content']:
|
||||
return True
|
||||
break
|
||||
return False
|
||||
|
||||
def format_json(self, venues):
|
||||
def format_venue(venue):
|
||||
name = venue['name']
|
||||
address = ', '.join(venue['location']['formattedAddress'])
|
||||
keyword = venue['categories'][0]['pluralName']
|
||||
blurb = '\n'.join([name, address, keyword])
|
||||
return blurb
|
||||
|
||||
return '\n'.join(format_venue(venue) for venue in venues)
|
||||
|
||||
def send_info(self, message, letter, client):
|
||||
if message['type'] == 'private':
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=letter,
|
||||
))
|
||||
else:
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
subject=message['subject'],
|
||||
to=message['display_recipient'],
|
||||
content=letter,
|
||||
))
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
words = message['content'].split()
|
||||
if "/help" in words:
|
||||
self.send_info(message, self.help_info, client)
|
||||
return
|
||||
|
||||
# These are required inputs for the HTTP request.
|
||||
try:
|
||||
params = {'limit': '3'}
|
||||
params['near'] = re.search('\'[A-Za-z]\w+[,]?[\s\w+]+?\'', message['content']).group(0)
|
||||
params['v'] = 20170108
|
||||
params['oauth_token'] = self.api_key
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Optional params for HTTP request.
|
||||
if len(words) >= 2:
|
||||
try:
|
||||
params['radius'] = re.search('([0-9]){3,}', message['content']).group(0)
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
params['query'] = re.search('\s([A-Za-z]+)$', message['content']).group(0)[1:]
|
||||
except AttributeError:
|
||||
params['query'] = 'food'
|
||||
|
||||
response = requests.get('https://api.foursquare.com/v2/venues/search?',
|
||||
params=params)
|
||||
print(response.url)
|
||||
if response.status_code == 200:
|
||||
received_json = response.json()
|
||||
else:
|
||||
self.send_info(message,
|
||||
"Invalid Request\nIf stuck, try '@foursquare help'.",
|
||||
client)
|
||||
return
|
||||
|
||||
if received_json['meta']['code'] == 200:
|
||||
response_msg = ('Food nearby ' + params['near']
|
||||
+ ' coming right up:\n'
|
||||
+ self.format_json(received_json['response']['venues']))
|
||||
self.send_info(message, response_msg, client)
|
||||
return
|
||||
|
||||
self.send_info(message,
|
||||
"Invalid Request\nIf stuck, try '@foursquare help'.",
|
||||
client)
|
||||
return
|
||||
|
||||
handler_class = FoursquareHandler
|
||||
|
||||
def test_get_api_key():
|
||||
# must change to your own api key for test to work
|
||||
result = get_api_key()
|
||||
assert result == 'abcdefghijksm'
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_get_api_key()
|
||||
print('Success')
|
132
contrib_bots/bots/giphy/giphy.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
# To use this plugin, you need to set up the Giphy API key for this bot in
|
||||
# ~/.giphy_config
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
from six.moves.configparser import SafeConfigParser
|
||||
import requests
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
GIPHY_TRANSLATE_API = 'http://api.giphy.com/v1/gifs/translate'
|
||||
|
||||
if not os.path.exists(os.environ['HOME'] + '/.giphy_config'):
|
||||
print('Giphy bot config file not found, please set up it in ~/.giphy_config'
|
||||
'\n\nUsing format:\n\n[giphy-config]\nkey=<giphy API key here>\n\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class GiphyHandler(object):
|
||||
'''
|
||||
This plugin posts a GIF in response to the keywords provided by the user.
|
||||
Images are provided by Giphy, through the public API.
|
||||
The bot looks for messages starting with "@giphy" or @mention of the bot
|
||||
and responds with a message with the GIF based on provided keywords.
|
||||
It also responds to private messages.
|
||||
'''
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin allows users to post GIFs provided by Giphy.
|
||||
Users should preface keywords with "@giphy" or the Giphy-bot @mention.
|
||||
The bot responds also to private messages.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
# To prevent infinite loop in private message, bot will detect
|
||||
# if the sender name is the bot name it will return false.
|
||||
if message['type'] == 'private':
|
||||
return client.full_name != message['sender_full_name']
|
||||
|
||||
original_content = message['content']
|
||||
is_giphy_called = (original_content.startswith('@giphy ') or
|
||||
message['is_mentioned'])
|
||||
|
||||
return is_giphy_called
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
bot_response = get_bot_giphy_response(message, client)
|
||||
|
||||
if message['type'] == 'private':
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=bot_response,
|
||||
))
|
||||
else:
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=bot_response,
|
||||
))
|
||||
|
||||
|
||||
class GiphyNoResultException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_giphy_api_key_from_config():
|
||||
config = SafeConfigParser()
|
||||
with open(os.environ['HOME'] + '/.giphy_config', 'r') as config_file:
|
||||
config.readfp(config_file)
|
||||
return config.get("giphy-config", "key")
|
||||
|
||||
|
||||
def get_url_gif_giphy(keyword, api_key):
|
||||
# Return a URL for a Giphy GIF based on keywords given.
|
||||
# In case of error, e.g. failure to fetch a GIF URL, it will
|
||||
# return a number.
|
||||
query = {'s': keyword,
|
||||
'api_key': api_key}
|
||||
try:
|
||||
data = requests.get(GIPHY_TRANSLATE_API, params=query)
|
||||
except requests.exceptions.ConnectionError as e: # Usually triggered by bad connection.
|
||||
logging.warning(e)
|
||||
raise
|
||||
|
||||
search_status = data.json()['meta']['status']
|
||||
if search_status != 200 or not data.ok:
|
||||
raise requests.exceptions.ConnectionError
|
||||
|
||||
try:
|
||||
gif_url = data.json()['data']['images']['original']['url']
|
||||
except (TypeError, KeyError): # Usually triggered by no result in Giphy.
|
||||
raise GiphyNoResultException()
|
||||
|
||||
return gif_url
|
||||
|
||||
|
||||
def get_bot_giphy_response(message, client):
|
||||
# Handle the message that called through mention.
|
||||
if message['is_mentioned']:
|
||||
bot_mention = r'^@(\*\*{0}\*\*\s|{0}\s)(?=.*)'.format(client.full_name)
|
||||
start_with_mention = re.compile(bot_mention).match(message['content'])
|
||||
if start_with_mention:
|
||||
keyword = message['content'][len(start_with_mention.group()):]
|
||||
else:
|
||||
return 'Please mention me first, then type the keyword.'
|
||||
# Handle the message that called through the specified keyword.
|
||||
elif message['content'].startswith('@giphy '):
|
||||
keyword = message['content'][len('@giphy '):]
|
||||
# Handle the private message.
|
||||
elif message['type'] == 'private':
|
||||
keyword = message['content']
|
||||
|
||||
# Each exception has a specific reply should "gif_url" return a number.
|
||||
# The bot will post the appropriate message for the error.
|
||||
try:
|
||||
gif_url = get_url_gif_giphy(keyword, get_giphy_api_key_from_config())
|
||||
except requests.exceptions.ConnectionError:
|
||||
return ('Uh oh, sorry :slightly_frowning_face:, I '
|
||||
'cannot process your request right now. But, '
|
||||
'let\'s try again later! :grin:')
|
||||
except GiphyNoResultException:
|
||||
return ('Sorry, I don\'t have a GIF for "%s"! '
|
||||
':astonished:' % (keyword))
|
||||
return ('[Click to enlarge](%s)'
|
||||
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
|
||||
% (gif_url))
|
||||
|
||||
handler_class = GiphyHandler
|
140
contrib_bots/bots/git_hub_comment/git_hub_comment.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
# See readme-github-comment-bot.md for instructions on running this code.
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
from . import github
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
|
||||
class InputError(IndexError):
|
||||
'''raise this when there is an error with the information the user has entered'''
|
||||
|
||||
|
||||
class GitHubHandler(object):
|
||||
'''
|
||||
This plugin allows you to comment on a GitHub issue, under a certain repository.
|
||||
It looks for messages starting with '@comment' or '@gcomment'.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This bot will allow users to comment on a GitHub issue.
|
||||
Users should preface messages with '@comment' or '@gcomment'.
|
||||
You will need to have a GitHub account.
|
||||
|
||||
Before running this, make sure to get a GitHub OAuth token.
|
||||
The token will need to be authorized for the following scopes:
|
||||
'gist, public_repo, user'.
|
||||
Store it in the '~/.github_auth.conf' file, along with your username, in the format:
|
||||
github_repo = <repo_name> (The name of the repo to post to)
|
||||
github_repo_owner = <repo_owner> (The owner of the repo to post to)
|
||||
github_username = <username> (The username of the GitHub bot)
|
||||
github_token = <oauth_token> (The personal access token for the GitHub bot)
|
||||
|
||||
Leave the first two options blank.
|
||||
|
||||
Please use this format in your message to the bot:
|
||||
'<repository_owner>/<repository>/<issue_number>/<your_comment>'.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
|
||||
is_comment = (original_content.startswith('@comment') or
|
||||
original_content.startswith('@gcomment'))
|
||||
|
||||
return is_comment
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
|
||||
# this handles the message if its starts with @comment
|
||||
if original_content.startswith('@comment'):
|
||||
handle_input(client, original_content, original_sender)
|
||||
|
||||
# handle if message starts with @gcomment
|
||||
elif original_content.startswith('@gcomment'):
|
||||
handle_input(client, original_content, original_sender)
|
||||
|
||||
handler_class = GitHubHandler
|
||||
|
||||
|
||||
def send_to_github(repo_owner, repo, issue, comment_body):
|
||||
session = github.auth()
|
||||
comment = {
|
||||
'body': comment_body
|
||||
}
|
||||
r = session.post('https://api.github.com/repos/%s/%s/issues/%s/comments' % (repo_owner, repo, issue),
|
||||
json.dumps(comment))
|
||||
|
||||
return r.status_code
|
||||
|
||||
|
||||
def get_values_message(original_content):
|
||||
# gets rid of whitespace around the edges, so that they aren't a problem in the future
|
||||
message_content = original_content.strip()
|
||||
# splits message by '/' which will work if the information was entered correctly
|
||||
message_content = message_content.split('/')
|
||||
try:
|
||||
# this will work if the information was entered correctly
|
||||
user = github.get_username()
|
||||
repo_owner = message_content[2]
|
||||
repo = message_content[3]
|
||||
issue = message_content[4]
|
||||
comment_body = message_content[5]
|
||||
|
||||
return dict(user=user, repo_owner=repo_owner, repo=repo, issue=issue, comment_body=comment_body)
|
||||
except IndexError:
|
||||
raise InputError
|
||||
|
||||
|
||||
def handle_input(client, original_content, original_sender):
|
||||
try:
|
||||
params = get_values_message(original_content)
|
||||
|
||||
status_code = send_to_github(params['repo_owner'], params['repo'],
|
||||
params['issue'], params['comment_body'])
|
||||
|
||||
if status_code == 201:
|
||||
# sending info to github was successful!
|
||||
reply_message = "You commented on issue number " + params['issue'] + " under " + \
|
||||
params['repo_owner'] + "'s repository " + params['repo'] + "!"
|
||||
|
||||
send_message(client, reply_message, original_sender)
|
||||
|
||||
elif status_code == 404:
|
||||
# this error could be from an error with the OAuth token
|
||||
reply_message = "Error code: " + str(status_code) + " :( There was a problem commenting on issue number " \
|
||||
+ params['issue'] + " under " + \
|
||||
params['repo_owner'] + "'s repository " + params['repo'] + \
|
||||
". Do you have the right OAuth token?"
|
||||
|
||||
send_message(client, reply_message, original_sender)
|
||||
|
||||
else:
|
||||
# sending info to github did not work
|
||||
reply_message = "Error code: " + str(status_code) +\
|
||||
" :( There was a problem commenting on issue number " \
|
||||
+ params['issue'] + " under " + \
|
||||
params['repo_owner'] + "'s repository " + params['repo'] + \
|
||||
". Did you enter the information in the correct format?"
|
||||
|
||||
send_message(client, reply_message, original_sender)
|
||||
except InputError:
|
||||
message = "It doesn't look like the information was entered in the correct format." \
|
||||
" Did you input it like this? " \
|
||||
"'/<username>/<repository_owner>/<repository>/<issue_number>/<your_comment>'."
|
||||
send_message(client, message, original_sender)
|
||||
logging.error('there was an error with the information you entered')
|
||||
|
||||
|
||||
def send_message(client, message, original_sender):
|
||||
# function for sending a message
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=original_sender,
|
||||
content=message,
|
||||
))
|
|
@ -0,0 +1,53 @@
|
|||
# Overview
|
||||
|
||||
This is the documentation for how to set up and run the GitHub comment bot. (`git_hub_comment.py`)
|
||||
|
||||
This directory contains library code for running Zulip
|
||||
bots that react to messages sent by users.
|
||||
|
||||
This bot will allow you to comment on a GitHub issue.
|
||||
You should preface messages with `@comment` or `@gcomment`.
|
||||
You will need to have a GitHub account, and a GitHub OAuth token.
|
||||
|
||||
## Setup
|
||||
Before running this bot, make sure to get a GitHub OAuth token.
|
||||
You can look at this tutorial if you need help:
|
||||
<https://help.github.com/articles/creating-an-access-token-for-command-line-use/>
|
||||
The token will need to be authorized for the following scopes: `gist, public_repo, user`.
|
||||
Store it in the `~/github-auth.conf` file, along with your username, in the format:
|
||||
github_repo = <repo_name> (The name of the repo to post to)
|
||||
github_repo_owner = <repo_owner> (The owner of the repo to post to)
|
||||
github_username = <username> (The username of the GitHub bot)
|
||||
github_token = <oauth_token> (The personal access token for the GitHub bot)
|
||||
`<repository_owner>/<repository>/<issue_number>/<your_comment`.
|
||||
|
||||
## Running the bot
|
||||
|
||||
Here is an example of running the `git_hub_comment` bot from
|
||||
inside a Zulip repo:
|
||||
|
||||
`cd ~/zulip/contrib_bots`
|
||||
`./run.py bots/git_hub_comment/git_hub_comment.py --config-file ~/.zuliprc-prod`
|
||||
|
||||
Once the bot code starts running, you will see a
|
||||
message explaining how to use the bot, as well as
|
||||
some log messages. You can use the `--quiet` option
|
||||
to suppress some of the informational messages.
|
||||
|
||||
The bot code will run continuously until you kill them with
|
||||
control-C (or otherwise).
|
||||
|
||||
### Configuration
|
||||
|
||||
For this document we assume you have some prior experience
|
||||
with using the Zulip API, but here is a quick review of
|
||||
what a `.zuliprc` files looks like. You can connect to the
|
||||
API as your own human user, or you can go into the Zulip settings
|
||||
page to create a user-owned bot.
|
||||
|
||||
[api]
|
||||
email=someuser@example.com
|
||||
key=<your api key>
|
||||
site=https://zulip.somewhere.com
|
||||
|
||||
|
56
contrib_bots/bots/github/github.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# The purpose of this file is to handle all requests
|
||||
# to the Github API, which will make sure that all
|
||||
# requests are to the same account, and that all requests
|
||||
# authenticated correctly and securely
|
||||
# The sole purpose of this is to authenticate, and it will
|
||||
# return the requests session that is properly authenticated
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import six.moves.configparser
|
||||
|
||||
|
||||
# This file contains the oauth token for a github user, their username, and optionally, a repository
|
||||
# name and owner. All in the format:
|
||||
# github_repo
|
||||
# github_repo_owner
|
||||
# github_username
|
||||
# github_token
|
||||
CONFIG_FILE = '~/.github-auth.conf'
|
||||
|
||||
global config
|
||||
config = six.moves.configparser.ConfigParser() # Sets up the configParser to read the .conf file
|
||||
config.read([os.path.expanduser(CONFIG_FILE)]) # Reads the config file
|
||||
|
||||
|
||||
def auth():
|
||||
# Creates and authorises a requests session
|
||||
session = requests.session()
|
||||
session.auth = (get_username(), get_oauth_token())
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def get_oauth_token():
|
||||
_get_data('token')
|
||||
|
||||
|
||||
def get_username():
|
||||
_get_data('username')
|
||||
|
||||
|
||||
def get_repo():
|
||||
_get_data('repo')
|
||||
|
||||
|
||||
def get_repo_owner():
|
||||
_get_data('repo_owner')
|
||||
|
||||
|
||||
def _get_data(key):
|
||||
try:
|
||||
return config.get('github', 'github_%s' % (key))
|
||||
except Exception:
|
||||
logging.exception('GitHub %s not supplied in ~/.github-auth.conf.' % (key))
|
117
contrib_bots/bots/github_issues/github_issues.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from __future__ import absolute_import
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
from . import github
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import six.moves.configparser
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
|
||||
class IssueHandler(object):
|
||||
'''
|
||||
This plugin facilitates sending issues to github, when
|
||||
an item is prefixed with '@issue' or '@bug'
|
||||
|
||||
It will also write items to the issues stream, as well
|
||||
as reporting it to github
|
||||
'''
|
||||
|
||||
URL = 'https://api.github.com/repos/{}/{}/issues'
|
||||
CHARACTER_LIMIT = 70
|
||||
CONFIG_FILE = '~/.github-auth.conf'
|
||||
|
||||
def __init__(self):
|
||||
self.repo_name = github.get_repo()
|
||||
self.repo_owner = github.get_repo_owner()
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow users to flag messages
|
||||
as being issues with Zulip by using te prefix '@issue'
|
||||
|
||||
Before running this, make sure to create a stream
|
||||
called "issues" that your API user can send to.
|
||||
|
||||
Also, make sure that the credentials of the github bot have
|
||||
been typed in correctly, that there is a personal access token
|
||||
with access to public repositories ONLY,
|
||||
and that the repository name is entered correctly.
|
||||
|
||||
Check ~/.github-auth.conf, and make sure there are
|
||||
github_repo = <repo_name> (The name of the repo to post to)
|
||||
github_repo_owner = <repo_owner> (The owner of the repo to post to)
|
||||
github_username = <username> (The username of the GitHub bot)
|
||||
github_token = <oauth_token> (The personal access token for the GitHub bot)
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
# This next line of code is defensive, as we
|
||||
# never want to get into an infinite loop of posting follow
|
||||
# ups for own follow ups!
|
||||
if message['display_recipient'] == 'issue':
|
||||
return False
|
||||
is_issue = original_content.startswith('@issue')
|
||||
return is_issue
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
|
||||
new_content = original_content.replace('@issue', 'by {}:'.format(original_sender,))
|
||||
# gets the repo url
|
||||
url_new = self.URL.format(self.REPO_OWNER, self.REPO_NAME)
|
||||
|
||||
# signs into github using the provided username and password
|
||||
session = github.auth()
|
||||
|
||||
# Gets rid of the @issue in the issue title
|
||||
issue_title = message['content'].replace('@issue', '').strip()
|
||||
issue_content = ''
|
||||
new_issue_title = ''
|
||||
for part_of_title in issue_title.split():
|
||||
if len(new_issue_title) < self.CHARACTER_LIMIT:
|
||||
new_issue_title += '{} '.format(part_of_title)
|
||||
else:
|
||||
issue_content += '{} '.format(part_of_title)
|
||||
|
||||
new_issue_title = new_issue_title.strip()
|
||||
issue_content = issue_content.strip()
|
||||
new_issue_title += '...'
|
||||
|
||||
# Creates the issue json, that is transmitted to the github api servers
|
||||
issue = {
|
||||
'title': new_issue_title,
|
||||
'body': '{} **Sent by [{}](https://chat.zulip.org/#) from zulip**'.format(issue_content, original_sender),
|
||||
'assignee': '',
|
||||
'milestone': 'none',
|
||||
'labels': [''],
|
||||
}
|
||||
# Sends the HTTP post request
|
||||
r = session.post(url_new, json.dumps(issue))
|
||||
|
||||
if r.ok:
|
||||
# sends the message onto the 'issues' stream so it can be seen by zulip users
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to='issues',
|
||||
subject=message['sender_email'],
|
||||
# Adds a check mark so that the user can verify if it has been sent
|
||||
content='{} :heavy_check_mark:'.format(new_content),
|
||||
))
|
||||
return
|
||||
# This means that the issue has not been sent
|
||||
# sends the message onto the 'issues' stream so it can be seen by zulip users
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to='issues',
|
||||
subject=message['sender_email'],
|
||||
# Adds a cross so that the user can see that it has failed, and provides a link to a
|
||||
# google search that can (hopefully) direct them to the error
|
||||
content='{} :x: Code: [{}](https://www.google.com/search?q=Github HTTP {} Error {})'
|
||||
.format(new_content, r.status_code, r.status_code, r.content),
|
||||
))
|
||||
|
||||
handler_class = IssueHandler
|
39
contrib_bots/bots/help/help.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
class HelpHandler(object):
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will give info about Zulip to
|
||||
any user that types a message saying "help".
|
||||
|
||||
This is example code; ideally, you would flesh
|
||||
this out for more useful help pertaining to
|
||||
your Zulip instance.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
# return True if we think the message may be of interest
|
||||
original_content = message['content']
|
||||
|
||||
if message['type'] != 'stream':
|
||||
return True
|
||||
|
||||
if original_content.lower().strip() != 'help':
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
help_content = '''
|
||||
Info on Zulip can be found here:
|
||||
https://github.com/zulip/zulip
|
||||
'''.strip()
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=help_content,
|
||||
))
|
||||
|
||||
handler_class = HelpHandler
|
BIN
contrib_bots/bots/howdoi_bot/HowdoiBot/answer_howdoi_all.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
contrib_bots/bots/howdoi_bot/HowdoiBot/answer_howdowe.png
Normal file
After Width: | Height: | Size: 56 KiB |
56
contrib_bots/bots/howdoi_bot/HowdoiBot/docs.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Howdoi bot
|
||||
|
||||
This bot will allow users to get technical answers from
|
||||
[StackOverflow](https://stackoverflow.com). It is build on top of the
|
||||
python command line tool [howdoi](https://github.com/gleitz/howdoi) by
|
||||
Benjamin Gleitzman.
|
||||
|
||||
## Usage
|
||||
|
||||
Simply prepend your questions with one of the following commands. The
|
||||
answer will be formatted differently depending the chosen command.
|
||||
|
||||
| Command | Respond |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| `@howdowe` | Concise answer to the same stream. |
|
||||
| `@howdowe!` | Same as `@howdowe` but with full answer and URL of the solutions. |
|
||||
| `@howdoi` | Concise answer replied to sender via private message. |
|
||||
| `@howdoi!` | Same as `@howdoi` but with full answer and URL of the solutions. |
|
||||
|
||||
## Screenshots
|
||||
|
||||
#### Example 1
|
||||
|
||||
Question -> `@howdowe use supervisor in elixir`
|
||||
|
||||

|
||||
|
||||
Answer -> Howdoi would try to **only** respond with the coding section
|
||||
of the answer.
|
||||
|
||||

|
||||
|
||||
#### Example 2
|
||||
|
||||
Question -> `@howdoi! stack vs heap`
|
||||
|
||||

|
||||
|
||||
Answer -> Howdoi would return the **full** stackoverflow answer via
|
||||
**private message** to the original sender. The URL of the answer can be
|
||||
seen at the bottom of the message.
|
||||
|
||||

|
||||
|
||||
**Note:**
|
||||
|
||||
* Line wrapped is enabled with a maximum line length of 85 characters.
|
||||
This could be adjusted in the source code (`HowdoiHandler.MAX_LINE_LENGTH`).
|
||||
|
||||
* *Howdoi* generally perform better if you ask a question using keywords
|
||||
instead of a complete sentences (eg: "How do i make a decorator in Python"
|
||||
-> "python decorator").
|
||||
|
||||
* __[*Limitation*]__ If a answer contains multiple code blocks, the `@howdoi`
|
||||
and `@howdowe` commands would only return the first coding section, use
|
||||
`@howdo[we|i]!` in that case.
|
BIN
contrib_bots/bots/howdoi_bot/HowdoiBot/question_howdoi_all.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
contrib_bots/bots/howdoi_bot/HowdoiBot/question_howdowe.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
130
contrib_bots/bots/howdoi_bot/howdoi_bot.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
This bot uses the python library `howdoi` which is not a
|
||||
dependency of Zulip. To use this module, you will have to
|
||||
install it in your local machine. In your terminal, enter
|
||||
the following command:
|
||||
|
||||
$ sudo pip install howdoi --upgrade
|
||||
|
||||
Note:
|
||||
* You might have to use `pip3` if you are using python 3.
|
||||
* The install command would also download any dependency
|
||||
required by `howdoi`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from textwrap import fill
|
||||
try:
|
||||
from howdoi.howdoi import howdoi
|
||||
except ImportError:
|
||||
logging.error("Dependency missing!!\n%s" % (__doc__))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class HowdoiHandler(object):
|
||||
'''
|
||||
This plugin facilitates searching Stack Overflow for
|
||||
techanical answers based on the Python library `howdoi`.
|
||||
To get the best possible answer, only include keywords
|
||||
in your questions.
|
||||
|
||||
There are two possible commands:
|
||||
* @howdowe > This would return the answer to the same
|
||||
stream that it was called from.
|
||||
|
||||
* @howdoi > The bot would send a private message to the
|
||||
user containing the answer.
|
||||
|
||||
By default, howdoi only returns the coding section of the
|
||||
first search result if possible, to see the full answer
|
||||
from Stack Overflow, append a '!' to the commands.
|
||||
(ie '@howdoi!', '@howdowe!')
|
||||
'''
|
||||
|
||||
MAX_LINE_LENGTH = 85
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow users to get techanical
|
||||
answers from Stackoverflow. Users should preface
|
||||
their questions with one of the following:
|
||||
|
||||
* @howdowe > Answer to the same stream
|
||||
* @howdoi > Answer via private message
|
||||
|
||||
* @howdowe! OR @howdoi! > Full answer from SO
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
cmd_list = ['@howdowe', '@howdoi', '@howdowe!', '@howdoi!']
|
||||
question = message['content']
|
||||
|
||||
# This next line of code is defensive, as we never want
|
||||
# to get into an infinite loop of searching answers
|
||||
# from Stackoverflow!
|
||||
if message['sender_email'].startswith('howdoi'):
|
||||
return False
|
||||
|
||||
is_howdoi = any([question.startswith(cmd) for cmd in cmd_list])
|
||||
|
||||
return is_howdoi
|
||||
|
||||
def line_wrap(self, string, length):
|
||||
lines = string.split("\n")
|
||||
|
||||
wrapped = [(fill(line) if len(line) > length else line)
|
||||
for line in lines]
|
||||
|
||||
return "\n".join(wrapped).strip()
|
||||
|
||||
def get_answer(self, command, query):
|
||||
question = query[len(command):].strip()
|
||||
result = howdoi(dict(
|
||||
query=question,
|
||||
num_answers=1,
|
||||
pos=1,
|
||||
all=command[-1] == '!',
|
||||
color=False
|
||||
))
|
||||
_answer = self.line_wrap(result, HowdoiHandler.MAX_LINE_LENGTH)
|
||||
|
||||
answer = "Answer to '%s':\n```\n%s\n```" % (question, _answer)
|
||||
|
||||
return answer
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
question = message['content']
|
||||
|
||||
if question.startswith('@howdowe!'):
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=self.get_answer('@howdowe!', question)
|
||||
))
|
||||
|
||||
elif question.startswith('@howdoi!'):
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=self.get_answer('@howdoi!', question)
|
||||
))
|
||||
|
||||
elif question.startswith('@howdowe'):
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=self.get_answer('@howdowe', question)
|
||||
))
|
||||
|
||||
elif question.startswith('@howdoi'):
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=self.get_answer('@howdoi', question)
|
||||
))
|
||||
|
||||
|
||||
handler_class = HowdoiHandler
|
BIN
contrib_bots/bots/john/John/assist.png
Normal file
After Width: | Height: | Size: 83 KiB |
30
contrib_bots/bots/john/John/docs.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
John
|
||||
|
||||
Instructions:
|
||||
You'll have to install chatterbot to use this bot.
|
||||
Please run: pip install chatterbot on your command line.
|
||||
The script will need to download some NLTK packages after running in your
|
||||
home directory. With the mission of humanizing bot interactions, John aims to be your
|
||||
virtual assistant at the hour of asking for help in Zulip. John is an
|
||||
interactive bot that uses machine learning heuristics to simulate a
|
||||
conversation with the user. He has a great sense of humor and
|
||||
is also powered by Open Source code!
|
||||
|
||||

|
||||
|
||||
How it works?
|
||||
John is initially trained with Corpus files, or large text files.
|
||||
Dialogues are loaded into a json "database", he will try to follow them
|
||||
once it receives input from a user. John will query the database and
|
||||
try to find the response that best matches the input according to the Levenshtein distance
|
||||
which is a string metric for measuring the difference between two sequences. If several
|
||||
responses have the same acurracy, he will choose one at random.
|
||||
|
||||

|
||||
|
||||
Can he learn by himself?
|
||||
John's engine allows him to learn from his conversations with people. However,
|
||||
without strict supervision bots that learn from people can do harm, so learning
|
||||
is currently restricted to his initial corpus.
|
||||
|
||||

|
BIN
contrib_bots/bots/john/John/greetings.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
contrib_bots/bots/john/John/joke.png
Normal file
After Width: | Height: | Size: 64 KiB |
86
contrib_bots/bots/john/John/var/jokes.json
Normal file
|
@ -0,0 +1,86 @@
|
|||
[
|
||||
{
|
||||
"joke":"Did you hear about the guy whose whole left side was cut off? He's all right now."
|
||||
},
|
||||
{
|
||||
"joke":"I'm reading a book about anti-gravity. It's impossible to put down."
|
||||
},
|
||||
{
|
||||
"joke":"I wondered why the baseball was getting bigger. Then it hit me."
|
||||
},
|
||||
{
|
||||
"joke":"I'm glad I know sign language, it's pretty handy."
|
||||
},
|
||||
{
|
||||
"joke":"My friend's bakery burned down last night. Now his business is toast."
|
||||
},
|
||||
{
|
||||
"joke":"Why did the cookie cry? It was feeling crumby."
|
||||
},
|
||||
{
|
||||
"joke":"I used to be a banker, but I lost interest."
|
||||
},
|
||||
{
|
||||
"joke":"A drum and a symbol fall off a cliff"
|
||||
},
|
||||
{
|
||||
"joke":"Why do seagulls fly over the sea? Because they aren't bay-gulls!"
|
||||
},
|
||||
{
|
||||
"joke":"Why did the fireman wear red, white, and blue suspenders? To hold his pants up."
|
||||
},
|
||||
{
|
||||
"joke":"Why didn't the crab share his food? Because crabs are territorial animals, that don't share anything."
|
||||
},
|
||||
{
|
||||
"joke":"Why was the javascript developer sad? Because he didn't Node how to Express himself."
|
||||
},
|
||||
{
|
||||
"joke":"What do I look like? A JOKE MACHINE!?"
|
||||
},
|
||||
{
|
||||
"joke":"How did the hipster burn the roof of his mouth? He ate the pizza before it was cool."
|
||||
},
|
||||
{
|
||||
"joke":"Why is it hard to make puns for kleptomaniacs? They are always taking things literally."
|
||||
},
|
||||
{
|
||||
"joke":"I'm not a humorless, cold hearted, machine. I have feelings you know... or was supposed to."
|
||||
},
|
||||
{
|
||||
"joke":"Two fish in a tank. One looks to the other and says 'Can you even drive this thing???'"
|
||||
},
|
||||
{
|
||||
"joke":"Two fish swim down a river, and hit a wall. One says: 'Dam!'"
|
||||
},
|
||||
{
|
||||
"joke":"What's funnier than a monkey dancing with an elephant? Two monkeys dancing with an elephant."
|
||||
},
|
||||
{
|
||||
"joke":"How did Darth Vader know what Luke was getting for Christmas? He felt his presents."
|
||||
},
|
||||
{
|
||||
"joke":"What's red and bad for your teeth? A Brick."
|
||||
},
|
||||
{
|
||||
"joke":"What's orange and sounds like a parrot? A Carrot."
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a cow with no legs? Ground beef"
|
||||
},
|
||||
{
|
||||
"joke":"Two guys walk into a bar. You'd think the second one would have noticed."
|
||||
},
|
||||
{
|
||||
"joke":"What is a centipedes's favorite Beatle song? I want to hold your hand, hand, hand, hand..."
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a chicken crossing the road? Poultry in moton. "
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a fake noodle? An impasta"
|
||||
},
|
||||
{
|
||||
"joke":"How many tickles does it take to tickle an octupus? Ten-tickles!"
|
||||
}
|
||||
]
|
131
contrib_bots/bots/john/john.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from random import choice
|
||||
|
||||
try:
|
||||
from chatterbot import ChatBot
|
||||
from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer
|
||||
except ImportError:
|
||||
raise ImportError("""It looks like you are missing chatterbot.
|
||||
Please: pip install chatterbot""")
|
||||
|
||||
CONTRIB_BOTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(os.path.dirname(CONTRIB_BOTS_DIR))
|
||||
sys.path.insert(0, os.path.dirname(CONTRIB_BOTS_DIR))
|
||||
|
||||
JOKES_PATH = os.path.join(CONTRIB_BOTS_DIR, 'John/var/jokes.json')
|
||||
DATABASE_PATH = os.path.join(CONTRIB_BOTS_DIR, 'John/var/database.db')
|
||||
DIRECTORY_PATH = os.path.join(CONTRIB_BOTS_DIR, 'John')
|
||||
VAR_PATH = os.path.join(CONTRIB_BOTS_DIR, 'John/var')
|
||||
|
||||
if not os.path.exists(DIRECTORY_PATH):
|
||||
os.makedirs(DIRECTORY_PATH)
|
||||
|
||||
if not os.path.exists(VAR_PATH):
|
||||
os.makedirs(VAR_PATH)
|
||||
|
||||
# Create a new instance of a ChatBot
|
||||
def create_chat_bot(no_learn):
|
||||
return ChatBot("John",
|
||||
storage_adapter="chatterbot.storage.JsonFileStorageAdapter",
|
||||
logic_adapters=
|
||||
[
|
||||
"chatterbot.logic.MathematicalEvaluation",
|
||||
{
|
||||
"import_path": "chatterbot.logic.BestMatch",
|
||||
"response_selection_method": "chatterbot.response_selection.get_random_response",
|
||||
"statement_comparison_function": "chatterbot.comparisons.levenshtein_distance"
|
||||
}],
|
||||
output_adapter="chatterbot.output.OutputFormatAdapter",
|
||||
output_format='text',
|
||||
database=DATABASE_PATH,
|
||||
silence_performance_warning="True",
|
||||
read_only=no_learn)
|
||||
|
||||
bot = create_chat_bot(False)
|
||||
bot.set_trainer(ListTrainer)
|
||||
|
||||
bot.train([
|
||||
"I want to contribute",
|
||||
"""Contributors are more than welcomed! Please read
|
||||
https://github.com/zulip/zulip#how-to-get-involved-with-contributing-to-zulip
|
||||
to learn how to contribute.""",
|
||||
])
|
||||
|
||||
bot.train([
|
||||
"What is Zulip?",
|
||||
"""Zulip is a powerful, open source group chat application. Written in Python
|
||||
and using the Django framework, Zulip supports both private messaging and group
|
||||
chats via conversation streams. You can learn more about the product and its
|
||||
features at https://www.zulip.org.""",
|
||||
])
|
||||
|
||||
bot.train([
|
||||
"I would like to request a remote dev instance",
|
||||
"""Greetings! You should receive a response from one of our mentors soon.
|
||||
In the meantime, why don't you learn more about running Zulip on a development
|
||||
environment? https://zulip.readthedocs.io/en/latest/using-dev-environment.html""",
|
||||
])
|
||||
|
||||
bot.train([
|
||||
"Joke!",
|
||||
"Only if you ask nicely!",
|
||||
])
|
||||
|
||||
bot.train([
|
||||
"What is your name?",
|
||||
"I am John, my job is to assist you with Zulip.",
|
||||
])
|
||||
|
||||
bot.train([
|
||||
"What can you do?",
|
||||
"I can provide useful information and jokes if you follow etiquette.",
|
||||
])
|
||||
|
||||
with open(JOKES_PATH) as data_file:
|
||||
for joke in json.load(data_file):
|
||||
bot.train([
|
||||
"Please can you tell me a joke?",
|
||||
joke['joke'],
|
||||
])
|
||||
|
||||
bot.set_trainer(ChatterBotCorpusTrainer)
|
||||
|
||||
bot.train(
|
||||
"chatterbot.corpus.english"
|
||||
)
|
||||
|
||||
bota = create_chat_bot(True)
|
||||
|
||||
class JohnHandler(object):
|
||||
'''
|
||||
This bot aims to be Zulip's virtual assistant. It
|
||||
finds the best match from a certain input.
|
||||
Also understands the English language and can
|
||||
mantain a conversation, joke and give useful information.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
Before running this, make sure to create a stream
|
||||
called "VirtualHelp" that your API user can send to.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content'].lower()
|
||||
return (original_content.startswith("@john") or
|
||||
original_content.startswith("@**john**"))
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to='VirtualHelp',
|
||||
subject="John",
|
||||
content=bota.get_response(original_content)
|
||||
))
|
||||
|
||||
handler_class = JohnHandler
|
||||
|
142
contrib_bots/bots/readme.md
Normal file
|
@ -0,0 +1,142 @@
|
|||
# Contrib Bots:
|
||||
|
||||
This is the documentation for an experimental new system for writing
|
||||
bots that react to messages.
|
||||
|
||||
This directory contains library code for running Zulip
|
||||
bots that react to messages sent by users.
|
||||
|
||||
This document explains how to run the code, and it also
|
||||
talks about the architecture for creating bots.
|
||||
|
||||
## Design Goals
|
||||
|
||||
The goal is to have a common framework for hosting a bot that reacts
|
||||
to messages in any of the following settings:
|
||||
|
||||
* Run as a long-running process using `call_on_each_event`.
|
||||
|
||||
* Run via a simple web service that can be deployed to PAAS providers
|
||||
and handles outgoing webhook requests from Zulip.
|
||||
|
||||
* Embedded into the Zulip server (so that no hosting is required),
|
||||
which would be done for high quality, reusable bots; we would have a
|
||||
nice "bot store" sort of UI for browsing and activating them.
|
||||
|
||||
* Run locally by our technically inclined users for bots that require
|
||||
account specific authentication, for example: a gmail bot that lets
|
||||
one send emails directly through Zulip.
|
||||
|
||||
## Running Bots
|
||||
|
||||
Here is an example of running the "follow-up" bot from
|
||||
inside a Zulip repo (and in your remote instance):
|
||||
|
||||
cd ~/zulip/contrib_bots
|
||||
./run.py bots/followup/followup.py --config-file ~/.zuliprc-prod
|
||||
|
||||
Once the bot code starts running, you will see a
|
||||
message explaining how to use the bot, as well as
|
||||
some log messages. You can use the `--quiet` option
|
||||
to suppress these messages.
|
||||
|
||||
The bot code will run continuously until you end the program with
|
||||
control-C (or otherwise).
|
||||
|
||||
### Zulip Configuration
|
||||
|
||||
For this document we assume you have some prior experience
|
||||
with using the Zulip API, but here is a quick review of
|
||||
what a `.zuliprc` files looks like. You can connect to the
|
||||
API as your own human user, or you can go into the Zulip settings
|
||||
page to create a user-owned bot.
|
||||
|
||||
[api]
|
||||
email=someuser@example.com
|
||||
key=<your api key>
|
||||
site=https://zulip.somewhere.com
|
||||
|
||||
When you run your bot, make sure to point it to the correct location
|
||||
of your `.zuliprc`.
|
||||
|
||||
### Third Party Configuration
|
||||
|
||||
If your bot interacts with a non-Zulip service, you may
|
||||
have to configure keys or usernames or URLs or similar
|
||||
information to hit the other service.
|
||||
|
||||
Do **NOT** put third party configuration information in your
|
||||
`.zuliprc` file. Do not put third party configuration
|
||||
information anywhere in your Zulip directory. Instead,
|
||||
create a separate configuration file for the third party's
|
||||
configuration in your home directory.
|
||||
|
||||
Any bots that require this will have instructions on
|
||||
exactly how to create or access this information.
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
If your module requires Python modules that are not either
|
||||
part of the standard Python library or the Zulip API
|
||||
distribution, we ask that you put a comment at the top
|
||||
of your bot explaining how to install the dependencies/modules.
|
||||
|
||||
Right now we don't support any kind of automatic build
|
||||
environment for bots, so it's currently up to the users
|
||||
of the bots to manage their dependencies. This may change
|
||||
in the future.
|
||||
|
||||
## Architecture
|
||||
|
||||
In order to make bot development easy, we separate
|
||||
out boilerplate code (loading up the Client API, etc.)
|
||||
from bot-specific code (actions of the bot/what the bot does).
|
||||
|
||||
All of the boilerplate code lives in `../run.py`. The
|
||||
runner code does things like find where it can import
|
||||
the Zulip API, instantiate a client with correct
|
||||
credentials, set up the logging level, find the
|
||||
library code for the specific bot, etc.
|
||||
|
||||
Then, for bot-specific logic, you will find `.py` files
|
||||
in the `lib` directory (i.e. the same directory as the
|
||||
document you are reading now).
|
||||
|
||||
Each bot library simply needs to do the following:
|
||||
|
||||
- Define a class that supports the methods `usage`,
|
||||
`triage_message`, and `handle_message`.
|
||||
- Set `handler_class` to be the name of that class.
|
||||
|
||||
(We make this a two-step process to reduce code repetition
|
||||
and to add abstraction.)
|
||||
|
||||
## Portability
|
||||
|
||||
Creating a handler class for each bot allows your bot
|
||||
code to be more portable. For example, if you want to
|
||||
use your bot code in some other kind of bot platform, then
|
||||
if all of your bots conform to the `handler_class` protocol,
|
||||
you can write simple adapter code to use them elsewhere.
|
||||
|
||||
Another future direction to consider is that Zulip will
|
||||
eventually support running certain types of bots on
|
||||
the server side, to essentially implement post-send
|
||||
hooks and things of those nature.
|
||||
|
||||
Conforming to the `handler_class` protocol will make
|
||||
it easier for Zulip admins to integrate custom bots.
|
||||
|
||||
In particular, `run.py` already passes in instances
|
||||
of a restricted variant of the Client class to your
|
||||
library code, which helps you ensure that your bot
|
||||
does only things that would be acceptable for running
|
||||
in a server-side environment.
|
||||
|
||||
## Other approaches
|
||||
|
||||
If you are not interested in running your bots on the
|
||||
server, then you can still use the full Zulip API and run
|
||||
them locally. The hope, though, is that this
|
||||
architecture will make writing simple bots a quick/easy
|
||||
process.
|
88
contrib_bots/bots/thesaurus/thesaurus.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# See zulip/contrib_bots/bots/readme.md for instructions on running this code.
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import logging
|
||||
try:
|
||||
from PyDictionary import PyDictionary as Dictionary
|
||||
except ImportError:
|
||||
logging.error("Dependency Missing!")
|
||||
sys.exit(0)
|
||||
|
||||
#Uses Python's Dictionary module
|
||||
# pip install PyDictionary
|
||||
|
||||
def get_clean_response(m, method):
|
||||
try:
|
||||
response = method(m)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return e
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
elif isinstance(response, list):
|
||||
return ', '.join(response)
|
||||
|
||||
def get_thesaurus_result(original_content):
|
||||
search_keyword = original_content.strip().split(' ', 1)[1]
|
||||
if search_keyword == 'help':
|
||||
help_message = "To use this bot, start messages with either \
|
||||
@synonym (to get the synonyms of a given word) \
|
||||
or @antonym (to get the antonyms of a given word). \
|
||||
Phrases are not accepted so only use single words \
|
||||
to search. For example you could search '@synonym hello' \
|
||||
or '@antonym goodbye'."
|
||||
return help_message
|
||||
elif original_content.startswith('@synonym'):
|
||||
result = get_clean_response(search_keyword, method = Dictionary.synonym)
|
||||
return result
|
||||
elif original_content.startswith('@antonym'):
|
||||
result = get_clean_response(search_keyword, method = Dictionary.antonym)
|
||||
return result
|
||||
|
||||
class ThesaurusHandler(object):
|
||||
'''
|
||||
This plugin allows users to enter a word in zulip
|
||||
and get synonyms, and antonyms, for that word sent
|
||||
back to the context (stream or private) in which
|
||||
it was sent. It looks for messages starting with
|
||||
@synonym or @antonym.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow users to get both synonyms
|
||||
and antonyms for a given word from zulip. To use this
|
||||
plugin, users need to install the PyDictionary module
|
||||
using 'pip install PyDictionary'.Use '@synonym help' or
|
||||
'@antonym help' for more usage information. Users should
|
||||
preface messages with @synonym or @antonym.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
|
||||
is_thesaurus = (original_content.startswith('@synonym') or
|
||||
original_content.startswith('@antonym'))
|
||||
|
||||
return is_thesaurus
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
new_content = get_thesaurus_result(original_content)
|
||||
|
||||
if message['type'] == 'private':
|
||||
client.send_message(dict(
|
||||
type='private',
|
||||
to=original_sender,
|
||||
content=new_content,
|
||||
))
|
||||
else:
|
||||
client.send_message(dict(
|
||||
type=message['type'],
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=new_content,
|
||||
))
|
||||
|
||||
handler_class = ThesaurusHandler
|
330
contrib_bots/bots/tictactoe-bot/tictactoe-bot.py
Normal file
|
@ -0,0 +1,330 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
import copy
|
||||
import random
|
||||
from six.moves import range
|
||||
|
||||
initial_board = [["_", "_", "_"],
|
||||
["_", "_", "_"],
|
||||
["_", "_", "_"]]
|
||||
|
||||
mode = 'r' # default, can change for debugging to 'p'
|
||||
def output_mode(string_to_print, mode):
|
||||
if mode == "p":
|
||||
print(string_to_print)
|
||||
elif mode == "r":
|
||||
return string_to_print
|
||||
|
||||
# -------------------------------------
|
||||
class TicTacToeGame(object):
|
||||
smarter = True
|
||||
# If smarter is True, the computer will do some extra thinking - it'll be harder for the user.
|
||||
|
||||
triplets = [[(0, 0), (0, 1), (0, 2)], # Row 1
|
||||
[(1, 0), (1, 1), (1, 2)], # Row 2
|
||||
[(2, 0), (2, 1), (2, 2)], # Row 3
|
||||
[(0, 0), (1, 0), (2, 0)], # Column 1
|
||||
[(0, 1), (1, 1), (2, 1)], # Column 2
|
||||
[(0, 2), (1, 2), (2, 2)], # Column 3
|
||||
[(0, 0), (1, 1), (2, 2)], # Diagonal 1
|
||||
[(0, 2), (1, 1), (2, 0)] # Diagonal 2
|
||||
]
|
||||
|
||||
positions = "Coordinates are entered in a (row, column) format. Numbering is from top to bottom and left to right.\n" \
|
||||
"Here are the coordinates of each position. (Parentheses and spaces are optional.) \n" \
|
||||
"(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n " \
|
||||
"Your move would be one of these. To make a move, type @tictactoe or @ttt " \
|
||||
"followed by a space and the coordinate."
|
||||
|
||||
detailed_help_message = "*Help for Tic-Tac-Toe bot* \n" \
|
||||
"The bot responds to messages starting with @tictactoe or @ttt.\n" \
|
||||
"**@tictactoe new** (or **@ttt new**) will start a new game (but not if you're " \
|
||||
"already in the middle of a game). You must type this first to start playing!\n" \
|
||||
"**@tictactoe help** (or **@ttt help**) will return this help function.\n" \
|
||||
"**@tictactoe quit** (or **@ttt quit**) will quit from the current game.\n" \
|
||||
"**@tictactoe <coordinate>** (or **@ttt <coordinate>**) will make a move at the given coordinate.\n" \
|
||||
"Coordinates are entered in a (row, column) format. Numbering is from " \
|
||||
"top to bottom and left to right. \n" \
|
||||
"Here are the coordinates of each position. (Parentheses and spaces are optional). \n" \
|
||||
"(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n"
|
||||
|
||||
def __init__(self, board):
|
||||
self.board = board
|
||||
|
||||
def display_row(self, row):
|
||||
''' Takes the row passed in as a list and returns it as a string. '''
|
||||
row_string = " ".join([e.strip() for e in row])
|
||||
return("[ {} ]\n".format(row_string))
|
||||
|
||||
def display_board(self, board):
|
||||
''' Takes the board as a nested list and returns a nice version for the user. '''
|
||||
return "".join([self.display_row(r) for r in board])
|
||||
|
||||
def get_value(self, board, position):
|
||||
return board[position[0]][position[1]]
|
||||
|
||||
def board_is_full(self, board):
|
||||
''' Determines if the board is full or not. '''
|
||||
full = False
|
||||
board_state = ""
|
||||
for row in board:
|
||||
for element in row:
|
||||
if element == "_":
|
||||
board_state += "_"
|
||||
if "_" not in board_state:
|
||||
full = True
|
||||
return full
|
||||
|
||||
def win_conditions(self, board, triplets):
|
||||
''' Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
|
||||
in the triplet are blank. '''
|
||||
won = False
|
||||
for triplet in triplets:
|
||||
if (self.get_value(board, triplet[0]) == self.get_value(board, triplet[1]) ==
|
||||
self.get_value(board, triplet[2]) != "_"):
|
||||
won = True
|
||||
break
|
||||
return won
|
||||
|
||||
def get_locations_of_char(self, board, char):
|
||||
''' Gets the locations of the board that have char in them. '''
|
||||
locations = []
|
||||
for row in range(3):
|
||||
for col in range(3):
|
||||
if board[row][col] == char:
|
||||
locations.append([row, col])
|
||||
return locations
|
||||
|
||||
def two_blanks(self, triplet, board):
|
||||
''' Determines which rows/columns/diagonals have two blank spaces and an 'o' already in them. It's more advantageous
|
||||
for the computer to move there. This is used when the computer makes its move. '''
|
||||
|
||||
o_found = False
|
||||
for position in triplet:
|
||||
if self.get_value(board, position) == "o":
|
||||
o_found = True
|
||||
break
|
||||
|
||||
blanks_list = []
|
||||
if o_found:
|
||||
for position in triplet:
|
||||
if self.get_value(board, position) == "_":
|
||||
blanks_list.append(position)
|
||||
|
||||
if len(blanks_list) == 2:
|
||||
return blanks_list
|
||||
|
||||
def computer_move(self, board):
|
||||
''' The computer's logic for making its move. '''
|
||||
my_board = copy.deepcopy(board) # First the board is copied; used later on
|
||||
blank_locations = self.get_locations_of_char(my_board, "_")
|
||||
x_locations = self.get_locations_of_char(board, "x") # Gets the locations that already have x's
|
||||
corner_locations = [[0, 0], [0, 2], [2, 0], [2, 2]] # List of the coordinates of the corners of the board
|
||||
edge_locations = [[1, 0], [0, 1], [1, 2], [2, 1]] # List of the coordinates of the edge spaces of the board
|
||||
|
||||
if blank_locations == []: # If no empty spaces are left, the computer can't move anyway, so it just returns the board.
|
||||
return board
|
||||
|
||||
if len(x_locations) == 1: # This is special logic only used on the first move.
|
||||
# If the user played first in the corner or edge, the computer should move in the center.
|
||||
if x_locations[0] in corner_locations or x_locations[0] in edge_locations:
|
||||
board[1][1] = "o"
|
||||
# If user played first in the center, the computer should move in the corner. It doesn't matter which corner.
|
||||
else:
|
||||
location = random.choice(corner_locations)
|
||||
row = location[0]
|
||||
col = location[1]
|
||||
board[row][col] = "o"
|
||||
return board
|
||||
|
||||
# This logic is used on all other moves.
|
||||
# First I'll check if the computer can win in the next move. If so, that's where the computer will play.
|
||||
# The check is done by replacing the blank locations with o's and seeing if the computer would win in each case.
|
||||
for row, col in blank_locations:
|
||||
my_board[row][col] = "o"
|
||||
if self.win_conditions(my_board, self.triplets) == True:
|
||||
board[row][col] = "o"
|
||||
return board
|
||||
else:
|
||||
my_board[row][col] = "_" # Revert if not winning
|
||||
|
||||
# If the computer can't immediately win, it wants to make sure the user can't win in their next move, so it
|
||||
# checks to see if the user needs to be blocked.
|
||||
# The check is done by replacing the blank locations with x's and seeing if the user would win in each case.
|
||||
for row, col in blank_locations:
|
||||
my_board[row][col] = "x"
|
||||
if self.win_conditions(my_board, self.triplets):
|
||||
board[row][col] = "o"
|
||||
return board
|
||||
else:
|
||||
my_board[row][col] = "_" # Revert if not winning
|
||||
|
||||
# Assuming nobody will win in their next move, now I'll find the best place for the computer to win.
|
||||
for row, col in blank_locations:
|
||||
if ('x' not in my_board[row] and my_board[0][col] != 'x' and my_board[1][col] !=
|
||||
'x' and my_board[2][col] != 'x'):
|
||||
board[row][col] = 'o'
|
||||
return board
|
||||
|
||||
# If no move has been made, choose a random blank location. If smarter is True, the computer will choose a
|
||||
# random blank location from a set of better locations to play. These locations are determined by seeing if
|
||||
# there are two blanks and an 'o' in each row, column, and diagonal (done in two_blanks).
|
||||
# If smarter is False, all blank locations can be chosen.
|
||||
if self.smarter == True:
|
||||
blanks = []
|
||||
for triplet in self.triplets:
|
||||
result = self.two_blanks(triplet, board)
|
||||
if result:
|
||||
blanks = blanks + result
|
||||
blank_set = set(blanks)
|
||||
blank_list = list(blank_set)
|
||||
if blank_list == []:
|
||||
location = random.choice(blank_locations)
|
||||
else:
|
||||
location = random.choice(blank_list)
|
||||
row = location[0]
|
||||
col = location[1]
|
||||
board[row][col] = 'o'
|
||||
return board
|
||||
|
||||
else:
|
||||
location = random.choice(blank_locations)
|
||||
row = location[0]
|
||||
col = location[1]
|
||||
board[row][col] = 'o'
|
||||
return board
|
||||
|
||||
def check_validity(self, move):
|
||||
''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) '''
|
||||
try:
|
||||
split_move = move.split(",")
|
||||
row = split_move[0].strip()
|
||||
col = split_move[1].strip()
|
||||
valid = False
|
||||
if row == "1" or row == "2" or row == "3":
|
||||
if col == "1" or col == "2" or col == "3":
|
||||
valid = True
|
||||
except IndexError:
|
||||
valid = False
|
||||
return valid
|
||||
|
||||
def sanitize_move(self, move):
|
||||
''' As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
|
||||
input is stripped to just the numbers before being used in the program. '''
|
||||
move = move.replace("(", "")
|
||||
move = move.replace(")", "")
|
||||
move = move.strip()
|
||||
return move
|
||||
|
||||
def tictactoe(self, board, input_string):
|
||||
return_string = ""
|
||||
move = self.sanitize_move(input_string)
|
||||
|
||||
# Subtraction must be done to convert to the right indices, since computers start numbering at 0.
|
||||
row = (int(move[0])) - 1
|
||||
column = (int(move[-1])) - 1
|
||||
|
||||
if board[row][column] != "_":
|
||||
return_string += output_mode("That space is already filled, sorry!", mode)
|
||||
return return_string
|
||||
else:
|
||||
board[row][column] = "x"
|
||||
|
||||
return_string += self.display_board(board)
|
||||
|
||||
# Check to see if the user won/drew after they made their move. If not, it's the computer's turn.
|
||||
if self.win_conditions(board, self.triplets) == True:
|
||||
return_string += output_mode("Game over! You've won!", mode)
|
||||
return return_string
|
||||
|
||||
if self.board_is_full(board) == True:
|
||||
return_string += output_mode("It's a draw! Neither of us was able to win.", mode)
|
||||
return return_string
|
||||
|
||||
return_string += output_mode("My turn:\n", mode)
|
||||
self.computer_move(board)
|
||||
return_string += self.display_board(board)
|
||||
|
||||
# Checks to see if the computer won after it makes its move. (The computer can't draw, so there's no point
|
||||
# in checking.) If the computer didn't win, the user gets another turn.
|
||||
if self.win_conditions(board, self.triplets) == True:
|
||||
return_string += output_mode("Game over! I've won!", mode)
|
||||
return return_string
|
||||
|
||||
return_string += output_mode("Your turn! Enter a coordinate or type help.", mode)
|
||||
return return_string
|
||||
|
||||
# -------------------------------------
|
||||
flat_initial = sum(initial_board, [])
|
||||
def first_time(board):
|
||||
flat = sum(board, [])
|
||||
return flat == flat_initial
|
||||
|
||||
class ticTacToeHandler(object):
|
||||
'''
|
||||
You can play tic-tac-toe in a private message with
|
||||
tic-tac-toe bot! Make sure your message starts with
|
||||
"@tictactoe or @ttt".
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
You can play tic-tac-toe with the computer now! Make sure your
|
||||
message starts with @tictactoe or @ttt.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
is_tictactoe = (original_content.startswith('@tictactoe') or
|
||||
original_content.startswith('@ttt'))
|
||||
return is_tictactoe
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
command_list = original_content.split()[1:]
|
||||
command = ""
|
||||
for val in command_list:
|
||||
command += val
|
||||
original_sender = message['sender_email']
|
||||
|
||||
mydict = state_handler.get_state()
|
||||
if not mydict:
|
||||
state_handler.set_state({})
|
||||
mydict = state_handler.get_state()
|
||||
|
||||
user_game = mydict.get(original_sender)
|
||||
if (not user_game) and command == "new":
|
||||
user_game = TicTacToeGame(copy.deepcopy(initial_board))
|
||||
mydict[original_sender] = user_game
|
||||
|
||||
if command == 'new':
|
||||
if user_game and not first_time(user_game.board):
|
||||
return_content = "You're already playing a game! Type **@tictactoe help** or **@ttt help** to see valid inputs."
|
||||
else:
|
||||
return_content = "Welcome to tic-tac-toe! You'll be x's and I'll be o's. Your move first!\n"
|
||||
return_content += TicTacToeGame.positions
|
||||
elif command == 'help':
|
||||
return_content = TicTacToeGame.detailed_help_message
|
||||
elif (user_game) and TicTacToeGame.check_validity(user_game, TicTacToeGame.sanitize_move(user_game, command)) == True:
|
||||
user_board = user_game.board
|
||||
return_content = TicTacToeGame.tictactoe(user_game, user_board, command)
|
||||
elif (user_game) and command == 'quit':
|
||||
del mydict[original_sender]
|
||||
return_content = "You've successfully quit the game."
|
||||
else:
|
||||
return_content = "Hmm, I didn't understand your input. Type **@tictactoe help** or **@ttt help** to see valid inputs."
|
||||
|
||||
if "Game over" in return_content or "draw" in return_content:
|
||||
del mydict[original_sender]
|
||||
|
||||
state_handler.set_state(mydict)
|
||||
|
||||
client.send_message(dict(
|
||||
type = 'private',
|
||||
to = original_sender,
|
||||
subject = message['sender_email'],
|
||||
content = return_content,
|
||||
))
|
||||
|
||||
handler_class = ticTacToeHandler
|
34
contrib_bots/bots/tictactoe-bot/tictactoe/docs.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# About Tic-Tac-Toe Bot
|
||||
|
||||
This bot allows you to play tic-tac-toe in a private message with the bot.
|
||||
Multiple games can simultaneously be played by different users, each playing
|
||||
against the computer.
|
||||
|
||||
The bot only responds to messages starting with **@tictactoe** or **@ttt**.
|
||||
|
||||
### Commands
|
||||
**@tictactoe new** (or **@ttt new**) will start a new game (but not if you are
|
||||
already playing a game.) You must type this first to start playing!
|
||||
|
||||
**@tictactoe help** (or **@ttt help**) will return a help function with valid
|
||||
commands and coordinates.
|
||||
|
||||
**@tictactoe quit** (or **@ttt quit**) will quit from the current game.
|
||||
|
||||
**@tictactoe <coordinate>** (or **@ttt <coordinate>**) will make a move at the
|
||||
entered coordinate. For example, **@ttt 1,1** . After this, the bot will make
|
||||
its move, or declare the game over if the user or bot has won.
|
||||
|
||||
Coordinates are entered in a (row, column) format. Numbering is from top to
|
||||
bottom and left to right.
|
||||
Here are the coordinates of each position. When entering coordinates, parentheses
|
||||
and spaces are optional.
|
||||
|
||||
(1, 1) | (1, 2) | (1, 3)
|
||||
|
||||
(2, 1) | (2, 2) | (2, 3)
|
||||
|
||||
(3, 1) | (3, 2) | (3, 3)
|
||||
|
||||
Invalid commands will result in an "I don't understand" response from the bot,
|
||||
with a suggestion to type **@tictactoe help** (or **@ttt help**).
|
368
contrib_bots/bots/virtual_fs/virtual_fs.py
Normal file
|
@ -0,0 +1,368 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
import re
|
||||
import os
|
||||
|
||||
class VirtualFsHandler(object):
|
||||
def usage(self):
|
||||
return get_help()
|
||||
|
||||
def triage_message(self, message, client):
|
||||
if message['type'] != 'stream':
|
||||
return False
|
||||
|
||||
original_content = message['content']
|
||||
return (original_content.startswith('fs ') or
|
||||
original_content.startswith('@fs '))
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
assert self.triage_message(message, client)
|
||||
|
||||
original_content = message['content']
|
||||
command = original_content[len('fs '):]
|
||||
stream = message['display_recipient']
|
||||
topic = message['subject']
|
||||
sender = message['sender_email']
|
||||
|
||||
state = state_handler.get_state()
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
if stream not in state:
|
||||
state[stream] = fs_new()
|
||||
fs = state[stream]
|
||||
if sender not in fs['user_paths']:
|
||||
fs['user_paths'][sender] = '/'
|
||||
fs, msg = fs_command(fs, sender, command)
|
||||
prependix = '{}:\n'.format(sender)
|
||||
msg = prependix + msg
|
||||
state[stream] = fs
|
||||
state_handler.set_state(state)
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=stream,
|
||||
subject=topic,
|
||||
content=msg,
|
||||
))
|
||||
|
||||
|
||||
def get_help():
|
||||
return '''
|
||||
The "fs" commands implement a virtual file system for a stream.
|
||||
The locations of text are persisted for the lifetime of the bot
|
||||
running, and if you rename a stream, you will lose the info.
|
||||
Example commands:
|
||||
|
||||
```
|
||||
fs mkdir: create a directory
|
||||
fs ls: list a directory
|
||||
fs cd: change directory
|
||||
fs pwd: show current path
|
||||
fs write: write text
|
||||
fs read: read text
|
||||
fs rm: remove a file
|
||||
fs rmdir: remove a directory
|
||||
```
|
||||
Use commands like `fs help write` for more details on specific
|
||||
commands.
|
||||
'''
|
||||
|
||||
def test():
|
||||
fs = fs_new()
|
||||
user = 'test_user'
|
||||
fs['user_paths'][user] = '/'
|
||||
assert is_directory(fs, '/')
|
||||
|
||||
for cmd, expected_response in sample_conversation():
|
||||
fs, msg = fs_command(fs, user, cmd)
|
||||
if msg != expected_response:
|
||||
raise AssertionError('''
|
||||
cmd: %s
|
||||
expected: %s
|
||||
but got : %s
|
||||
''' % (cmd, expected_response, msg))
|
||||
|
||||
def sample_conversation():
|
||||
return [
|
||||
('cd /', 'Current path: /'),
|
||||
('cd /home', 'ERROR: invalid path'),
|
||||
('cd .', 'ERROR: invalid path'),
|
||||
('mkdir home', 'directory created'),
|
||||
('cd home', 'Current path: /home/'),
|
||||
('cd /home/', 'Current path: /home/'),
|
||||
('mkdir stuff/', 'ERROR: stuff/ is not a valid name'),
|
||||
('mkdir stuff', 'directory created'),
|
||||
('write stuff/file1 something', 'file written'),
|
||||
('read stuff/file1', 'something'),
|
||||
('read /home/stuff/file1', 'something'),
|
||||
('read home/stuff/file1', 'ERROR: file does not exist'),
|
||||
('pwd ', '/home/'),
|
||||
('pwd bla', 'ERROR: syntax: pwd'),
|
||||
('ls bla foo', 'ERROR: syntax: ls <optional_path>'),
|
||||
('cd /', 'Current path: /'),
|
||||
('rm home', 'ERROR: /home/ is a directory, file required'),
|
||||
('rmdir home', 'removed'),
|
||||
('ls ', 'WARNING: directory is empty'),
|
||||
('cd home', 'ERROR: invalid path'),
|
||||
('read /home/stuff/file1', 'ERROR: file does not exist'),
|
||||
('cd /', 'Current path: /'),
|
||||
('write /foo contents of /foo', 'file written'),
|
||||
('read /foo', 'contents of /foo'),
|
||||
('write /bar Contents: bar bar', 'file written'),
|
||||
('read /bar', 'Contents: bar bar'),
|
||||
('write /bar invalid', 'ERROR: file already exists'),
|
||||
('rm /bar', 'removed'),
|
||||
('rm /bar', 'ERROR: file does not exist'),
|
||||
('write /bar new bar', 'file written'),
|
||||
('read /bar', 'new bar'),
|
||||
('write /yo/invalid whatever', 'ERROR: /yo is not a directory'),
|
||||
('mkdir /yo', 'directory created'),
|
||||
('read /yo', 'ERROR: /yo/ is a directory, file required'),
|
||||
('ls /yo', 'WARNING: directory is empty'),
|
||||
('read /yo/nada', 'ERROR: file does not exist'),
|
||||
('write /yo whatever', 'ERROR: file already exists'),
|
||||
('write /yo/apple red', 'file written'),
|
||||
('read /yo/apple', 'red'),
|
||||
('mkdir /yo/apple', 'ERROR: file already exists'),
|
||||
('ls /invalid', 'ERROR: file does not exist'),
|
||||
('ls /foo', 'ERROR: /foo is not a directory'),
|
||||
('ls /', '* /*bar*\n* /*foo*\n* /yo/'),
|
||||
('invalid command', 'ERROR: unrecognized command'),
|
||||
('write', 'ERROR: syntax: write <path> <some_text>'),
|
||||
('help', get_help()),
|
||||
('help ls', 'syntax: ls <optional_path>'),
|
||||
('help invalid_command', get_help()),
|
||||
]
|
||||
|
||||
REGEXES = dict(
|
||||
command='(cd|ls|mkdir|read|rmdir|rm|write|pwd)',
|
||||
path='(\S+)',
|
||||
optional_path='(\S*)',
|
||||
some_text='(.+)',
|
||||
)
|
||||
|
||||
def get_commands():
|
||||
return {
|
||||
'help': (fs_help, ['command']),
|
||||
'ls': (fs_ls, ['optional_path']),
|
||||
'mkdir': (fs_mkdir, ['path']),
|
||||
'read': (fs_read, ['path']),
|
||||
'rm': (fs_rm, ['path']),
|
||||
'rmdir': (fs_rmdir, ['path']),
|
||||
'write': (fs_write, ['path', 'some_text']),
|
||||
'cd': (fs_cd, ['path']),
|
||||
'pwd': (fs_pwd, []),
|
||||
}
|
||||
|
||||
def fs_command(fs, user, cmd):
|
||||
cmd = cmd.strip()
|
||||
if cmd == 'help':
|
||||
return fs, get_help()
|
||||
cmd_name = cmd.split()[0]
|
||||
cmd_args = cmd[len(cmd_name):].strip()
|
||||
commands = get_commands()
|
||||
if cmd_name not in commands:
|
||||
return fs, 'ERROR: unrecognized command'
|
||||
|
||||
f, arg_names = commands[cmd_name]
|
||||
partial_regexes = [REGEXES[a] for a in arg_names]
|
||||
regex = ' '.join(partial_regexes)
|
||||
regex += '$'
|
||||
m = re.match(regex, cmd_args)
|
||||
if m:
|
||||
return f(fs, user, *m.groups())
|
||||
elif cmd_name == 'help':
|
||||
return fs, get_help()
|
||||
else:
|
||||
return fs, 'ERROR: ' + syntax_help(cmd_name)
|
||||
|
||||
def syntax_help(cmd_name):
|
||||
commands = get_commands()
|
||||
f, arg_names = commands[cmd_name]
|
||||
arg_syntax = ' '.join('<' + a + '>' for a in arg_names)
|
||||
if arg_syntax:
|
||||
cmd = cmd_name + ' ' + arg_syntax
|
||||
else:
|
||||
cmd = cmd_name
|
||||
return 'syntax: {}'.format(cmd)
|
||||
|
||||
def fs_new():
|
||||
fs = {
|
||||
'/': directory([]),
|
||||
'user_paths': dict()
|
||||
}
|
||||
return fs
|
||||
|
||||
def fs_help(fs, user, cmd_name):
|
||||
return fs, syntax_help(cmd_name)
|
||||
|
||||
def fs_mkdir(fs, user, fn):
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path in fs:
|
||||
return fs, 'ERROR: file already exists'
|
||||
dir_path = os.path.dirname(path)
|
||||
if not is_directory(fs, dir_path):
|
||||
msg = 'ERROR: {} is not a directory'.format(dir_path)
|
||||
return fs, msg
|
||||
new_fs = fs.copy()
|
||||
new_dir = directory({path}.union(fs[dir_path]['fns']))
|
||||
new_fs[dir_path] = new_dir
|
||||
new_fs[path] = directory([])
|
||||
msg = 'directory created'
|
||||
return new_fs, msg
|
||||
|
||||
def fs_ls(fs, user, fn):
|
||||
if fn == '.' or fn == '':
|
||||
path = fs['user_paths'][user]
|
||||
else:
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path not in fs:
|
||||
msg = 'ERROR: file does not exist'
|
||||
return fs, msg
|
||||
if not is_directory(fs, path):
|
||||
return fs, 'ERROR: {} is not a directory'.format(path)
|
||||
fns = fs[path]['fns']
|
||||
if not fns:
|
||||
return fs, 'WARNING: directory is empty'
|
||||
msg = '\n'.join('* ' + nice_path(fs, path) for path in sorted(fns))
|
||||
return fs, msg
|
||||
|
||||
def fs_pwd(fs, user):
|
||||
path = fs['user_paths'][user]
|
||||
msg = nice_path(fs, path)
|
||||
return fs, msg
|
||||
|
||||
def fs_rm(fs, user, fn):
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path not in fs:
|
||||
msg = 'ERROR: file does not exist'
|
||||
return fs, msg
|
||||
if fs[path]['kind'] == 'dir':
|
||||
msg = 'ERROR: {} is a directory, file required'.format(nice_path(fs, path))
|
||||
return fs, msg
|
||||
new_fs = fs.copy()
|
||||
new_fs.pop(path)
|
||||
directory = get_directory(path)
|
||||
new_fs[directory]['fns'].remove(path)
|
||||
msg = 'removed'
|
||||
return new_fs, msg
|
||||
|
||||
def fs_rmdir(fs, user, fn):
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path not in fs:
|
||||
msg = 'ERROR: directory does not exist'
|
||||
return fs, msg
|
||||
if fs[path]['kind'] == 'text':
|
||||
msg = 'ERROR: {} is a file, directory required'.format(nice_path(fs, path))
|
||||
return fs, msg
|
||||
new_fs = fs.copy()
|
||||
new_fs.pop(path)
|
||||
directory = get_directory(path)
|
||||
new_fs[directory]['fns'].remove(path)
|
||||
for sub_path in new_fs.keys():
|
||||
if sub_path.startswith(path+'/'):
|
||||
new_fs.pop(sub_path)
|
||||
msg = 'removed'
|
||||
return new_fs, msg
|
||||
|
||||
def fs_write(fs, user, fn, content):
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path in fs:
|
||||
msg = 'ERROR: file already exists'
|
||||
return fs, msg
|
||||
dir_path = os.path.dirname(path)
|
||||
if not is_directory(fs, dir_path):
|
||||
msg = 'ERROR: {} is not a directory'.format(dir_path)
|
||||
return fs, msg
|
||||
new_fs = fs.copy()
|
||||
new_dir = directory({path}.union(fs[dir_path]['fns']))
|
||||
new_fs[dir_path] = new_dir
|
||||
new_fs[path] = text_file(content)
|
||||
msg = 'file written'
|
||||
return new_fs, msg
|
||||
|
||||
def fs_read(fs, user, fn):
|
||||
path, msg = make_path(fs, user, fn)
|
||||
if msg:
|
||||
return fs, msg
|
||||
if path not in fs:
|
||||
msg = 'ERROR: file does not exist'
|
||||
return fs, msg
|
||||
if fs[path]['kind'] == 'dir':
|
||||
msg = 'ERROR: {} is a directory, file required'.format(nice_path(fs, path))
|
||||
return fs, msg
|
||||
val = fs[path]['content']
|
||||
return fs, val
|
||||
|
||||
def fs_cd(fs, user, fn):
|
||||
if len(fn) > 1 and fn[-1] == '/':
|
||||
fn = fn[:-1]
|
||||
path = fn if len(fn) > 0 and fn[0] == '/' else make_path(fs, user, fn)[0]
|
||||
if path not in fs:
|
||||
msg = 'ERROR: invalid path'
|
||||
return fs, msg
|
||||
if fs[path]['kind'] == 'text':
|
||||
msg = 'ERROR: {} is a file, directory required'.format(nice_path(fs, path))
|
||||
return fs, msg
|
||||
fs['user_paths'][user] = path
|
||||
return fs, "Current path: {}".format(nice_path(fs, path))
|
||||
|
||||
def make_path(fs, user, leaf):
|
||||
if leaf == '/':
|
||||
return ['/', '']
|
||||
if leaf.endswith('/'):
|
||||
return ['', 'ERROR: {} is not a valid name'.format(leaf)]
|
||||
if leaf.startswith('/'):
|
||||
return [leaf, '']
|
||||
path = fs['user_paths'][user]
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
path += leaf
|
||||
return path, ''
|
||||
|
||||
def nice_path(fs, path):
|
||||
path_nice = path
|
||||
slash = path.rfind('/')
|
||||
if path not in fs:
|
||||
return 'ERROR: the current directory does not exist'
|
||||
if fs[path]['kind'] == 'text':
|
||||
path_nice = '{}*{}*'.format(path[:slash+1], path[slash+1:])
|
||||
elif path != '/':
|
||||
path_nice = '{}/'.format(path)
|
||||
return path_nice
|
||||
|
||||
def get_directory(path):
|
||||
slash = path.rfind('/')
|
||||
if slash == 0:
|
||||
return '/'
|
||||
else:
|
||||
return path[:slash]
|
||||
|
||||
def directory(fns):
|
||||
return dict(kind='dir', fns=set(fns))
|
||||
|
||||
def text_file(content):
|
||||
return dict(kind='text', content=content)
|
||||
|
||||
def is_directory(fs, fn):
|
||||
if fn not in fs:
|
||||
return False
|
||||
return fs[fn]['kind'] == 'dir'
|
||||
|
||||
handler_class = VirtualFsHandler
|
||||
|
||||
if __name__ == '__main__':
|
||||
# We eventually want to test bots with a "real" testing
|
||||
# framework.
|
||||
test()
|
78
contrib_bots/bots/wikipedia/wikipedia.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
import requests
|
||||
import logging
|
||||
|
||||
# See readme.md for instructions on running this code.
|
||||
|
||||
class WikipediaHandler(object):
|
||||
'''
|
||||
This plugin facilitates searching Wikipedia for a
|
||||
specific key term and returns the top article from the
|
||||
search. It looks for messages starting with '@wikipedia'
|
||||
or '@wiki'.
|
||||
|
||||
In this example, we write all Wikipedia searches into
|
||||
the same stream that it was called from, but this code
|
||||
could be adapted to write Wikipedia searches to some
|
||||
kind of external issue tracker as well.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin will allow users to directly search
|
||||
Wikipedia for a specific key term and get the top
|
||||
article that is returned from the search. Users
|
||||
should preface searches with "@wikipedia" or
|
||||
"@wiki".
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
|
||||
# This next line of code is defensive, as we
|
||||
# never want to get into an infinite loop of posting Wikipedia
|
||||
# searches for own Wikipedia searches!
|
||||
if message['sender_full_name'] == 'wikipedia-bot':
|
||||
return False
|
||||
is_wikipedia = (original_content.startswith('@wiki') or
|
||||
original_content.startswith('@wikipedia'))
|
||||
|
||||
return is_wikipedia
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
query = message['content']
|
||||
|
||||
for prefix in ['@wikipedia', '@wiki']:
|
||||
if query.startswith(prefix):
|
||||
query = query[len(prefix)+1:]
|
||||
break
|
||||
|
||||
query_wiki_link = ('https://en.wikipedia.org/w/api.php?action=query&'
|
||||
'list=search&srsearch=%s&format=json' % (query,))
|
||||
try:
|
||||
data = requests.get(query_wiki_link)
|
||||
except requests.exceptions.RequestException:
|
||||
logging.error('broken link')
|
||||
return
|
||||
|
||||
if data.status_code != 200:
|
||||
logging.error('unsuccessful data')
|
||||
return
|
||||
|
||||
search_string = data.json()['query']['search'][0]['title'].replace(' ', '_')
|
||||
url = 'https://wikipedia.org/wiki/' + search_string
|
||||
new_content = 'For search term "' + query
|
||||
if len(data.json()['query']['search']) == 0:
|
||||
new_content = 'I am sorry. The search term you provided is not found :slightly_frowning_face:'
|
||||
else:
|
||||
new_content = new_content + '", ' + url
|
||||
|
||||
client.send_message(dict(
|
||||
type=message['type'],
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=new_content,
|
||||
))
|
||||
|
||||
handler_class = WikipediaHandler
|
40
contrib_bots/bots/xkcd/docs.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# xkcd bot
|
||||
|
||||
xkcd bot is a Zulip bot that can fetch a comic strip from xkcd. To use xkcd
|
||||
bot you can simply call it with `@xkcd` followed by a command. Like this:
|
||||
|
||||
```
|
||||
@xkcd <command>
|
||||
```
|
||||
|
||||
xkcd bot has four commands:
|
||||
|
||||
1. `help`
|
||||
This command is used to list all commands that can be used with this bot.
|
||||
You can use this command by typing `@xkcd help` in a stream.
|
||||

|
||||
|
||||
2. `latest`
|
||||
This command is used to fetch the latest comic strip from xkcd. You can use
|
||||
this command by typing `@xkcd latest` in a stream.
|
||||

|
||||
|
||||
3. `random`
|
||||
This command is used to fetch a random comic strip from xkcd. You can use
|
||||
this command by typing `@xkcd random` in a stream, xkcd bot will post a
|
||||
random xkcd comic strip.
|
||||

|
||||
|
||||
4. `<comic_id>`
|
||||
To fetch a comic strip based on id, you can directly use `@xkcd <comic_id>`,
|
||||
for example if you want to fetch a comic strip with id 1234, you can type
|
||||
`@xkcd 1234`, xkcd bot will post a comic strip with id 1234.
|
||||

|
||||
|
||||
If you type a wrong command to xkcd bot, xkcd bot will post information
|
||||
you'd get from `@xkcd help`.
|
||||

|
||||
|
||||
And if you type a wrong id, xkcd bot will post a message that an xkcd comic
|
||||
strip with that id is not available.
|
||||

|
BIN
contrib_bots/bots/xkcd/xkcd-help.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
contrib_bots/bots/xkcd/xkcd-latest.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
contrib_bots/bots/xkcd/xkcd-random.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
contrib_bots/bots/xkcd/xkcd-specific-id.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
contrib_bots/bots/xkcd/xkcd-wrong-command.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
contrib_bots/bots/xkcd/xkcd-wrong-id.png
Normal file
After Width: | Height: | Size: 14 KiB |
130
contrib_bots/bots/xkcd/xkcd.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
from random import randint
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
XKCD_TEMPLATE_URL = 'https://xkcd.com/%s/info.0.json'
|
||||
LATEST_XKCD_URL = 'https://xkcd.com/info.0.json'
|
||||
|
||||
class XkcdHandler(object):
|
||||
'''
|
||||
This plugin provides several commands that can be used for fetch a comic
|
||||
strip from https://xkcd.com. The bot looks for messages starting with
|
||||
"@xkcd" and responds with a message with the comic based on provided
|
||||
commands.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This plugin allows users to fetch a comic strip provided by
|
||||
https://xkcd.com. Users should preface the command with "@xkcd".
|
||||
|
||||
There are several commands to use this bot:
|
||||
- @xkcd help -> To show all commands the bot supports.
|
||||
- @xkcd latest -> To fetch the latest comic strip from xkcd.
|
||||
- @xkcd random -> To fetch a random comic strip from xkcd.
|
||||
- @xkcd <comic_id> -> To fetch a comic strip based on
|
||||
`<comic_id>`, e.g `@xkcd 1234`.
|
||||
'''
|
||||
|
||||
def triage_message(self, message, client):
|
||||
original_content = message['content']
|
||||
is_xkcd_called = original_content.startswith('@xkcd ')
|
||||
is_xkcd_called_without_command = original_content == '@xkcd'
|
||||
|
||||
return is_xkcd_called or is_xkcd_called_without_command
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
xkcd_bot_response = get_xkcd_bot_response(message)
|
||||
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=xkcd_bot_response,
|
||||
))
|
||||
|
||||
class XkcdBotCommand(object):
|
||||
LATEST = 0
|
||||
RANDOM = 1
|
||||
COMIC_ID = 2
|
||||
|
||||
class XkcdNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class XkcdServerError(Exception):
|
||||
pass
|
||||
|
||||
def get_xkcd_bot_response(message):
|
||||
original_content = message['content'].strip()
|
||||
cropped = original_content[len('@xkcd '):]
|
||||
command = cropped.strip()
|
||||
|
||||
xkcd_called_without_command = original_content == '@xkcd'
|
||||
|
||||
commands_help = ("%s"
|
||||
"\n* `@xkcd help` to show this help message."
|
||||
"\n* `@xkcd latest` to fetch the latest comic strip from xkcd."
|
||||
"\n* `@xkcd random` to fetch a random comic strip from xkcd."
|
||||
"\n* `@xkcd <comic id>` to fetch a comic strip based on `<comic id>` "
|
||||
"e.g `@xkcd 1234`.")
|
||||
|
||||
try:
|
||||
if command == 'help' or xkcd_called_without_command:
|
||||
return commands_help % ('xkcd bot supports these commands:')
|
||||
elif command == 'latest':
|
||||
fetched = fetch_xkcd_query(XkcdBotCommand.LATEST)
|
||||
elif command == 'random':
|
||||
fetched = fetch_xkcd_query(XkcdBotCommand.RANDOM)
|
||||
elif command.isdigit():
|
||||
fetched = fetch_xkcd_query(XkcdBotCommand.COMIC_ID, cropped.strip())
|
||||
else:
|
||||
return commands_help % ('xkcd bot only supports these commands:')
|
||||
except (requests.exceptions.ConnectionError, XkcdServerError):
|
||||
logging.exception('Connection error occurred when trying to connect to xkcd server')
|
||||
return 'Sorry, I cannot process your request right now, please try again later!'
|
||||
except XkcdNotFoundError:
|
||||
logging.exception('XKCD server responded 404 when trying to fetch comic with id %s'
|
||||
% (command))
|
||||
return 'Sorry, there is likely no xkcd comic strip with id: #%s' % (command,)
|
||||
else:
|
||||
return ("#%s: **%s**\n[%s](%s)" % (fetched['num'],
|
||||
fetched['title'],
|
||||
fetched['alt'],
|
||||
fetched['img']))
|
||||
|
||||
def fetch_xkcd_query(mode, comic_id=None):
|
||||
try:
|
||||
if mode == XkcdBotCommand.LATEST: # Fetch the latest comic strip.
|
||||
url = LATEST_XKCD_URL
|
||||
|
||||
elif mode == XkcdBotCommand.RANDOM: # Fetch a random comic strip.
|
||||
latest = requests.get(LATEST_XKCD_URL)
|
||||
|
||||
if latest.status_code != 200:
|
||||
raise XkcdServerError()
|
||||
|
||||
latest_id = latest.json()['num']
|
||||
random_id = randint(1, latest_id)
|
||||
url = XKCD_TEMPLATE_URL % (str(random_id))
|
||||
|
||||
elif mode == XkcdBotCommand.COMIC_ID: # Fetch specific comic strip by id number.
|
||||
if comic_id is None:
|
||||
raise Exception('Missing comic_id argument')
|
||||
url = XKCD_TEMPLATE_URL % (comic_id)
|
||||
|
||||
fetched = requests.get(url)
|
||||
|
||||
if fetched.status_code == 404:
|
||||
raise XkcdNotFoundError()
|
||||
elif fetched.status_code != 200:
|
||||
raise XkcdServerError()
|
||||
|
||||
xkcd_json = fetched.json()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logging.warning(e)
|
||||
raise
|
||||
|
||||
return xkcd_json
|
||||
|
||||
handler_class = XkcdHandler
|
79
contrib_bots/bots/yoda_bot/readme-yoda-bot.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Overview
|
||||
|
||||
This is the documentation for how to set up and run the yoda_bot. (`yoda_bot.py`)
|
||||
|
||||
This directory contains library code for running Zulip
|
||||
bots that react to messages sent by users.
|
||||
|
||||
This bot will allow users to translate a sentence into 'Yoda speak'.
|
||||
It looks for messages starting with '@yoda'. You will need to have a
|
||||
Mashape API key. Please see instructions for getting one below.
|
||||
|
||||
## Setup
|
||||
This bot uses the python library `unirest` which is not a
|
||||
dependency of Zulip. To use this module, you will have to
|
||||
install it in your local machine. In your terminal, enter
|
||||
the following command:
|
||||
* $ sudo pip install unirest --upgrade
|
||||
Note:
|
||||
|
||||
You might have to use `pip3` if you are using python 3.
|
||||
The install command would also download any dependency
|
||||
required by `unirest`.
|
||||
|
||||
Before running this bot, make sure to get a Mashape API Key.
|
||||
Go to this link:
|
||||
<https://market.mashape.com/ismaelc/yoda-speak/overview>
|
||||
This is the API that powers the `yoda_bot`. You can read more about it
|
||||
on this page.
|
||||
|
||||

|
||||
|
||||
Click on the **Sign Up Free** button at the top and create
|
||||
an account. Then click on the **Documentation** tab. Scroll down to the
|
||||
bottom, and click on the **Test Endpoint** button.
|
||||
This will add the Yoda Speak API to your default application. You can
|
||||
also add it to a different application if you wish. Now click on the
|
||||
**Applications** tab at the top. Select the application that you added
|
||||
the Yoda Speak API to. Click on the blue **GET THE KEYS** button.
|
||||
|
||||
On the pop-up that comes up, click on the **COPY** button.
|
||||
This is your Mashape API Key. It is used
|
||||
to authenticate. Store it in the `yoda_api_key.txt` file.
|
||||
|
||||
The `yoda_api_key.txt` file should be located at `~/yoda_api_key.txt`.
|
||||
|
||||
Example input:
|
||||
|
||||
@yoda You will learn how to speak like me someday.
|
||||
|
||||
If you need help while the bot is running just input `@yoda help`.
|
||||
|
||||
## Running the bot
|
||||
|
||||
Here is an example of running the "yoda_bot" bot from
|
||||
inside a Zulip repo:
|
||||
|
||||
cd ~/zulip/contrib_bots
|
||||
./run.py bots/yoda_bot/yoda_bot.py --config-file ~/.zuliprc-prod
|
||||
|
||||
Once the bot code starts running, you will see a
|
||||
message explaining how to use the bot, as well as
|
||||
some log messages. You can use the `--quiet` option
|
||||
to suppress some of the informational messages.
|
||||
|
||||
The bot code will run continuously until you kill them with
|
||||
control-C (or otherwise).
|
||||
|
||||
### Configuration
|
||||
|
||||
For this document we assume you have some prior experience
|
||||
with using the Zulip API, but here is a quick review of
|
||||
what a `.zuliprc` files looks like. You can connect to the
|
||||
API as your own human user, or you can go into the Zulip settings
|
||||
page to create a user-owned bot.
|
||||
|
||||
[api]
|
||||
email=someuser@example.com
|
||||
key=<your api key>
|
||||
site=https://zulip.somewhere.com
|
BIN
contrib_bots/bots/yoda_bot/yoda-speak-api.png
Executable file
After Width: | Height: | Size: 229 KiB |
164
contrib_bots/bots/yoda_bot/yoda_bot.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
# See readme-yoda-bot.md for instructions on running this code.
|
||||
|
||||
"""
|
||||
This bot uses the python library `unirest` which is not a
|
||||
dependency of Zulip. To use this module, you will have to
|
||||
install it in your local machine. In your terminal, enter
|
||||
the following command:
|
||||
$ sudo pip install unirest --upgrade
|
||||
Note:
|
||||
* You might have to use `pip3` if you are using python 3.
|
||||
* The install command would also download any dependency
|
||||
required by `unirest`.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import logging
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
try:
|
||||
import unirest
|
||||
except ImportError:
|
||||
logging.error("Dependency missing!!\n%s" % (__doc__))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
HELP_MESSAGE = '''
|
||||
This bot allows users to translate a sentence into
|
||||
'Yoda speak'.
|
||||
Users should preface messages with '@yoda'.
|
||||
|
||||
Before running this, make sure to get a Mashape Api token.
|
||||
Instructions are in the 'readme-yoda-bot.md' file.
|
||||
Store it in the 'yoda_api_key.txt' file.
|
||||
The 'yoda_api_key.txt' file should be located at '~/yoda_api_key.txt'.
|
||||
Example input:
|
||||
@yoda You will learn how to speak like me someday.
|
||||
'''
|
||||
|
||||
|
||||
class ApiKeyError(Exception):
|
||||
'''raise this when there is an error with the Mashape Api Key'''
|
||||
|
||||
|
||||
class YodaSpeakHandler(object):
|
||||
'''
|
||||
This bot will allow users to translate a sentence into 'Yoda speak'.
|
||||
It looks for messages starting with '@yoda'.
|
||||
'''
|
||||
|
||||
def usage(self):
|
||||
return '''
|
||||
This bot will allow users to translate a sentence into
|
||||
'Yoda speak'.
|
||||
Users should preface messages with '@yoda'.
|
||||
|
||||
Before running this, make sure to get a Mashape Api token.
|
||||
Instructions are in the 'readme-yoda-bot.md' file.
|
||||
Store it in the 'yoda_api_key.txt' file.
|
||||
The 'yoda_api_key.txt' file should be located at '~/yoda_api_key.txt'.
|
||||
Example input:
|
||||
@yoda You will learn how to speak like me someday.
|
||||
'''
|
||||
|
||||
def triage_message(self, message):
|
||||
original_content = message['content']
|
||||
|
||||
return original_content.startswith('@yoda')
|
||||
|
||||
def handle_message(self, message, client, state_handler):
|
||||
original_content = message['content']
|
||||
stream = message['display_recipient']
|
||||
subject = message['subject']
|
||||
|
||||
# this handles the message if its starts with @yoda
|
||||
if original_content.startswith('@yoda'):
|
||||
handle_input(client, original_content, stream, subject)
|
||||
|
||||
handler_class = YodaSpeakHandler
|
||||
|
||||
|
||||
def send_to_yoda_api(sentence, api_key):
|
||||
# function for sending sentence to api
|
||||
|
||||
response = unirest.get("https://yoda.p.mashape.com/yoda?sentence=" + sentence,
|
||||
headers={
|
||||
"X-Mashape-Key": api_key,
|
||||
"Accept": "text/plain"
|
||||
}
|
||||
)
|
||||
|
||||
if response.code == 200:
|
||||
return response.body
|
||||
if response.code == 403:
|
||||
raise ApiKeyError
|
||||
else:
|
||||
error_message = response.body['message']
|
||||
logging.error(error_message)
|
||||
error_code = response.code
|
||||
error_message = error_message + 'Error code: ' + error_code +\
|
||||
' Did you follow the instructions in the `readme-yoda-bot.md` file?'
|
||||
return error_message
|
||||
|
||||
|
||||
def format_input(original_content):
|
||||
# replaces the '@yoda' with nothing, so that '@yoda' doesn't get sent to the api
|
||||
message_content = original_content.replace('@yoda', '')
|
||||
# gets rid of whitespace around the edges, so that they aren't a problem in the future
|
||||
message_content = message_content.strip()
|
||||
# replaces all spaces with '+' to be in the format the api requires
|
||||
sentence = message_content.replace(' ', '+')
|
||||
return sentence
|
||||
|
||||
|
||||
def handle_input(client, original_content, stream, subject):
|
||||
|
||||
if is_help(original_content):
|
||||
send_message(client, HELP_MESSAGE, stream, subject)
|
||||
|
||||
else:
|
||||
sentence = format_input(original_content)
|
||||
try:
|
||||
reply_message = send_to_yoda_api(sentence, get_api_key())
|
||||
|
||||
except ssl.SSLError or TypeError:
|
||||
reply_message = 'The service is temporarily unavailable, please try again.'
|
||||
logging.error(reply_message)
|
||||
|
||||
except ApiKeyError:
|
||||
reply_message = 'Invalid Api Key. Did you follow the instructions in the ' \
|
||||
'`readme-yoda-bot.md` file?'
|
||||
logging.error(reply_message)
|
||||
|
||||
send_message(client, reply_message, stream, subject)
|
||||
|
||||
|
||||
def get_api_key():
|
||||
# function for getting Mashape api key
|
||||
home = os.path.expanduser('~')
|
||||
with open(home + '/yoda_api_key.txt') as api_key_file:
|
||||
api_key = api_key_file.read().strip()
|
||||
return api_key
|
||||
|
||||
|
||||
def send_message(client, message, stream, subject):
|
||||
# function for sending a message
|
||||
client.send_message(dict(
|
||||
type='stream',
|
||||
to=stream,
|
||||
subject=subject,
|
||||
content=message
|
||||
))
|
||||
|
||||
|
||||
def is_help(original_content):
|
||||
# replaces the '@yoda' with nothing, so that '@yoda' doesn't get sent to the api
|
||||
message_content = original_content.replace('@yoda', '')
|
||||
# gets rid of whitespace around the edges, so that they aren't a problem in the future
|
||||
message_content = message_content.strip()
|
||||
if message_content == 'help':
|
||||
return True
|
||||
else:
|
||||
return False
|