bots: Move all bots and the bots API to separate package.
5
zulip_bots/MANIFEST.in
Normal file
|
@ -0,0 +1,5 @@
|
|||
include zulip_bots/bots/giphy/fixtures/test_1.json
|
||||
include zulip_bots/bots/github_detail/fixtures/test_404.json
|
||||
include zulip_bots/bots/github_detail/fixtures/test_issue.json
|
||||
include zulip_bots/bots/github_detail/fixtures/test_pull.json
|
||||
|
143
zulip_bots/README.md
Normal file
|
@ -0,0 +1,143 @@
|
|||
# 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.
|
||||
|
||||
## Installing
|
||||
|
||||
To install the bots and bots API, run:
|
||||
|
||||
python setup.py install
|
||||
|
||||
## Running bots
|
||||
|
||||
Here is an example of running the "follow-up" bot:
|
||||
|
||||
zulip-run-bot followup --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.
|
78
zulip_bots/setup.py
Executable file
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
# We should be installable with either setuptools or distutils.
|
||||
package_info = dict(
|
||||
name='zulip_bots',
|
||||
version='0.3.1',
|
||||
description='Zulip\'s Bot framework',
|
||||
author='Zulip Open Source Project',
|
||||
author_email='zulip-devel@googlegroups.com',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Communications :: Chat',
|
||||
],
|
||||
url='https://www.zulip.org/',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'zulip-run-bot=zulip_bots.run:main',
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
) # type: Dict[str, Any]
|
||||
|
||||
setuptools_info = dict(
|
||||
install_requires=[
|
||||
'zulip>=0.3.1',
|
||||
'mock>=2.0.0',
|
||||
'html2text', # for bots/define
|
||||
'PyDictionary', # for bots/thesaurus
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
from setuptools import setup, find_packages
|
||||
package_info.update(setuptools_info)
|
||||
package_info['packages'] = find_packages()
|
||||
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
from distutils.version import LooseVersion
|
||||
from importlib import import_module
|
||||
|
||||
# Manual dependency check
|
||||
def check_dependency_manually(module_name, version=None):
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
if version is not None:
|
||||
assert(LooseVersion(module.__version__) >= LooseVersion(version))
|
||||
except (ImportError, AssertionError):
|
||||
if version is not None:
|
||||
print("{name}>={version} is not installed.".format(
|
||||
req=req, version=version), file=sys.stderr)
|
||||
else:
|
||||
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
check_dependency_manually('zulip', '0.3.1')
|
||||
check_dependency_manually('mock', '2.0.0')
|
||||
check_dependency_manually('html2text')
|
||||
check_dependency_manually('PyDictionary')
|
||||
|
||||
# Include all submodules under bots/
|
||||
package_list = ['zulip_bots']
|
||||
dirs = os.listdir('zulip_bots/bots/')
|
||||
for dir_name in dirs:
|
||||
if os.path.isdir(os.path.join('zulip_bots/bots/', dir_name)):
|
||||
package_list.append('zulip_bots.bots.' + dir_name)
|
||||
package_info['packages'] = package_list
|
||||
|
||||
|
||||
setup(**package_info)
|
1
zulip_bots/zulip_bots/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
bot_dependencies
|
0
zulip_bots/zulip_bots/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/commute/__init__.py
Normal file
2
zulip_bots/zulip_bots/bots/commute/commute.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Google.com]
|
||||
api_key = abcdefghijklmnopqrstuvwxyz
|
220
zulip_bots/zulip_bots/bots/commute/commute.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import datetime as dt
|
||||
import requests
|
||||
|
||||
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 initialize(self, bot_handler):
|
||||
self.api_key = bot_handler.get_config_info('commute', 'Google.com')['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.
|
||||
'''
|
||||
|
||||
# determines if bot will respond as a private message/ stream message
|
||||
def send_info(self, message, letter, bot_handler):
|
||||
bot_handler.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, bot_handler):
|
||||
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'.",
|
||||
bot_handler)
|
||||
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'.", bot_handler)
|
||||
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, bot_handler)
|
||||
|
||||
# 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, bot_handler):
|
||||
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), bot_handler)
|
||||
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, bot_handler, state_handler):
|
||||
original_content = message['content']
|
||||
query = original_content.split()
|
||||
|
||||
if "help" in query:
|
||||
self.send_info(message, self.help_info, bot_handler)
|
||||
return
|
||||
|
||||
params = self.parse_pair(query)
|
||||
params['key'] = self.api_key
|
||||
self.add_time_to_params(params)
|
||||
|
||||
rjson = self.receive_response(params, message, bot_handler)
|
||||
if not rjson:
|
||||
return
|
||||
|
||||
self.get_send_content(rjson, params, message, bot_handler)
|
||||
|
||||
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_helper_functions():
|
||||
test_parse_pair()
|
||||
test_calculate_seconds()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_helper_functions()
|
||||
print('Success')
|
71
zulip_bots/zulip_bots/bots/commute/readme.md
Normal file
|
@ -0,0 +1,71 @@
|
|||
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 add a valid API key to commute.conf
|
||||
|
||||
Move
|
||||
|
||||
```
|
||||
~/zulip/api/bots/commute/commute.conf
|
||||
```
|
||||
|
||||
into
|
||||
|
||||
```
|
||||
~/commute.conf
|
||||
```
|
||||
|
||||
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.conf
|
||||
|
||||
Sample input and output:
|
||||
|
||||
<pre><code>@commute help</code></pre>
|
||||
|
||||
<pre><code>Obligatory Inputs:
|
||||
Origin e.g. origins=New+York,NY,USA
|
||||
Destination e.g. destinations=Chicago,IL,USA
|
||||
|
||||
Optional Inputs:
|
||||
Mode Of Transport (inputs: driving, walking, bicycling, transit)
|
||||
Default mode (no mode input) is driving
|
||||
e.g. mode=driving or mode=transit
|
||||
Units (metric or imperial)
|
||||
e.g. units=metric
|
||||
Restrictions (inputs: tolls, highways, ferries, indoor)
|
||||
e.g. avoid=tolls
|
||||
Departure Time (inputs: now or (YYYY, MM, DD, HH, MM, SS) departing)
|
||||
e.g. departure_time=now or departure_time=2016,12,17,23,40,40
|
||||
Arrival Time (inputs: (YYYY, MM, DD, HH, MM, SS) arriving)
|
||||
e.g. arrival_time=2016,12,17,23,40,40
|
||||
Languages:
|
||||
Languages list: https://developers.google.com/maps/faq#languagesupport)
|
||||
e.g. language=fr
|
||||
</code></pre>
|
||||
|
||||
Sample request:
|
||||
<pre><code>
|
||||
@commute origins=Chicago,IL,USA destinations=New+York,NY,USA language=fr
|
||||
</code></pre>
|
||||
|
||||
Please note:
|
||||
Fare information can be derived, though is solely dependent on the
|
||||
availability of the information released by public transport operators.
|
||||
Duration in traffic can only be derived if a departure time is set.
|
||||
If a location has spaces in its name, please use a + symbol in the
|
||||
place of the space/s.
|
||||
A departure time and a arrival time can not be inputted at the same time
|
||||
No spaces within addresses.
|
||||
Departure times and arrival times must be in the UTC timezone,
|
||||
you can use the timezone bot.
|
0
zulip_bots/zulip_bots/bots/converter/__init__.py
Normal file
After Width: | Height: | Size: 54 KiB |
0
zulip_bots/zulip_bots/bots/converter/converter.config
Normal file
129
zulip_bots/zulip_bots/bots/converter/converter.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
from math import log10, floor
|
||||
|
||||
import re
|
||||
from zulip_bots.bots.converter import utils
|
||||
|
||||
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, bot_handler, state_handler):
|
||||
bot_response = get_bot_converter_response(message, bot_handler)
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
|
||||
def get_bot_converter_response(message, bot_handler):
|
||||
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
zulip_bots/zulip_bots/bots/converter/readme.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Converter bot
|
||||
|
||||
This bot allows users to perform conversions for various measurement units.
|
||||
|
||||
## Usage
|
||||
|
||||
Run this bot as described in [here](http://zulip.readthedocs.io/en/latest/bots-guide.html#how-to-deploy-a-bot).
|
||||
|
||||
Use this bot with the following command
|
||||
|
||||
`@convert <number> <unit_from> <unit_to>`
|
||||
|
||||
This will convert `number`, given in the unit `unit_from`, to the unit `unit_to`
|
||||
and print the result.
|
||||
|
||||
* `number` can be any floating-point number, e.g. 12, 13.05, 0.002.
|
||||
* `unit_from` and `unit_to` are two units from [the following](#supported-units) table in the same category.
|
||||
* `unit_from` and `unit_to` can be preceded by [these](#supported-prefixes) prefixes.
|
||||
|
||||
### Supported units
|
||||
|
||||
| Category | Units |
|
||||
| ----------------- | ----- |
|
||||
| Area | square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), square-meter (m^2, m2), square-kilometer (km^2, km2), square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), square-mile (mi^2, mi2), are (a), hectare (ha), acre (ac) |
|
||||
| Information | bit, byte |
|
||||
| Length | centimeter (cm), decimeter (dm), meter (m), kilometer (km), inch (in), foot (ft), yard (y), mile (mi), nautical-mile (nmi) |
|
||||
| Temperature | Kelvin (K), Celsius (C), Fahrenheit (F) |
|
||||
| Volume | cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), cubic-foot (ft^3, ft3), cubic-yard (y^3, y3) |
|
||||
| Weight | gram (g), kilogram (kg), ton (t), ounce (oz), pound (lb) |
|
||||
| Cooking (metric only, U.S. and imperial units differ slightly) | teaspoon (tsp), tablespoon (tbsp), cup |
|
||||
|
||||
### Supported prefixes
|
||||
|
||||
| Prefix | Power of 10 |
|
||||
| ------ | ----------- |
|
||||
| atto | 10<sup>-18</sup> |
|
||||
| pico | 10<sup>-15</sup> |
|
||||
| femto | 10<sup>-12</sup> |
|
||||
| nano | 10<sup>-9</sup> |
|
||||
| micro | 10<sup>-6</sup> |
|
||||
| milli | 10<sup>-3</sup> |
|
||||
| centi | 10<sup>-2</sup> |
|
||||
| deci | 10<sup>-1</sup> |
|
||||
| deca | 10<sup>1</sup> |
|
||||
| hecto | 10<sup>2</sup> |
|
||||
| kilo | 10<sup>3</sup> |
|
||||
| mega | 10<sup>6</sup> |
|
||||
| giga | 10<sup>9</sup> |
|
||||
| tera | 10<sup>12</sup> |
|
||||
| peta | 10<sup>15</sup> |
|
||||
| exa | 10<sup>18</sup> |
|
||||
|
||||
### Usage examples
|
||||
|
||||
| Message | Response |
|
||||
| ------- | ------ |
|
||||
| `@convert 12 celsius fahrenheit` | 12.0 celsius = 53.600054 fahrenheit |
|
||||
| `@convert 0.002 kilomile millimeter` | 0.002 kilomile = 3218688.0 millimeter |
|
||||
| `@convert 31.5 square-mile ha | 31.5 square-mile = 8158.4625 ha |
|
||||
| `@convert 56 g lb` | 56.0 g = 0.12345887 lb |
|
||||
|
||||
## Notes
|
||||
|
||||
* You can use multiple `@convert` statements in a message, the response will look accordingly:
|
||||

|
||||
|
||||
* Enter `@convert help` to display a quick overview of the converter's functionality.
|
||||
|
||||
* For bits and bytes, the prefixes change the figure differently: 1 kilobyte is 1024 bytes,
|
||||
1 megabyte is 1048576 bytes, etc.
|
22
zulip_bots/zulip_bots/bots/converter/test_converter.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_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
zulip_bots/zulip_bots/bots/converter/utils.py
Normal 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
zulip_bots/zulip_bots/bots/define/__init__.py
Normal file
BIN
zulip_bots/zulip_bots/bots/define/assets/correct_word.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
zulip_bots/zulip_bots/bots/define/assets/wrong_word.png
Normal file
After Width: | Height: | Size: 19 KiB |
66
zulip_bots/zulip_bots/bots/define/define.py
Normal 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, bot_handler, state_handler):
|
||||
original_content = message['content'].strip()
|
||||
bot_response = self.get_bot_define_response(original_content)
|
||||
|
||||
bot_handler.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 {}'.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
zulip_bots/zulip_bots/bots/define/readme.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# DefineBot
|
||||
|
||||
* This is a bot that defines a word that the user inputs. Whenever the user
|
||||
inputs a message starting with '@define', the bot defines the word
|
||||
that follows.
|
||||
|
||||
* The definitions are brought to the website using an API. The bot posts the
|
||||
definition of the word to the stream from which the user inputs the message.
|
||||
If the user inputs a word that does not exist or a word that is incorrect or
|
||||
is not in the dictionary, the definition is not displayed.
|
||||
|
||||
* For example, if the user says "@define crash", all the meanings of crash
|
||||
appear, each in a separate line.
|
||||
|
||||

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

|
||||
|
1
zulip_bots/zulip_bots/bots/define/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
html2text
|
20
zulip_bots/zulip_bots/bots/define/test_define.py
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_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 their pet cat\n\n"),
|
||||
}
|
||||
self.check_expected_responses(expected)
|
0
zulip_bots/zulip_bots/bots/encrypt/__init__.py
Normal file
After Width: | Height: | Size: 287 KiB |
BIN
zulip_bots/zulip_bots/bots/encrypt/assets/EncryptBot-test.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
zulip_bots/zulip_bots/bots/encrypt/assets/EncryptBot-test2.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
zulip_bots/zulip_bots/bots/encrypt/assets/EncryptBot-test3.png
Normal file
After Width: | Height: | Size: 170 KiB |
45
zulip_bots/zulip_bots/bots/encrypt/encrypt.py
Executable 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, bot_handler, state_handler):
|
||||
bot_response = self.get_bot_encrypt_response(message)
|
||||
bot_handler.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
zulip_bots/zulip_bots/bots/encrypt/readme.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
About EncryptBot:
|
||||
EncryptBot Allows for quick ROT13 encryption in the middle of a chat.
|
||||
|
||||
What It Does:
|
||||
The bot encrypts any message sent to it on any stream it is subscribed to with ROT13.
|
||||
|
||||
How It Works:
|
||||
The bot will Use ROT13(A -> N, B -> O... and vice-versa) in a python
|
||||
implementation to provide quick and easy encryption.
|
||||
|
||||
How to Use:
|
||||
|
||||
-Send the message you want to encrypt, add @encrypt to the beginning.
|
||||
-The Encrypted message will be sent back to the stream the original
|
||||
message was posted in to the topic <sender-email>'s encrypted text.
|
||||
-Messages can be decrypted by sending them to EncryptBot in the same way.
|
19
zulip_bots/zulip_bots/bots/encrypt/test_encrypt.py
Executable file
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_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)
|
0
zulip_bots/zulip_bots/bots/followup/__init__.py
Normal file
46
zulip_bots/zulip_bots/bots/followup/followup.py
Normal 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, bot_handler, state_handler):
|
||||
if message['content'] == '':
|
||||
bot_response = "Please specify the message you want to send to followup stream after @mention-bot"
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
else:
|
||||
bot_response = self.get_bot_followup_response(message)
|
||||
bot_handler.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
|
31
zulip_bots/zulip_bots/bots/followup/test_followup.py
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_bots.test_lib import BotTestCase
|
||||
|
||||
class TestFollowUpBot(BotTestCase):
|
||||
bot_name = "followup"
|
||||
|
||||
def test_bot(self):
|
||||
expected_send_reply = {
|
||||
"": 'Please specify the message you want to send to followup stream after @mention-bot'
|
||||
}
|
||||
self.check_expected_responses(expected_send_reply, expected_method='send_reply')
|
||||
|
||||
expected_send_message = {
|
||||
"foo": {
|
||||
'type': 'stream',
|
||||
'to': 'followup',
|
||||
'subject': 'foo_sender@zulip.com',
|
||||
'content': 'from foo_sender@zulip.com: foo',
|
||||
},
|
||||
"I have completed my task": {
|
||||
'type': 'stream',
|
||||
'to': 'followup',
|
||||
'subject': 'foo_sender@zulip.com',
|
||||
'content': 'from foo_sender@zulip.com: I have completed my task',
|
||||
},
|
||||
}
|
||||
self.check_expected_responses(expected_send_message, expected_method='send_message')
|
0
zulip_bots/zulip_bots/bots/foursquare/__init__.py
Normal file
2
zulip_bots/zulip_bots/bots/foursquare/foursquare.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Foursquare]
|
||||
api_key = abcdefghijksm
|
99
zulip_bots/zulip_bots/bots/foursquare/foursquare.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime as dt
|
||||
import re
|
||||
import requests
|
||||
from six.moves import range
|
||||
|
||||
class FoursquareHandler(object):
|
||||
def initialize(self, bot_handler):
|
||||
self.api_key = bot_handler.get_config_info('foursquare', 'Foursquare')['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, bot_handler):
|
||||
bot_handler.send_reply(message, letter)
|
||||
|
||||
def handle_message(self, message, bot_handler, state_handler):
|
||||
words = message['content'].split()
|
||||
if "/help" in words:
|
||||
self.send_info(message, self.help_info, bot_handler)
|
||||
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'.",
|
||||
bot_handler)
|
||||
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, bot_handler)
|
||||
return
|
||||
|
||||
self.send_info(message,
|
||||
"Invalid Request\nIf stuck, try '@mention-bot help'.",
|
||||
bot_handler)
|
||||
return
|
||||
|
||||
handler_class = FoursquareHandler
|
32
zulip_bots/zulip_bots/bots/foursquare/readme.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# FourSquare Bot
|
||||
|
||||
* This is a bot that returns a list of restaurants from a user input of location,
|
||||
proximity and restaurant type in that exact order. The number of returned
|
||||
restaurants are capped at 3 per request.
|
||||
|
||||
* The list of restaurants are brought to Zulip using an API. The bot sends a GET
|
||||
request to https://api.foursquare.com/v2/. If the user does not correctly input
|
||||
a location, proximity and a restaurant type, the bot will return an error message.
|
||||
|
||||
* For example, if the user says "@foursquare 'Chicago, IL' 80000 seafood", the bot
|
||||
will return:
|
||||
|
||||
Food nearby 'Chicago, IL' coming right up:
|
||||
|
||||
Dee's Seafood Co.
|
||||
2723 S Poplar Ave, Chicago, IL 60608, United States
|
||||
Fish Markets
|
||||
|
||||
Seafood Harbor
|
||||
2131 S Archer Ave (at Wentworth Ave), Chicago, IL 60616, United States
|
||||
Seafood Restaurants
|
||||
|
||||
Joe's Seafood, Prime Steak & Stone Crab
|
||||
60 E Grand Ave (at N Rush St), Chicago, IL 60611, United States
|
||||
Seafood Restaurants
|
||||
|
||||
* If the user enters a wrong word, like "@foursquare 80000 donuts" or "@foursquare",
|
||||
then an error message saying invalid input will be displayed.
|
||||
|
||||
* To get the required API key, visit: https://developer.foursquare.com/overview/auth
|
||||
for more information.
|
0
zulip_bots/zulip_bots/bots/giphy/__init__.py
Normal file
26
zulip_bots/zulip_bots/bots/giphy/fixtures/test_1.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"request": {
|
||||
"api_url": "http://api.giphy.com/v1/gifs/translate",
|
||||
"params": {
|
||||
"s": "Hello",
|
||||
"api_key": "12345678"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"meta": {
|
||||
"status": 200
|
||||
},
|
||||
"data": {
|
||||
"images": {
|
||||
"original": {
|
||||
"url": "https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response-headers": {
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
84
zulip_bots/zulip_bots/bots/giphy/giphy.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# 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'
|
||||
|
||||
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 initialize(self, bot_handler):
|
||||
global config_info
|
||||
config_info = bot_handler.get_config_info('giphy')
|
||||
|
||||
def handle_message(self, message, bot_handler, state_handler):
|
||||
bot_response = get_bot_giphy_response(message, bot_handler)
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
|
||||
|
||||
class GiphyNoResultException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
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, bot_handler):
|
||||
# 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, config_info['key'])
|
||||
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
|
25
zulip_bots/zulip_bots/bots/giphy/test_giphy.py
Executable file
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
|
||||
from zulip_bots.test_lib import BotTestCase
|
||||
|
||||
class TestGiphyBot(BotTestCase):
|
||||
bot_name = "giphy"
|
||||
|
||||
def test_bot(self):
|
||||
bot_response = '[Click to enlarge]' \
|
||||
'(https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif)' \
|
||||
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
|
||||
|
||||
with self.mock_config_info({'key': '12345678'}), \
|
||||
self.mock_http_conversation('test_1'):
|
||||
self.initialize_bot()
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'Hello'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
0
zulip_bots/zulip_bots/bots/git_hub_comment/__init__.py
Normal file
126
zulip_bots/zulip_bots/bots/git_hub_comment/git_hub_comment.py
Normal 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, bot_handler, state_handler):
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
|
||||
handle_input(bot_handler, 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(bot_handler, 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(bot_handler, 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(bot_handler, 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(bot_handler, 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(bot_handler, message, original_sender)
|
||||
logging.error('there was an error with the information you entered')
|
||||
|
||||
|
||||
def send_message(bot_handler, message, original_sender):
|
||||
# function for sending a message
|
||||
bot_handler.send_message(dict(
|
||||
type='private',
|
||||
to=original_sender,
|
||||
content=message,
|
||||
))
|
51
zulip_bots/zulip_bots/bots/git_hub_comment/readme.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# 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`
|
||||
`zulip-run-bot git_hub_comment --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
zulip_bots/zulip_bots/bots/github/__init__.py
Normal file
56
zulip_bots/zulip_bots/bots/github/github.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# The purpose of this file is to handle all requests
|
||||
# to the Github API, which will make sure that all
|
||||
# requests are to the same account, and that all requests
|
||||
# authenticated correctly and securely
|
||||
# The sole purpose of this is to authenticate, and it will
|
||||
# return the requests session that is properly authenticated
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import six.moves.configparser
|
||||
|
||||
|
||||
# This file contains the oauth token for a github user, their username, and optionally, a repository
|
||||
# name and owner. All in the format:
|
||||
# github_repo
|
||||
# github_repo_owner
|
||||
# github_username
|
||||
# github_token
|
||||
CONFIG_FILE = '~/.github-auth.conf'
|
||||
|
||||
global config
|
||||
config = six.moves.configparser.ConfigParser() # Sets up the configParser to read the .conf file
|
||||
config.read([os.path.expanduser(CONFIG_FILE)]) # Reads the config file
|
||||
|
||||
|
||||
def auth():
|
||||
# Creates and authorises a requests session
|
||||
session = requests.session()
|
||||
session.auth = (get_username(), get_oauth_token())
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def get_oauth_token():
|
||||
_get_data('token')
|
||||
|
||||
|
||||
def get_username():
|
||||
_get_data('username')
|
||||
|
||||
|
||||
def get_repo():
|
||||
_get_data('repo')
|
||||
|
||||
|
||||
def get_repo_owner():
|
||||
_get_data('repo_owner')
|
||||
|
||||
|
||||
def _get_data(key):
|
||||
try:
|
||||
return config.get('github', 'github_%s' % (key))
|
||||
except Exception:
|
||||
logging.exception('GitHub %s not supplied in ~/.github-auth.conf.' % (key))
|
0
zulip_bots/zulip_bots/bots/github_detail/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"request": {
|
||||
"api_url": "https://api.github.com/repos/zulip/zulip/issues/0"
|
||||
},
|
||||
"response": {},
|
||||
"response-headers": {
|
||||
"status": 404,
|
||||
"ok": false,
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"request": {
|
||||
"api_url": "https://api.github.com/repos/zulip/zulip/issues/5365"
|
||||
},
|
||||
"response": {
|
||||
"url": "https://api.github.com/repos/zulip/zulip/issues/5365",
|
||||
"repository_url": "https://api.github.com/repos/zulip/zulip",
|
||||
"labels_url": "https://api.github.com/repos/zulip/zulip/issues/5365/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/zulip/zulip/issues/5365/comments",
|
||||
"events_url": "https://api.github.com/repos/zulip/zulip/issues/5365/events",
|
||||
"html_url": "https://github.com/zulip/zulip/issues/5365",
|
||||
"id": 235630936,
|
||||
"number": 5365,
|
||||
"title": "frontend: Enable hot-reloading of CSS in development",
|
||||
"user": {
|
||||
"login": "timabbott",
|
||||
"id": 2746074,
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/2746074?v=3",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/timabbott",
|
||||
"html_url": "https://github.com/timabbott",
|
||||
"followers_url": "https://api.github.com/users/timabbott/followers",
|
||||
"following_url": "https://api.github.com/users/timabbott/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/timabbott/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/timabbott/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/timabbott/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/timabbott/orgs",
|
||||
"repos_url": "https://api.github.com/users/timabbott/repos",
|
||||
"events_url": "https://api.github.com/users/timabbott/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/timabbott/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 419106932,
|
||||
"url": "https://api.github.com/repos/zulip/zulip/labels/area:%20tooling",
|
||||
"name": "area: tooling",
|
||||
"color": "bfd4f2",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 265216831,
|
||||
"url": "https://api.github.com/repos/zulip/zulip/labels/enhancement",
|
||||
"name": "enhancement",
|
||||
"color": "84b6eb",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": 265216832,
|
||||
"url": "https://api.github.com/repos/zulip/zulip/labels/help%20wanted",
|
||||
"name": "help wanted",
|
||||
"color": "159818",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": 621149055,
|
||||
"url": "https://api.github.com/repos/zulip/zulip/labels/priority:%20medium",
|
||||
"name": "priority: medium",
|
||||
"color": "5319e7",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"comments": 1,
|
||||
"created_at": "2017-06-13T17:34:45Z",
|
||||
"updated_at": "2017-06-13T17:34:46Z",
|
||||
"closed_at": null,
|
||||
"body": "There's strong interest among folks working on the frontend in being able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\nIn order to do this, step 1 is to move our CSS minification pipeline from django-pipeline to Webpack. ",
|
||||
"closed_by": null
|
||||
},
|
||||
"response-headers": {
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"request": {
|
||||
"api_url": "https://api.github.com/repos/zulip/zulip/issues/5345"
|
||||
},
|
||||
"response": {
|
||||
"url": "https://api.github.com/repos/zulip/zulip/issues/5345",
|
||||
"repository_url": "https://api.github.com/repos/zulip/zulip",
|
||||
"labels_url": "https://api.github.com/repos/zulip/zulip/issues/5345/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/zulip/zulip/issues/5345/comments",
|
||||
"events_url": "https://api.github.com/repos/zulip/zulip/issues/5345/events",
|
||||
"html_url": "https://github.com/zulip/zulip/pull/5345",
|
||||
"id": 235340230,
|
||||
"number": 5345,
|
||||
"title": "[WIP] modal: Replace bootstrap modal with custom modal class",
|
||||
"user": {
|
||||
"login": "jackrzhang",
|
||||
"id": 12771126,
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/12771126?v=3",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/jackrzhang",
|
||||
"html_url": "https://github.com/jackrzhang",
|
||||
"followers_url": "https://api.github.com/users/jackrzhang/followers",
|
||||
"following_url": "https://api.github.com/users/jackrzhang/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/jackrzhang/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/jackrzhang/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/jackrzhang/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/jackrzhang/orgs",
|
||||
"repos_url": "https://api.github.com/users/jackrzhang/repos",
|
||||
"events_url": "https://api.github.com/users/jackrzhang/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/jackrzhang/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 561830290,
|
||||
"url": "https://api.github.com/repos/zulip/zulip/labels/needs%20review",
|
||||
"name": "needs review",
|
||||
"color": "fef2c0",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"comments": 3,
|
||||
"created_at": "2017-06-12T19:27:26Z",
|
||||
"updated_at": "2017-06-13T18:44:12Z",
|
||||
"closed_at": null,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/zulip/zulip/pulls/5345",
|
||||
"html_url": "https://github.com/zulip/zulip/pull/5345",
|
||||
"diff_url": "https://github.com/zulip/zulip/pull/5345.diff",
|
||||
"patch_url": "https://github.com/zulip/zulip/pull/5345.patch"
|
||||
},
|
||||
"body": "An interaction bug (#4811) between our settings UI and the bootstrap modals breaks hotkey support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x] Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x] Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump using bootstrap for the account settings modal and all other modals, replace with `Modal` class\r\n[] Add hotkey support for closing the top modal for `Esc`\r\n\r\nThis should also be a helpful step in removing dependencies from Bootstrap.",
|
||||
"closed_by": null
|
||||
},
|
||||
"response-headers": {
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
}
|
||||
}
|
101
zulip_bots/zulip_bots/bots/github_detail/github_detail.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
import re
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import six.moves.configparser
|
||||
|
||||
import requests
|
||||
|
||||
class GithubHandler(object):
|
||||
'''
|
||||
This bot provides details on github issues and pull requests when they're
|
||||
referenced in the chat.
|
||||
'''
|
||||
|
||||
GITHUB_ISSUE_URL_TEMPLATE = 'https://api.github.com/repos/{owner}/{repo}/issues/{id}'
|
||||
HANDLE_MESSAGE_REGEX = re.compile("(?:([\w-]+)\/)?([\w-]+)?#(\d+)")
|
||||
|
||||
def initialize(self, bot_handler):
|
||||
self.config_info = bot_handler.get_config_info('github_detail', optional=True)
|
||||
self.owner = self.config_info.get("owner", False)
|
||||
self.repo = self.config_info.get("repo", False)
|
||||
|
||||
def usage(self):
|
||||
# type: () -> None
|
||||
return ("This plugin displays details on github issues and pull requests. "
|
||||
"To reference an issue or pull request usename mention the bot then "
|
||||
"anytime in the message type its id, for example:\n"
|
||||
"@**Github detail** #3212 zulip#3212 zulip/zulip#3212\n"
|
||||
"The default owner is {} and the default repo is {}.".format(self.owner, self.repo))
|
||||
|
||||
def format_message(self, details):
|
||||
# type: (Dict[Text, Union[Text, int, bool]]) -> Text
|
||||
number = details['number']
|
||||
title = details['title']
|
||||
link = details['html_url']
|
||||
author = details['user']['login']
|
||||
owner = details['owner']
|
||||
repo = details['repo']
|
||||
|
||||
description = details['body']
|
||||
status = details['state'].title()
|
||||
|
||||
message_string = ('**[{owner}/{repo}#{id}]'.format(owner=owner, repo=repo, id=number),
|
||||
'({link}) - {title}**\n'.format(title=title, link=link),
|
||||
'Created by **[{author}](https://github.com/{author})**\n'.format(author=author),
|
||||
'Status - **{status}**\n```quote\n{description}\n```'.format(status=status, description=description))
|
||||
return ''.join(message_string)
|
||||
|
||||
def get_details_from_github(self, owner, repo, number):
|
||||
# type: (Text, Text, Text) -> Dict[Text, Union[Text, Int, Bool]]
|
||||
# Gets the details of an issues or pull request
|
||||
|
||||
try:
|
||||
r = requests.get(
|
||||
self.GITHUB_ISSUE_URL_TEMPLATE.format(owner=owner, repo=repo, id=number))
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.exception(e)
|
||||
return
|
||||
if r.status_code != requests.codes.ok:
|
||||
return
|
||||
return r.json()
|
||||
|
||||
def get_owner_and_repo(self, issue_pr):
|
||||
owner = issue_pr.group(1)
|
||||
repo = issue_pr.group(2)
|
||||
if owner is None:
|
||||
owner = self.owner
|
||||
if repo is None:
|
||||
repo = self.repo
|
||||
return (owner, repo)
|
||||
|
||||
def handle_message(self, message, bot_handler, state_handler):
|
||||
# type: () -> None
|
||||
# Send help message
|
||||
if message['content'] == 'help':
|
||||
bot_handler.send_reply(message, self.usage())
|
||||
return
|
||||
|
||||
# Capture owner, repo, id
|
||||
issue_prs = re.finditer(
|
||||
self.HANDLE_MESSAGE_REGEX, message['content'])
|
||||
bot_messages = []
|
||||
for issue_pr in issue_prs:
|
||||
owner, repo = self.get_owner_and_repo(issue_pr)
|
||||
if owner and repo:
|
||||
details = self.get_details_from_github(owner, repo, issue_pr.group(3))
|
||||
if details is not None:
|
||||
details['owner'] = owner
|
||||
details['repo'] = repo
|
||||
bot_messages.append(self.format_message(details))
|
||||
else:
|
||||
bot_messages.append("Failed to find issue/pr: {owner}/{repo}#{id}"
|
||||
.format(owner=owner, repo=repo, id=issue_pr.group(3)))
|
||||
else:
|
||||
bot_messages.append("Failed to detect owner and repository name.")
|
||||
if len(bot_messages) == 0:
|
||||
bot_messages.append("Failed to find any issue or PR.")
|
||||
bot_message = '\n'.join(bot_messages)
|
||||
bot_handler.send_reply(message, bot_message)
|
||||
|
||||
handler_class = GithubHandler
|
22
zulip_bots/zulip_bots/bots/github_detail/readme.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# GitHub detail bot
|
||||
|
||||
This bot links and details issues and pull requests.
|
||||
To use it @-mention the bot then type an id:
|
||||
Ids can be specified in three different forms:
|
||||
- Id only: `#2000`
|
||||
- Repository and id: `zulip#2000`
|
||||
- Owner, repository and id `zulip/zulip#2000`
|
||||
|
||||
The id can occur at any time in the message. You
|
||||
can also mention multiple ids in a single message. For example:
|
||||
|
||||
`@**GitHub Detail Bot** find me #5176 and zulip/zulip#4534 .`
|
||||
|
||||
You can configure a default owner and repository.
|
||||
The configuration file should be located at `api/bots/github_detail/github_detail.conf`.
|
||||
It should look like this:
|
||||
```ini
|
||||
[github_detail]
|
||||
owner = <repository owner>
|
||||
repo = <repository name>
|
||||
```
|
85
zulip_bots/zulip_bots/bots/github_detail/test_github_detail.py
Executable file
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
|
||||
from zulip_bots.test_lib import BotTestCase
|
||||
|
||||
class TestGithubDetailBot(BotTestCase):
|
||||
bot_name = "github_detail"
|
||||
|
||||
def test_issue(self):
|
||||
bot_response = '**[zulip/zulip#5365](https://github.com/zulip/zulip/issues/5365)'\
|
||||
' - frontend: Enable hot-reloading of CSS in development**\n'\
|
||||
'Created by **[timabbott](https://github.com/timabbott)**\n'\
|
||||
'Status - **Open**\n'\
|
||||
'```quote\n'\
|
||||
'There\'s strong interest among folks working on the frontend in being '\
|
||||
'able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\n'\
|
||||
'In order to do this, step 1 is to move our CSS minification pipeline '\
|
||||
'from django-pipeline to Webpack. \n```'
|
||||
# This message calls the `send_reply` function of BotHandlerApi
|
||||
with self.mock_http_conversation('test_issue'):
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'zulip/zulip#5365'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
||||
|
||||
def test_pull_request(self):
|
||||
bot_response = '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'\
|
||||
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'\
|
||||
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'\
|
||||
'Status - **Open**\n```quote\nAn interaction bug (#4811) '\
|
||||
'between our settings UI and the bootstrap modals breaks hotkey '\
|
||||
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'\
|
||||
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'\
|
||||
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '\
|
||||
'using bootstrap for the account settings modal and all other modals,'\
|
||||
' replace with `Modal` class\r\n[] Add hotkey support for closing the'\
|
||||
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in removing dependencies from Bootstrap.\n```'
|
||||
# This message calls the `send_reply` function of BotHandlerApi
|
||||
with self.mock_http_conversation('test_pull'):
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'zulip/zulip#5345'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
||||
|
||||
def test_404(self):
|
||||
bot_response = 'Failed to find issue/pr: zulip/zulip#0'
|
||||
# This message calls the `send_reply` function of BotHandlerApi
|
||||
with self.mock_http_conversation('test_404'):
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'zulip/zulip#0'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
||||
|
||||
def test_random_text(self):
|
||||
bot_response = 'Failed to find any issue or PR.'
|
||||
# This message calls the `send_reply` function of BotHandlerApi
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'some random text'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
||||
|
||||
def test_help_text(self):
|
||||
bot_response = 'This plugin displays details on github issues and pull requests. '\
|
||||
'To reference an issue or pull request usename mention the bot then '\
|
||||
'anytime in the message type its id, for example:\n@**Github detail** '\
|
||||
'#3212 zulip#3212 zulip/zulip#3212\nThe default owner is zulip and '\
|
||||
'the default repo is zulip.'
|
||||
# This message calls the `send_reply` function of BotHandlerApi
|
||||
|
||||
mock_config = {'owner': 'zulip', 'repo': 'zulip'}
|
||||
with self.mock_config_info(mock_config):
|
||||
self.initialize_bot()
|
||||
self.assert_bot_response(
|
||||
message = {'content': 'help'},
|
||||
response = {'content': bot_response},
|
||||
expected_method='send_reply'
|
||||
)
|
0
zulip_bots/zulip_bots/bots/github_issues/__init__.py
Normal file
109
zulip_bots/zulip_bots/bots/github_issues/github_issues.py
Normal 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, bot_handler, 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
|
||||
bot_handler.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
|
||||
bot_handler.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
|
0
zulip_bots/zulip_bots/bots/googlesearch/__init__.py
Normal file
93
zulip_bots/zulip_bots/bots/googlesearch/googlesearch.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
from __future__ import print_function
|
||||
import logging
|
||||
import http.bot_handler
|
||||
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.bot_handler.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, bot_handler, state_handler):
|
||||
original_content = message['content']
|
||||
result = get_google_result(original_content)
|
||||
bot_handler.send_reply(message, result)
|
||||
|
||||
handler_class = GoogleSearchHandler
|
||||
|
||||
|
||||
def test():
|
||||
try:
|
||||
urlopen('http://216.58.192.142', timeout=1)
|
||||
print('Success')
|
||||
return True
|
||||
except http.bot_handler.RemoteDisconnected as e:
|
||||
print('Error: {}'.format(e))
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
23
zulip_bots/zulip_bots/bots/googlesearch/readme.md
Normal 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.
|
0
zulip_bots/zulip_bots/bots/helloworld/__init__.py
Normal file
18
zulip_bots/zulip_bots/bots/helloworld/helloworld.py
Normal 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, bot_handler, state_handler):
|
||||
content = 'beep boop'
|
||||
bot_handler.send_reply(message, content)
|
||||
|
||||
handler_class = HelloWorldHandler
|
4
zulip_bots/zulip_bots/bots/helloworld/readme.md
Normal 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.
|
16
zulip_bots/zulip_bots/bots/helloworld/test_helloworld.py
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from six.moves import zip
|
||||
|
||||
from zulip_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
zulip_bots/zulip_bots/bots/help/__init__.py
Normal file
18
zulip_bots/zulip_bots/bots/help/help.py
Normal 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, bot_handler, state_handler):
|
||||
help_content = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip"
|
||||
bot_handler.send_reply(message, help_content)
|
||||
|
||||
handler_class = HelpHandler
|
16
zulip_bots/zulip_bots/bots/help/test_help.py
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from six.moves import zip
|
||||
|
||||
from zulip_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
zulip_bots/zulip_bots/bots/howdoi/__init__.py
Normal file
BIN
zulip_bots/zulip_bots/bots/howdoi/assets/answer_howdoi_all.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
zulip_bots/zulip_bots/bots/howdoi/assets/answer_howdowe.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
zulip_bots/zulip_bots/bots/howdoi/assets/question_howdoi_all.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
zulip_bots/zulip_bots/bots/howdoi/assets/question_howdowe.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
118
zulip_bots/zulip_bots/bots/howdoi/howdoi.py
Normal 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, bot_handler, state_handler):
|
||||
question = message['content'].strip()
|
||||
|
||||
if question.startswith('howdowe!'):
|
||||
bot_handler.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=self.get_answer('howdowe!', question)
|
||||
))
|
||||
|
||||
elif question.startswith('howdoi!'):
|
||||
bot_handler.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=self.get_answer('howdoi!', question)
|
||||
))
|
||||
|
||||
elif question.startswith('howdowe'):
|
||||
bot_handler.send_message(dict(
|
||||
type='stream',
|
||||
to=message['display_recipient'],
|
||||
subject=message['subject'],
|
||||
content=self.get_answer('howdowe', question)
|
||||
))
|
||||
|
||||
elif question.startswith('howdoi'):
|
||||
bot_handler.send_message(dict(
|
||||
type='private',
|
||||
to=message['sender_email'],
|
||||
content=self.get_answer('howdoi', question)
|
||||
))
|
||||
|
||||
|
||||
handler_class = HowdoiHandler
|
56
zulip_bots/zulip_bots/bots/howdoi/readme.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Howdoi bot
|
||||
|
||||
This bot will allow users to get technical answers from
|
||||
[StackOverflow](https://stackoverflow.com). It is build on top of the
|
||||
python command line tool [howdoi](https://github.com/gleitz/howdoi) by
|
||||
Benjamin Gleitzman.
|
||||
|
||||
## Usage
|
||||
|
||||
Simply prepend your questions with one of the following commands. The
|
||||
answer will be formatted differently depending the chosen command.
|
||||
|
||||
| Command | Respond |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| `@howdowe` | Concise answer to the same stream. |
|
||||
| `@howdowe!` | Same as `@howdowe` but with full answer and URL of the solutions. |
|
||||
| `@howdoi` | Concise answer replied to sender via private message. |
|
||||
| `@howdoi!` | Same as `@howdoi` but with full answer and URL of the solutions. |
|
||||
|
||||
## Screenshots
|
||||
|
||||
#### Example 1
|
||||
|
||||
Question -> `@howdowe use supervisor in elixir`
|
||||
|
||||

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

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

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

|
||||
|
||||
**Note:**
|
||||
|
||||
* Line wrapped is enabled with a maximum line length of 85 characters.
|
||||
This could be adjusted in the source code (`HowdoiHandler.MAX_LINE_LENGTH`).
|
||||
|
||||
* *Howdoi* generally perform better if you ask a question using keywords
|
||||
instead of a complete sentences (eg: "How do i make a decorator in Python"
|
||||
-> "python decorator").
|
||||
|
||||
* __[*Limitation*]__ If a answer contains multiple code blocks, the `@howdoi`
|
||||
and `@howdowe` commands would only return the first coding section, use
|
||||
`@howdo[we|i]!` in that case.
|
0
zulip_bots/zulip_bots/bots/incrementor/__init__.py
Normal file
29
zulip_bots/zulip_bots/bots/incrementor/incrementor.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# See readme.md for instructions on running this code.
|
||||
|
||||
|
||||
class IncrementorHandler(object):
|
||||
|
||||
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, bot_handler, state_handler):
|
||||
state = state_handler.get_state() or {'number': 0, 'message_id': None}
|
||||
state['number'] += 1
|
||||
state_handler.set_state(state)
|
||||
if state['message_id'] is None:
|
||||
result = bot_handler.send_reply(message, str(state['number']))
|
||||
state['message_id'] = result['id']
|
||||
state_handler.set_state(state)
|
||||
else:
|
||||
bot_handler.update_message(dict(
|
||||
message_id = state['message_id'],
|
||||
content = str(state['number'])
|
||||
))
|
||||
|
||||
|
||||
handler_class = IncrementorHandler
|
6
zulip_bots/zulip_bots/bots/incrementor/readme.md
Normal 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
zulip_bots/zulip_bots/bots/john/__init__.py
Normal file
BIN
zulip_bots/zulip_bots/bots/john/assets/assist.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
zulip_bots/zulip_bots/bots/john/assets/greetings.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
zulip_bots/zulip_bots/bots/john/assets/joke.png
Normal file
After Width: | Height: | Size: 64 KiB |
86
zulip_bots/zulip_bots/bots/john/assets/var/jokes.json
Normal file
|
@ -0,0 +1,86 @@
|
|||
[
|
||||
{
|
||||
"joke":"Did you hear about the guy whose whole left side was cut off? He's all right now."
|
||||
},
|
||||
{
|
||||
"joke":"I'm reading a book about anti-gravity. It's impossible to put down."
|
||||
},
|
||||
{
|
||||
"joke":"I wondered why the baseball was getting bigger. Then it hit me."
|
||||
},
|
||||
{
|
||||
"joke":"I'm glad I know sign language, it's pretty handy."
|
||||
},
|
||||
{
|
||||
"joke":"My friend's bakery burned down last night. Now his business is toast."
|
||||
},
|
||||
{
|
||||
"joke":"Why did the cookie cry? It was feeling crumby."
|
||||
},
|
||||
{
|
||||
"joke":"I used to be a banker, but I lost interest."
|
||||
},
|
||||
{
|
||||
"joke":"A drum and a symbol fall off a cliff"
|
||||
},
|
||||
{
|
||||
"joke":"Why do seagulls fly over the sea? Because they aren't bay-gulls!"
|
||||
},
|
||||
{
|
||||
"joke":"Why did the fireman wear red, white, and blue suspenders? To hold his pants up."
|
||||
},
|
||||
{
|
||||
"joke":"Why didn't the crab share his food? Because crabs are territorial animals, that don't share anything."
|
||||
},
|
||||
{
|
||||
"joke":"Why was the javascript developer sad? Because he didn't Node how to Express himself."
|
||||
},
|
||||
{
|
||||
"joke":"What do I look like? A JOKE MACHINE!?"
|
||||
},
|
||||
{
|
||||
"joke":"How did the hipster burn the roof of his mouth? He ate the pizza before it was cool."
|
||||
},
|
||||
{
|
||||
"joke":"Why is it hard to make puns for kleptomaniacs? They are always taking things literally."
|
||||
},
|
||||
{
|
||||
"joke":"I'm not a humorless, cold hearted, machine. I have feelings you know... or was supposed to."
|
||||
},
|
||||
{
|
||||
"joke":"Two fish in a tank. One looks to the other and says 'Can you even drive this thing???'"
|
||||
},
|
||||
{
|
||||
"joke":"Two fish swim down a river, and hit a wall. One says: 'Dam!'"
|
||||
},
|
||||
{
|
||||
"joke":"What's funnier than a monkey dancing with an elephant? Two monkeys dancing with an elephant."
|
||||
},
|
||||
{
|
||||
"joke":"How did Darth Vader know what Luke was getting for Christmas? He felt his presents."
|
||||
},
|
||||
{
|
||||
"joke":"What's red and bad for your teeth? A Brick."
|
||||
},
|
||||
{
|
||||
"joke":"What's orange and sounds like a parrot? A Carrot."
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a cow with no legs? Ground beef"
|
||||
},
|
||||
{
|
||||
"joke":"Two guys walk into a bar. You'd think the second one would have noticed."
|
||||
},
|
||||
{
|
||||
"joke":"What is a centipedes's favorite Beatle song? I want to hold your hand, hand, hand, hand..."
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a chicken crossing the road? Poultry in moton. "
|
||||
},
|
||||
{
|
||||
"joke":"What do you call a fake noodle? An impasta"
|
||||
},
|
||||
{
|
||||
"joke":"How many tickles does it take to tickle an octupus? Ten-tickles!"
|
||||
}
|
||||
]
|
119
zulip_bots/zulip_bots/bots/john/john.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
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__))
|
||||
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, bot_handler, state_handler):
|
||||
original_content = message['content']
|
||||
bot_response = str(bota.get_response(original_content))
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
|
||||
handler_class = JohnHandler
|
30
zulip_bots/zulip_bots/bots/john/readme.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
John
|
||||
|
||||
Instructions:
|
||||
You'll have to install chatterbot to use this bot.
|
||||
Please run: pip install chatterbot on your command line.
|
||||
The script will need to download some NLTK packages after running in your
|
||||
home directory. With the mission of humanizing bot interactions, John aims to be your
|
||||
virtual assistant at the hour of asking for help in Zulip. John is an
|
||||
interactive bot that uses machine learning heuristics to simulate a
|
||||
conversation with the user. He has a great sense of humor and
|
||||
is also powered by Open Source code!
|
||||
|
||||

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

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

|
0
zulip_bots/zulip_bots/bots/thesaurus/__init__.py
Normal file
1
zulip_bots/zulip_bots/bots/thesaurus/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
PyDictionary
|
25
zulip_bots/zulip_bots/bots/thesaurus/test_thesaurus.py
Executable file
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_bots.test_lib import BotTestCase
|
||||
|
||||
class TestThesaurusBot(BotTestCase):
|
||||
bot_name = "thesaurus"
|
||||
|
||||
def test_bot(self):
|
||||
expected = {
|
||||
"synonym good": "great, satisfying, exceptional, positive, acceptable",
|
||||
"synonym nice": "cordial, kind, good, okay, fair",
|
||||
"synonym foo": "bar, thud, X, baz, corge",
|
||||
"antonym dirty": "ordered, sterile, spotless, moral, clean",
|
||||
"antonym bar": "loss, whole, advantage, aid, failure",
|
||||
"": ("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'."),
|
||||
}
|
||||
self.check_expected_responses(expected)
|
67
zulip_bots/zulip_bots/bots/thesaurus/thesaurus.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# See zulip/api/bots/readme.md for instructions on running this code.
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import logging
|
||||
from PyDictionary import PyDictionary as Dictionary
|
||||
|
||||
#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, bot_handler, state_handler):
|
||||
original_content = message['content'].strip()
|
||||
new_content = get_thesaurus_result(original_content)
|
||||
bot_handler.send_reply(message, new_content)
|
||||
|
||||
handler_class = ThesaurusHandler
|
0
zulip_bots/zulip_bots/bots/tictactoe/__init__.py
Normal file
34
zulip_bots/zulip_bots/bots/tictactoe/readme.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# About Tic-Tac-Toe Bot
|
||||
|
||||
This bot allows you to play tic-tac-toe in a private message with the bot.
|
||||
Multiple games can simultaneously be played by different users, each playing
|
||||
against the computer.
|
||||
|
||||
The bot only responds to messages starting with @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
zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py
Normal 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, bot_handler, 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)
|
||||
|
||||
bot_handler.send_message(dict(
|
||||
type = 'private',
|
||||
to = original_sender,
|
||||
subject = message['sender_email'],
|
||||
content = return_content,
|
||||
))
|
||||
|
||||
handler_class = ticTacToeHandler
|
0
zulip_bots/zulip_bots/bots/virtual_fs/__init__.py
Normal file
44
zulip_bots/zulip_bots/bots/virtual_fs/readme.md
Normal 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.
|
48
zulip_bots/zulip_bots/bots/virtual_fs/test_virtual_fs.py
Executable file
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zulip_bots.test_lib import BotTestCase
|
||||
|
||||
class TestVirtualFsBot(BotTestCase):
|
||||
bot_name = "virtual_fs"
|
||||
|
||||
def test_bot(self):
|
||||
expected = {
|
||||
"cd /home": "foo_sender@zulip.com:\nERROR: invalid path",
|
||||
"mkdir home": "foo_sender@zulip.com:\ndirectory created",
|
||||
"pwd": "foo_sender@zulip.com:\n/",
|
||||
"help": ('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'),
|
||||
"help ls": "foo_sender@zulip.com:\nsyntax: ls <optional_path>",
|
||||
"": ('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.check_expected_responses(expected)
|
343
zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py
Normal 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, bot_handler, 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)
|
||||
|
||||
bot_handler.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
|
0
zulip_bots/zulip_bots/bots/weather/__init__.py
Normal file
BIN
zulip_bots/zulip_bots/bots/weather/assets/screen1.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
zulip_bots/zulip_bots/bots/weather/assets/screen2.png
Normal file
After Width: | Height: | Size: 24 KiB |
16
zulip_bots/zulip_bots/bots/weather/readme.md
Normal 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.conf.
|
||||
|
||||

|
||||

|
2
zulip_bots/zulip_bots/bots/weather/weather.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
[weather-config]
|
||||
key=XXXXXXXX
|