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'.
This commit is contained in:
Robert Hönig 2017-01-22 10:38:34 +00:00 committed by showell
parent 51b88e3d8c
commit fefe869748
57 changed files with 20 additions and 23 deletions

View file

View 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.

View file

@ -0,0 +1,2 @@
[Google.com]
api_key = abcdefghijklmnopqrstuvwxyz

View 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')

View 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:
![multiple-converts](multiple-converts.png)
* 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View 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.
![Correct Word](correct_word.png)
* If the user enters a wrong word, like "@define cresh" or "@define crish",
then an error message saying no definition is available is displayed.
![Wrong Word](wrong_word.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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&nbsp;&nbsp;{}'.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View 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.

View 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'

View 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

View 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.

View file

@ -0,0 +1,2 @@
[Foursquare]
api_key = abcdefghijksm

View 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')

View 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

View 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,
))

View file

@ -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

View 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))

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View 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`
![howdowe question](question_howdowe.png)
Answer -> Howdoi would try to **only** respond with the coding section
of the answer.
![howdowe answer](answer_howdowe.png)
#### Example 2
Question -> `@howdoi! stack vs heap`
![howdoi! question](question_howdoi_all.png)
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.
![howdoi! answer](answer_howdoi_all.png)
**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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View 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!
![Joke John](joke.png)
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.
![Meet John](greetings.png)
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.
![Assist](assist.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View 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!"
}
]

View 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
View 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.

View 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

View 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

View 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**).

View 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()

View 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

View 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.
![](xkcd-help.png)
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.
![](xkcd-latest.png)
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.
![](xkcd-random.png)
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.
![](xkcd-specific-id.png)
If you type a wrong command to xkcd bot, xkcd bot will post information
you'd get from `@xkcd help`.
![](xkcd-wrong-command.png)
And if you type a wrong id, xkcd bot will post a message that an xkcd comic
strip with that id is not available.
![](xkcd-wrong-id.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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

View 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.
![yoda api overview](yoda-speak-api.png)
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View 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