bots: Move contrib_bots to api/bots*.

This will make it convenient to include these bots in Zulip API
releases on pypi.

Fix #5009.
This commit is contained in:
Rohitt Vashishtha 2017-05-30 11:40:19 +05:30 committed by Tim Abbott
parent 7531c4fb26
commit 894adb1e43
110 changed files with 36 additions and 27 deletions

0
bots/__init__.py Normal file
View file

0
bots/commute/__init__.py Normal file
View file

View file

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

242
bots/commute/commute.py Normal file
View file

@ -0,0 +1,242 @@
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 + '/commute.config'
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 @mention of the bot.
'''
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 @mention of the bot.
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:
@mention-botname origins=Chicago,IL,USA destinations=New+York,NY,USA
@mention-botname 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:
@mention-botname 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.
'''
# adds API Authentication Key to url request
def get_api_key(self):
# commute.config must be moved from
# ~/zulip/api/bots/commute/commute.config into
# ~/commute.config for program to work
# see readme.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):
client.send_reply(message, 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']
query = original_content.split()
if "help" in query:
self.send_info(message, self.help_info, client)
return
params = self.parse_pair(query)
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')

72
bots/commute/readme.md Normal 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 commute.config into
the user home directory and add an API key.
Move
```
~/zulip/api/bots/commute/commute.config
```
into
```
~/commute.config
```
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 commute.config
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

130
bots/converter/converter.py Normal file
View file

@ -0,0 +1,130 @@
# See readme.md for instructions on running this code.
from __future__ import absolute_import
from __future__ import division
import copy
import importlib
import sys
from math import log10, floor
import utils
import re
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
'@mention-bot <number> <unit_from> <unit_to>'
The message '@mention-bot 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 '@mention-bot <number> <unit_from> <unit_to>'
The message '@mention-bot help' posts a short description of
how to use the plugin, along with a list of
all supported units.
'''
def handle_message(self, message, client, state_handler):
bot_response = get_bot_converter_response(message, client)
client.send_reply(message, bot_response)
def get_bot_converter_response(message, client):
content = message['content']
words = content.lower().split()
convert_indexes = [i for i, word in enumerate(words) if word == "@convert"]
convert_indexes = [-1] + convert_indexes
results = []
for convert_index in convert_indexes:
if (convert_index + 1) < len(words) and words[convert_index + 1] == 'help':
results.append(utils.HELP_MESSAGE)
continue
if (convert_index + 3) < len(words):
number = words[convert_index + 1]
unit_from = utils.ALIASES.get(words[convert_index + 2], words[convert_index + 2])
unit_to = utils.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. ' + utils.QUICK_HELP)
continue
number = float(number)
number_res = copy.copy(number)
for key, exp in utils.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 = utils.UNITS.get(unit_from, False)
ut_to_std = utils.UNITS.get(unit_to, False)
if uf_to_std is False:
results.append(unit_from + ' is not a valid unit. ' + utils.QUICK_HELP)
if ut_to_std is False:
results.append(unit_to + ' is not a valid unit.' + utils.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. ' + utils.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 ** (exponent // 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. ' + utils.QUICK_HELP)
new_content = ''
for idx, result in enumerate(results, 1):
new_content += ((str(idx) + '. conversion: ') if len(results) > 1 else '') + result + '\n'
return new_content
handler_class = ConverterHandler

70
bots/converter/readme.md Normal file
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](assets/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.

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestConverterBot(BotTestCase):
bot_name = "converter"
def test_bot(self):
expected = {
"": ('Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n'),
"foo bar": ('Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n'),
"2 m cm": "2.0 m = 200.0 cm\n",
"12.0 celsius fahrenheit": "12.0 celsius = 53.600054 fahrenheit\n",
"0.002 kilometer millimile": "0.002 kilometer = 1.2427424 millimile\n",
"3 megabyte kilobit": "3.0 megabyte = 24576.0 kilobit\n",
}
self.check_expected_responses(expected)

146
bots/converter/utils.py Normal file
View file

@ -0,0 +1,146 @@
# 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.'

0
bots/define/__init__.py Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

66
bots/define/define.py Normal file
View file

@ -0,0 +1,66 @@
# 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 '@mention-bot'.
'''
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(self):
return '''
This plugin will allow users to define a word. Users should preface
messages with @mention-bot.
'''
def handle_message(self, message, client, state_handler):
original_content = message['content'].strip()
bot_response = self.get_bot_define_response(original_content)
client.send_reply(message, bot_response)
def get_bot_define_response(self, original_content):
split_content = original_content.split(' ')
# If there are more than one word (a phrase)
if len(split_content) > 1:
return DefineHandler.PHRASE_ERROR_MESSAGE
to_define = split_content[0].strip()
to_define_lower = to_define.lower()
# No word was entered.
if not to_define_lower:
return self.EMPTY_WORD_REQUEST_ERROR_MESSAGE
else:
response = '**{}**:\n'.format(to_define)
try:
# Use OwlBot API to fetch definition.
api_result = requests.get(self.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 += self.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 += self.REQUEST_ERROR_MESSAGE
logging.exception(e)
return response
handler_class = DefineHandler

21
bots/define/readme.md Normal file
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](assets/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](assets/wrong_word.png)

View file

@ -0,0 +1,28 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestDefineBot(BotTestCase):
bot_name = "define"
def test_bot(self):
expected = {
"": 'Please enter a word to define.',
"foo": "**foo**:\nDefinition not available.",
"cat": ("**cat**:\n\n* (**noun**) a small domesticated carnivorous mammal "
"with soft fur, a short snout, and retractile claws. It is widely "
"kept as a pet or for catching mice, and many breeds have been "
"developed.\n&nbsp;&nbsp;their pet cat\n\n"),
}
self.check_expected_responses(expected)

0
bots/encrypt/__init__.py Normal file
View file

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

45
bots/encrypt/encrypt.py Executable file
View file

@ -0,0 +1,45 @@
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 @mention-bot.
'''
def usage(self):
return '''
This bot uses ROT13 encryption for its purposes.
It responds to me starting with @mention-bot.
Feeding encrypted messages into the bot decrypts them.
'''
def handle_message(self, message, client, state_handler):
bot_response = self.get_bot_encrypt_response(message)
client.send_reply(message, bot_response)
def get_bot_encrypt_response(self, message):
original_content = message['content']
temp_content = encrypt(original_content)
send_content = "Encrypted/Decrypted text: " + temp_content
return send_content
handler_class = EncryptHandler
if __name__ == '__main__':
assert encrypt('ABCDabcd1234') == 'NOPQnopq1234'
assert encrypt('NOPQnopq1234') == 'ABCDabcd1234'

16
bots/encrypt/readme.md Normal file
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,27 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestEncryptBot(BotTestCase):
bot_name = "encrypt"
def test_bot(self):
expected = {
"": "Encrypted/Decrypted text: ",
"Let\'s Do It": "Encrypted/Decrypted text: Yrg\'f Qb Vg",
"me&mom together..!!": "Encrypted/Decrypted text: zr&zbz gbtrgure..!!",
"foo bar": "Encrypted/Decrypted text: sbb one",
"Please encrypt this": "Encrypted/Decrypted text: Cyrnfr rapelcg guvf",
}
self.check_expected_responses(expected)

View file

46
bots/followup/followup.py Normal file
View file

@ -0,0 +1,46 @@
# 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 '@mention-bot'.
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 "@mention-bot".
Before running this, make sure to create a stream
called "followup" that your API user can send to.
'''
def handle_message(self, message, client, state_handler):
if message['content'] == '':
bot_response = "Please specify the message you want to send to followup stream after @mention-bot"
client.send_reply(message, bot_response)
else:
bot_response = self.get_bot_followup_response(message)
client.send_message(dict(
type='stream',
to='followup',
subject=message['sender_email'],
content=bot_response,
))
def get_bot_followup_response(self, message):
original_content = message['content']
original_sender = message['sender_email']
temp_content = 'from %s: ' % (original_sender,)
new_content = temp_content + original_content
return new_content
handler_class = FollowupHandler

View file

View file

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

View file

@ -0,0 +1,123 @@
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/api/bots/foursquare/foursquare.config'
def get_api_key():
# foursquare.config must have been moved from
# ~/zulip/api/bots/foursquare/foursquare.config into
# ~/foursquare.config for program to work
# see readme.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 '@mention-bot'.
If you need help, simply type:
@mention-bot /help into the Compose Message box
Sample input:
@mention-bot Chicago, IL
@mention-bot 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:
@mention-bot 'Millenium Park' 8000 donuts
@mention-bot 'Melbourne, Australia' 40000 seafood
'''
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):
client.send_reply(message, 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) >= 1:
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 '@mention-bot 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 '@mention-bot 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')

32
bots/foursquare/readme.md Normal file
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.

0
bots/giphy/__init__.py Normal file
View file

93
bots/giphy/giphy.py Normal file
View file

@ -0,0 +1,93 @@
# 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 @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 the Giphy-bot @mention.
The bot responds also to private messages.
'''
def handle_message(self, message, client, state_handler):
bot_response = get_bot_giphy_response(message, client)
client.send_reply(message, 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):
# Each exception has a specific reply should "gif_url" return a number.
# The bot will post the appropriate message for the error.
keyword = message['content']
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

View file

@ -0,0 +1,126 @@
# See readme.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 '@mention-bot'.
'''
def usage(self):
return '''
This bot will allow users to comment on a GitHub issue.
Users should preface messages with '@mention-bot'.
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 handle_message(self, message, client, state_handler):
original_content = message['content']
original_sender = message['sender_email']
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/api`
`bots_api/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

0
bots/github/__init__.py Normal file
View file

56
bots/github/github.py Normal file
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

View file

@ -0,0 +1,109 @@
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.error
import urllib.parse
import urllib.request
class IssueHandler(object):
'''
This plugin facilitates sending issues to github, when
an item is prefixed with '@mention-bot'.
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 '@mention-bot'.
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 handle_message(self, message, client, state_handler):
original_content = message['content']
original_sender = message['sender_email']
temp_content = 'by {}:'.format(original_sender,)
new_content = temp_content + original_content
# 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()
issue_title = message['content'].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

View file

@ -0,0 +1,93 @@
# See readme.md for instructions on running this code.
from __future__ import print_function
import logging
import http.client
from six.moves.urllib.request import urlopen
# Uses the Google search engine bindings
# pip install --upgrade google
from google import search
def get_google_result(search_keywords):
help_message = "To use this bot, start messages with @mentioned-bot, \
followed by what you want to search for. If \
found, Zulip will return the first search result \
on Google.\
\
An example message that could be sent is:\
'@mentioned-bot zulip' or \
'@mentioned-bot how to create a chatbot'."
if search_keywords == 'help':
return help_message
elif search_keywords == '' or search_keywords is None:
return help_message
else:
try:
urls = search(search_keywords, stop=20)
urlopen('http://216.58.192.142', timeout=1)
except http.client.RemoteDisconnected as er:
logging.exception(er)
return 'Error: No internet connection. {}.'.format(er)
except Exception as e:
logging.exception(e)
return 'Error: Search failed. {}.'.format(e)
try:
url = next(urls)
except AttributeError as a_err:
# google.search query failed and urls is of object
# 'NoneType'
logging.exception(a_err)
return "Error: Google search failed with a NoneType result. {}.".format(a_err)
except TypeError as t_err:
# google.search query failed and returned None
# This technically should not happen but the prior
# error check assumed this behavior
logging.exception(t_err)
return "Error: Google search function failed. {}.".format(t_err)
except Exception as e:
logging.exception(e)
return 'Error: Search failed. {}.'.format(e)
return 'Success: {}'.format(url)
class GoogleSearchHandler(object):
'''
This plugin allows users to enter a search
term in Zulip and get the top URL sent back
to the context (stream or private) in which
it was called. It looks for messages starting
with @mentioned-bot.
'''
def usage(self):
return '''
This plugin will allow users to search
for a given search term on Google from
Zulip. Use '@mentioned-bot help' to get
more information on the bot usage. Users
should preface messages with
@mentioned-bot.
'''
def handle_message(self, message, client, state_handler):
original_content = message['content']
result = get_google_result(original_content)
client.send_reply(message, result)
handler_class = GoogleSearchHandler
def test():
try:
urlopen('http://216.58.192.142', timeout=1)
print('Success')
return True
except http.client.RemoteDisconnected as e:
print('Error: {}'.format(e))
return False
if __name__ == '__main__':
test()

View file

@ -0,0 +1,23 @@
# Google Search bot
This bot allows users to do Google search queries and have the bot
respond with the first search result. It is by default set to the
highest safe-search setting.
## Usage
Run this bot as described
[here](http://zulip.readthedocs.io/en/latest/bots-guide.html#how-to-deploy-a-bot).
Use this bot with the following command
`@mentioned-bot <search terms>`
This will return the first link found by Google for `<search terms>`
and print the resulting URL.
If no `<search terms>` are entered, a help message is printed instead.
If there was an error in the process of running the search (socket
errors, Google search function failed, or general failures), an error
message is returned.

View file

View file

@ -0,0 +1,18 @@
# See readme.md for instructions on running this code.
class HelloWorldHandler(object):
def usage(self):
return '''
This is a boilerplate bot that responds to a user query with
"beep boop", which is robot for "Hello World".
This bot can be used as a template for other, more
sophisticated, bots.
'''
def handle_message(self, message, client, state_handler):
content = 'beep boop'
client.send_reply(message, content)
handler_class = HelloWorldHandler

View file

@ -0,0 +1,4 @@
Simple Zulip bot that will respond to any query with a "beep boop".
The helloworld bot is a boilerplate bot that can be used as a
template for more sophisticated/evolved Zulip bots.

View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
from six.moves import zip
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestHelloWorldBot(BotTestCase):
bot_name = "helloworld"
def test_bot(self):
txt = "beep boop"
messages = ["", "foo", "Hi, my name is abc"]
self.check_expected_responses(dict(list(zip(messages, len(messages)*[txt]))))

0
bots/help/__init__.py Normal file
View file

18
bots/help/help.py Normal file
View file

@ -0,0 +1,18 @@
# 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 handle_message(self, message, client, state_handler):
help_content = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip"
client.send_reply(message, help_content)
handler_class = HelpHandler

23
bots/help/test_help.py Normal file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
from six.moves import zip
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestHelpBot(BotTestCase):
bot_name = "help"
def test_bot(self):
txt = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip"
messages = ["", "help", "Hi, my name is abc"]
self.check_expected_responses(dict(list(zip(messages, len(messages)*[txt]))))

0
bots/howdoi/__init__.py Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

118
bots/howdoi/howdoi.py Normal file
View file

@ -0,0 +1,118 @@
"""
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`.
"""
from __future__ import absolute_import
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:
* @mention-bot howdowe > This would return the answer to the same
stream that it was called from.
* @mention-bot 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 '@mention-bot howdoi!', '@mention-bot 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:
* @mention-bot howdowe > Answer to the same stream
* @mention-bot howdoi > Answer via private message
* @mention-bot howdowe! OR @mention-bot howdoi! > Full answer from SO
'''
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'].strip()
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

56
bots/howdoi/readme.md Normal file
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](assets/question_howdowe.png)
Answer -> Howdoi would try to **only** respond with the coding section
of the answer.
![howdowe answer](assets/answer_howdowe.png)
#### Example 2
Question -> `@howdoi! stack vs heap`
![howdoi! question](assets/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](assets/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.

View file

View file

@ -0,0 +1,30 @@
# See readme.md for instructions on running this code.
class IncrementorHandler(object):
def __init__(self):
self.number = 0
self.message_id = None
def usage(self):
return '''
This is a boilerplate bot that makes use of the
update_message function. For the first @-mention, it initially
replies with one message containing a `1`. Every time the bot
is @-mentioned, this number will be incremented in the same message.
'''
def handle_message(self, message, client, state_handler):
self.number += 1
if self.message_id is None:
result = client.send_reply(message, str(self.number))
self.message_id = result['id']
else:
client.update_message(dict(
message_id=self.message_id,
content=str(self.number),
))
handler_class = IncrementorHandler

View file

@ -0,0 +1,6 @@
# Incrementor bot
This is a boilerplate bot that makes use of the
update_message function. For the first @-mention, it initially
replies with one message containing a `1`. Every time the bot
is @-mentioned, this number will be incremented in the same message.

0
bots/john/__init__.py Normal file
View file

BIN
bots/john/assets/assist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
bots/john/assets/joke.png Normal file

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

123
bots/john/john.py Normal file
View file

@ -0,0 +1,123 @@
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""")
BOTS_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(os.path.dirname(BOTS_DIR))
sys.path.insert(0, os.path.dirname(BOTS_DIR))
JOKES_PATH = os.path.join(BOTS_DIR, 'assets/var/jokes.json')
DATABASE_PATH = os.path.join(BOTS_DIR, 'assets/var/database.db')
DIRECTORY_PATH = os.path.join(BOTS_DIR, 'assets')
VAR_PATH = os.path.join(BOTS_DIR, 'assets/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.OutputAdapter",
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 '''
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 handle_message(self, message, client, state_handler):
original_content = message['content']
bot_response = str(bota.get_response(original_content))
client.send_reply(message, bot_response)
handler_class = JohnHandler

30
bots/john/readme.md Normal file
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](assets/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](assets/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](assets/assist.png)

139
bots/readme.md Normal file
View file

@ -0,0 +1,139 @@
# Contrib bots
This is the documentation for an experimental new system for writing
bots that react to messages. It explains how to run the code, and also
talks about the architecture for creating such bots.
This directory contains library code for running them.
## 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/api
bots_api/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`
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

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestThesaurusBot(BotTestCase):
bot_name = "thesaurus"
def test_bot(self):
self.assert_bot_output(
{'content': "synonym good", 'type': "private", 'sender_email': "foo"},
"great, satisfying, exceptional, positive, acceptable"
)
self.assert_bot_output(
{'content': "synonym nice", 'type': "stream", 'display_recipient': "foo", 'subject': "foo"},
"cordial, kind, good, okay, fair"
)
self.assert_bot_output(
{'content': "synonym foo", 'type': "stream", 'display_recipient': "foo", 'subject': "foo"},
"bar, thud, X, baz, corge"
)
self.assert_bot_output(
{'content': "antonym dirty", 'type': "stream", 'display_recipient': "foo", 'subject': "foo"},
"ordered, sterile, spotless, moral, clean"
)
self.assert_bot_output(
{'content': "antonym bar", 'type': "stream", 'display_recipient': "foo", 'subject': "foo"},
"loss, whole, advantage, aid, failure"
)
self.assert_bot_output(
{'content': "", 'type': "stream", 'display_recipient': "foo", 'subject': "foo"},
("To use this bot, start messages with either "
"@mention-bot synonym (to get the synonyms of a given word) "
"or @mention-bot 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 '@mention-bot synonym hello' "
"or '@mention-bot antonym goodbye'."),
)

View file

@ -0,0 +1,71 @@
# See zulip/api/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)
else:
return "Sorry, no result found! Please check the word."
def get_thesaurus_result(original_content):
help_message = ("To use this bot, start messages with either "
"@mention-bot synonym (to get the synonyms of a given word) "
"or @mention-bot 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 '@mention-bot synonym hello' "
"or '@mention-bot antonym goodbye'.")
query = original_content.strip().split(' ', 1)
if len(query) < 2:
return help_message
else:
search_keyword = query[1]
if original_content.startswith('synonym'):
result = get_clean_response(search_keyword, method = Dictionary.synonym)
elif original_content.startswith('antonym'):
result = get_clean_response(search_keyword, method = Dictionary.antonym)
else:
result = help_message
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
'@mention-bot synonym' or '@mention-bot @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 '@mention-bot synonym help' or
'@mention-bot antonym help' for more usage information. Users should
preface messages with @mention-bot synonym or @mention-bot antonym.
'''
def handle_message(self, message, client, state_handler):
original_content = message['content'].strip()
new_content = get_thesaurus_result(original_content)
client.send_reply(message, new_content)
handler_class = ThesaurusHandler

View file

34
bots/tictactoe/readme.md Normal file
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 @mention of the bot(botname).
### Commands
**@mention-botname new** will start a new game (but not if you are
already playing a game.) You must type this first to start playing!
**@mention-botname help** will return a help function with valid
commands and coordinates.
**@mention-botname quit** will quit from the current game.
**@mention-botname <coordinate>** will make a move at the
entered coordinate. For example, **@mention-botname 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 **@mention-botname help** .

323
bots/tictactoe/tictactoe.py Normal file
View file

@ -0,0 +1,323 @@
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 @mention-bot " \
"followed by a space and the coordinate."
detailed_help_message = "*Help for Tic-Tac-Toe bot* \n" \
"The bot responds to messages starting with @mention-bot.\n" \
"**@mention-bot 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" \
"**@mention-bot help** will return this help function.\n" \
"**@mention-bot quit** will quit from the current game.\n" \
"**@mention-bot <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):
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:
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):
return_string += output_mode("Game over! You've won!", mode)
return return_string
if self.board_is_full(board):
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):
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
"@mention-bot".
'''
def usage(self):
return '''
You can play tic-tac-toe with the computer now! Make sure your
message starts with @mention-bot.
'''
def handle_message(self, message, client, state_handler):
command_list = message['content']
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)):
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

44
bots/virtual_fs/readme.md Normal file
View file

@ -0,0 +1,44 @@
# Virtual fs bot
This bot allows users to store information in a virtual file system,
for a given stream or private chat.
## 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 any of the following 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
where `fs` may be the name of the bot you registered in the zulip system.
### Usage examples
`@fs ls` - Initially shows nothing (with a warning)
`@fs pwd` - Show which directory we are in: we start in /
`@fs mkdir foo` - Make directory foo
`@fs ls` - Show that foo is now created
`@fs cd foo` - Change into foo (and do a pwd, automatically)
`@fs write test hello world` - Write "hello world" to the file 'test'
`@fs read test` - Check the text was written
`@fs ls` - Show that the new file exists
`@fs rm test` - Remove that file
`@fs cd /` - Change back to root directory
`@fs rmdir foo` - Remove foo
## Notes
* In a stream, the bot must be mentioned; in a private chat, the bot
will assume every message is a command and so does not require this,
though doing so will still work.
* Use commands like `@fs help write` for more details on a command.

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestVirtualFsBot(BotTestCase):
bot_name = "virtual_fs"
def test_bot(self):
self.assert_bot_output(
{'content': "cd /home", 'type': "private", 'display_recipient': "foo", 'sender_email': "foo_sender@zulip.com"},
"foo_sender@zulip.com:\nERROR: invalid path"
)
self.assert_bot_output(
{'content': "mkdir home", 'type': "stream", 'display_recipient': "foo", 'subject': "foo", 'sender_email': "foo_sender@zulip.com"},
"foo_sender@zulip.com:\ndirectory created"
)
self.assert_bot_output(
{'content': "pwd", 'type': "stream", 'display_recipient': "foo", 'subject': "foo", 'sender_email': "foo_sender@zulip.com"},
"foo_sender@zulip.com:\n/"
)
self.assert_bot_output(
{'content': "help", 'type': "stream", 'display_recipient': "foo", 'subject': "foo", 'sender_email': "foo_sender@zulip.com"},
('foo_sender@zulip.com:\n\nThis bot implements a virtual file system for a stream.\n'
'The locations of text are persisted for the lifetime of the bot\n'
'running, and if you rename a stream, you will lose the info.\n'
'Example commands:\n\n```\n'
'@mention-bot sample_conversation: sample conversation with the bot\n'
'@mention-bot mkdir: create a directory\n'
'@mention-bot ls: list a directory\n'
'@mention-bot cd: change directory\n'
'@mention-bot pwd: show current path\n'
'@mention-bot write: write text\n'
'@mention-bot read: read text\n'
'@mention-bot rm: remove a file\n'
'@mention-bot rmdir: remove a directory\n'
'```\n'
'Use commands like `@mention-bot help write` for more details on specific\ncommands.\n'),
)
self.assert_bot_output(
{'content': "help ls", 'type': "stream", 'display_recipient': "foo", 'subject': "foo", 'sender_email': "foo_sender@zulip.com"},
"foo_sender@zulip.com:\nsyntax: ls <optional_path>"
)
self.assert_bot_output(
{'content': "", 'type': "stream", 'display_recipient': "foo", 'subject': "foo", 'sender_email': "foo_sender@zulip.com"},
('foo_sender@zulip.com:\n\nThis bot implements a virtual file system for a stream.\n'
'The locations of text are persisted for the lifetime of the bot\n'
'running, and if you rename a stream, you will lose the info.\n'
'Example commands:\n\n```\n'
'@mention-bot sample_conversation: sample conversation with the bot\n'
'@mention-bot mkdir: create a directory\n'
'@mention-bot ls: list a directory\n'
'@mention-bot cd: change directory\n'
'@mention-bot pwd: show current path\n'
'@mention-bot write: write text\n'
'@mention-bot read: read text\n'
'@mention-bot rm: remove a file\n'
'@mention-bot rmdir: remove a directory\n'
'```\n'
'Use commands like `@mention-bot help write` for more details on specific\ncommands.\n'),
)

View file

@ -0,0 +1,343 @@
# See readme.md for instructions on running this code.
import re
import os
class VirtualFsHandler(object):
def usage(self):
return get_help()
def handle_message(self, message, client, state_handler):
command = message['content']
if command == "":
command = "help"
sender = message['sender_email']
state = state_handler.get_state()
if state is None:
state = {}
recipient = message['display_recipient']
if isinstance(recipient, list): # If not a stream, then hash on list of emails
recipient = " ".join([x['email'] for x in recipient])
if recipient not in state:
state[recipient] = fs_new()
fs = state[recipient]
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[recipient] = fs
state_handler.set_state(state)
client.send_reply(message, msg)
def get_help():
return '''
This bot implements 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:
```
@mention-bot sample_conversation: sample conversation with the bot
@mention-bot mkdir: create a directory
@mention-bot ls: list a directory
@mention-bot cd: change directory
@mention-bot pwd: show current path
@mention-bot write: write text
@mention-bot read: read text
@mention-bot rm: remove a file
@mention-bot rmdir: remove a directory
```
Use commands like `@mention-bot help write` for more details on specific
commands.
'''
def sample_conversation():
return [
('cd /\nCurrent path: /\n\n'),
('cd /home\nERROR: invalid path\n\n'),
('cd .\nERROR: invalid path\n\n'),
('mkdir home\ndirectory created\n\n'),
('cd home\nCurrent path: /home/\n\n'),
('cd /home/\nCurrent path: /home/\n\n'),
('mkdir stuff/\nERROR: stuff/ is not a valid name\n\n'),
('mkdir stuff\ndirectory created\n\n'),
('write stuff/file1 something\nfile written\n\n'),
('read stuff/file1\nsomething\n\n'),
('read /home/stuff/file1\nsomething\n\n'),
('read home/stuff/file1\nERROR: file does not exist\n\n'),
('pwd \n/home/\n\n'),
('pwd bla\nERROR: syntax: pwd\n\n'),
('ls bla foo\nERROR: syntax: ls <optional_path>\n\n'),
('cd /\nCurrent path: /\n\n'),
('rm home\nERROR: /home/ is a directory, file required\n\n'),
('rmdir home\nremoved\n\n'),
('ls \nWARNING: directory is empty\n\n'),
('cd home\nERROR: invalid path\n\n'),
('read /home/stuff/file1\nERROR: file does not exist\n\n'),
('cd /\nCurrent path: /\n\n'),
('write /foo contents of /foo\nfile written\n\n'),
('read /foo\ncontents of /foo\n\n'),
('write /bar Contents: bar bar\nfile written\n\n'),
('read /bar\nContents: bar bar\n\n'),
('write /bar invalid\nERROR: file already exists\n\n'),
('rm /bar\nremoved\n\n'),
('rm /bar\nERROR: file does not exist\n\n'),
('write /bar new bar\nfile written\n\n'),
('read /bar\nnew bar\n\n'),
('write /yo/invalid whatever\nERROR: /yo is not a directory\n\n'),
('mkdir /yo\ndirectory created\n\n'),
('read /yo\nERROR: /yo/ is a directory, file required\n\n'),
('ls /yo\nWARNING: directory is empty\n\n'),
('read /yo/nada\nERROR: file does not exist\n\n'),
('write /yo whatever\nERROR: file already exists\n\n'),
('write /yo/apple red\nfile written\n\n'),
('read /yo/apple\nred\n\n'),
('mkdir /yo/apple\nERROR: file already exists\n\n'),
('ls /invalid\nERROR: file does not exist\n\n'),
('ls /foo\nERROR: /foo is not a directory\n\n'),
('ls /\n* /*bar*\n* /*foo*\n* /yo/\n\n'),
('invalid command\nERROR: unrecognized command\n\n'),
('write\nERROR: syntax: write <path> <some_text>\n\n'),
('help' + get_help() + '\n\n'),
('help ls\nsyntax: ls <optional_path>\n\n'),
('help invalid_command' + get_help() + '\n\n'),
]
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']),
'sample_conversation': (fs_sample_conversation, ['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()
if cmd == 'sample_conversation':
return fs, (''.join(sample_conversation()))
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_sample_conversation(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

View file

@ -0,0 +1,2 @@
[weather-config]
key=XXXXXXXX

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

16
bots/weather/readme.md Normal file
View file

@ -0,0 +1,16 @@
# WeatherBot
* This is a bot that sends weather information to a selected stream on
request.
* Weather information is brought to the website using an
OpenWeatherMap API. The bot posts the weather information to the
stream from which the user inputs the message. If the user inputs a
city that does not exist, the bot displays a "Sorry, city not found"
message.
* Before using this bot, you have to generate an OpenWeatherMap API
key and replace the dummy value in .weather_config.
![Example Usage](assets/screen1.png)
![Wrong City](assets/screen2.png)

75
bots/weather/weather.py Normal file
View file

@ -0,0 +1,75 @@
# See readme.md for instructions on running this code.
from __future__ import print_function
import requests
import json
import os
import sys
from six.moves.configparser import SafeConfigParser
class WeatherHandler(object):
def __init__(self):
self.directory = os.path.dirname(os.path.realpath(__file__)) + '/'
self.config_name = '.weather_config'
self.response_pattern = 'Weather in {}, {}:\n{} F / {} C\n{}'
if not os.path.exists(self.directory + self.config_name):
print('Weather bot config file not found, please set it up in {} file in this bot main directory'
'\n\nUsing format:\n\n[weather-config]\nkey=<OpenWeatherMap API key here>\n\n'.format(self.config_name))
sys.exit(1)
super(WeatherHandler, self).__init__()
def usage(self):
return '''
This plugin will give info about weather in a specified city
'''
def handle_message(self, message, client, state_handler):
help_content = '''
This bot returns weather info for specified city.
You specify city in the following format:
city, state/country
state and country parameter is optional(useful when there are many cities with the same name)
For example:
@**Weather Bot** Portland
@**Weather Bot** Portland, Me
'''.strip()
if (message['content'] == 'help') or (message['content'] == ''):
response = help_content
else:
url = 'http://api.openweathermap.org/data/2.5/weather?q=' + message['content'] + '&APPID='
r = requests.get(url + get_weather_api_key_from_config(self.directory, self.config_name))
if "city not found" in r.text:
response = "Sorry, city not found"
else:
response = format_response(r.text, message['content'], self.response_pattern)
client.send_reply(message, response)
def format_response(text, city, response_pattern):
j = json.loads(text)
city = j['name']
country = j['sys']['country']
fahrenheit = to_fahrenheit(j['main']['temp'])
celsius = to_celsius(j['main']['temp'])
description = j['weather'][0]['description'].title()
return response_pattern.format(city, country, fahrenheit, celsius, description)
def to_celsius(temp_kelvin):
return int(temp_kelvin) - 273.15
def to_fahrenheit(temp_kelvin):
return int(temp_kelvin) * 9 / 5 - 459.67
def get_weather_api_key_from_config(directory, config_name):
config = SafeConfigParser()
with open(directory + config_name, 'r') as config_file:
config.readfp(config_file)
return config.get("weather-config", "key")
handler_class = WeatherHandler

View file

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestWikipediaBot(BotTestCase):
bot_name = "wikipedia"
def test_bot(self):
expected = {
"": 'Please enter your message after @mention-bot',
"sssssss kkkkk": ('I am sorry. The search term you provided '
'is not found :slightly_frowning_face:'),
"foo": ('For search term "foo", '
'https://en.wikipedia.org/wiki/Foobar'),
"123": ('For search term "123", '
'https://en.wikipedia.org/wiki/123'),
"laugh": ('For search term "laugh", '
'https://en.wikipedia.org/wiki/Laughter'),
}
self.check_expected_responses(expected)

View file

@ -0,0 +1,60 @@
from __future__ import absolute_import
from __future__ import print_function
import requests
import logging
import re
# 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 '@mention-bot'
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 "@mention-bot".
'''
def handle_message(self, message, client, state_handler):
bot_response = self.get_bot_wiki_response(message, client)
client.send_reply(message, bot_response)
def get_bot_wiki_response(self, message, client):
help_text = 'Please enter your message after @mention-bot'
query = message['content']
if query == '':
return help_text
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
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:
search_string = data.json()['query']['search'][0]['title'].replace(' ', '_')
url = 'https://en.wikipedia.org/wiki/' + search_string
new_content = new_content + '", ' + url
return new_content
handler_class = WikipediaHandler

0
bots/xkcd/__init__.py Normal file
View file

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

40
bots/xkcd/readme.md Normal file
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.
![](assets/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.
![](assets/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.
![](assets/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.
![](assets/xkcd-specific-id.png)
If you type a wrong command to xkcd bot, xkcd bot will post information
you'd get from `@xkcd help`.
![](assets/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.
![](assets/xkcd-wrong-id.png)

43
bots/xkcd/test_xkcd.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import mock
import os
import sys
our_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.normpath(os.path.join(our_dir)))
# For dev setups, we can find the API in the repo itself.
if os.path.exists(os.path.join(our_dir, '..')):
sys.path.insert(0, '..')
from bots_test_lib import BotTestCase
class TestXkcdBot(BotTestCase):
bot_name = "xkcd"
@mock.patch('logging.exception')
def test_bot(self, mock_logging_exception):
help_txt = "xkcd bot supports these commands:"
err_txt = "xkcd bot only supports these commands:"
commands = '''
* `@xkcd help` to show this help message.
* `@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`.'''
invalid_id_txt = "Sorry, there is likely no xkcd comic strip with id: #"
expected = {
"": err_txt+commands,
"help": help_txt+commands,
"x": err_txt+commands,
"0": invalid_id_txt + "0",
"1": ("#1: **Barrel - Part 1**\n[Don't we all.]"
"(https://imgs.xkcd.com/comics/barrel_cropped_(1).jpg)"),
"1800": ("#1800: **Chess Notation**\n"
"[I've decided to score all my conversations "
"using chess win-loss notation. (??)]"
"(https://imgs.xkcd.com/comics/chess_notation.png)"),
"999999999": invalid_id_txt + "999999999",
}
self.check_expected_responses(expected)

114
bots/xkcd/xkcd.py Normal file
View file

@ -0,0 +1,114 @@
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
"@mention-bot" 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 "@mention-bot".
There are several commands to use this bot:
- @mention-bot help -> To show all commands the bot supports.
- @mention-bot latest -> To fetch the latest comic strip from xkcd.
- @mention-bot random -> To fetch a random comic strip from xkcd.
- @mention-bot <comic_id> -> To fetch a comic strip based on
`<comic_id>`, e.g `@mention-bot 1234`.
'''
def handle_message(self, message, client, state_handler):
xkcd_bot_response = get_xkcd_bot_response(message)
client.send_reply(message, 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()
command = original_content.strip()
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':
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, command)
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

0
bots/yoda/__init__.py Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

69
bots/yoda/readme.md Normal file
View file

@ -0,0 +1,69 @@
# Overview
This is the documentation for how to set up and run the yoda bot. (`yoda.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 at-mention of the botname. You will need to have a
Mashape API key. Please see instructions for getting one below.
## Setup
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`. You can read more about it
on this page.
![yoda api overview](assets/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.config` file.
The `yoda.config` file should be located at `~/yoda.config`.
Example input:
@mention-bot You will learn how to speak like me someday.
If you need help while the bot is running just input `@mention-bot help`.
## Running the bot
Here is an example of running the "yoda" bot from
inside a Zulip repo:
cd ~/zulip/api
bots_api/run.py bots/yoda/yoda.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

Some files were not shown because too many files have changed in this diff Show more