Meet Will.
Will is the friendliest, easiest-to-teach bot you've ever used. He works on hipchat, in rooms and 1-1 chats. Batteries included.
Quick-links:
Will can:
Hear
class CookiesPlugin(WillPlugin):
@hear("cookies")
def will_likes_cookies(self, message):
self.say("I LOOOOVE COOOKIEEESS!!!")
Reply
# All examples below are impled to be on a subclass of WillPlugin
@respond_to("^hi") # Basic
def hi(self, message):
self.reply(message, "hello, %s!" % message.sender.nick)
@respond_to("award (?P<num_stars>\d)+ gold stars? to (?P<user_name>.*)") # With named matches
def gold_stars(self, message, num_stars=1, user_name=None):
stars = self.load("gold_stars", {})
stars[user_name] += num_stars
self.save("gold_stars", stars)
self.say("Awarded %s stars to %s." % (num_stars, user_name), message=message)
Do things on a schedule.
@periodic(hour='10', minute='0', day_of_week="mon-fri")
def standup(self):
self.say("@all Standup! %s" % settings.WILL_HANGOUT_URL)
Do things randomly
@randomly(start_hour='10', end_hour='17', day_of_week="mon-fri", num_times_per_day=1)
def walkmaster(self):
self.say("@all time for a walk!")
Schedule things on the fly
@randomly(start_hour='10', end_hour='17', day_of_week="mon-fri", num_times_per_day=1)
def walkmaster(self):
now = datetime.datetime.now()
in_5_minutes = now + datetime.timedelta(minutes=5)
self.say("@all Walk happening in 5 minutes!")
self.schedule_say("@all It's walk time!", in_5_minutes)
Remember
Will can remember almost any python object, even across reboots.
self.save("my_key", "my_value")
self.load("my_key", "default value")
Respond to webhooks
# Simply
@route("/ping")
def ping(self):
return "PONG"
# With templates
@route("/keep-alive")
@rendered_template("keep_alive.html")
def keep_alive(self):
return {}
# With full control, multiple templates, still connected to chat.
@route("/complex_page/<page_id:int>", method=POST)
def complex_page(self, page_id):
# Talk to chat
self.say("Hey, somebody's loading the complex page.")
# Get JSON post data:
post_data = self.request.json
# Render templates
header = rendered_template("header.html", post_data)
some_other_context = {"page_id": page_id}
some_other_context["header"] = header
return rendered_template("complex_page.html", some_other_context)
Talk in HTML and plain text
roster.py
@respond_to("who do you know about\?")
def list_roster(self, message):
context = {"internal_roster": self.internal_roster.values(),}
self.say(rendered_template("roster.html", context), message=message, html=True)
roster.html
Here's who I know: <br>
<ul>
{% for user in internal_roster %}
<li><b>@{{user.nick|lower}}</b> - {{user.name}}. (# {{user.hipchat_id}})</li>
{% endfor %}
</ul>
Understand natural time
@respond_to("remind me to (?P<reminder_text>.*?) (at|on) (?P<remind_time>.*)")
def remind_me_at(self, message, reminder_text=None, remind_time=None):
# Parse the time
now = datetime.datetime.now()
parsed_time = self.parse_natural_time(remind_time)
# Make a friendly reply
natural_datetime = self.to_natural_day_and_time(parsed_time)
# Schedule the reminder
formatted_reminder_text = "@%(from_handle)s, you asked me to remind you %(reminder_text)s" % {
"from_handle": message.sender.nick,
"reminder_text": reminder_text,
}
self.schedule_say(formatted_reminder_text, parsed_time, message=message)
# Confirm that he heard you.
self.say("%(reminder_text)s %(natural_datetime)s. Got it." % locals(), message=message)
Then:
You: @will remind me to take out the trash at 6pm tomorrow
Will: take out the trash tomorrow at 6pm. Got it.
or:
You: @will remind me to take out the trash at 6pm monday
Will: take out the trash December 16 at 6pm. Got it.
Document himself
@respond_to("image me (?P<search_query>.*)$")
def image_me(self, message, search_query):
"""image me ___ : Search google images for ___, and post a random one."""
pass
Then:
You: @will help
Will: Sure thing, Steven.
Will: Here's what I know how to do:
image me ___ : Search google images for ___, and post a random one.
A lot more
We've built will to be easy to extend, change, and write. Check out the plugins directory for lots more examples!
You can also take a look at our will. He's open-source, handles our deploys and lots of fun things - enjoy!
High-level API
@hear(regex, include_me=False, case_sensitive=False, multiline=False)
regex
: a regular expression to match.include_me
: whether will should hear what he sayscase_sensitive
: should the regex be case sensitive?
Plugin method decorators
@hear(regex, include_me=False, case_sensitive=False, multiline=False)
regex
: a regular expression to match.include_me
: whether will should hear what he sayscase_sensitive
: should the regex be case sensitive?
@respond_to(regex, include_me=False, case_sensitive=False, multiline=False)
regex
: a regular expression to match.include_me
: whether will should hear what he sayscase_sensitive
: should the regex be case sensitive?
@periodic(**periodic_args)
Args are parsed by apscheduler.
year
: 4-digit year numbermonth
: month number (1-12)day
: day of the month (1-31)week
: ISO week number (1-53)day_of_week
: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)hour
: hour (0-23)minute
: minute (0-59)second
: second (0-59)
The following expressions are valid:
*
(any): Fire on every value*/a
(any): Fire every a values, starting from the minimuma-b
(any): Fire on any value within the a-b range (a must be smaller than b)a-b/c
(any): Fire every c values within the a-b rangexth y
(day): Fire on the x -th occurrence of weekday y within the monthlast x
(day): Fire on the last occurrence of weekday x within the monthlast
(day): Fire on the last day within the monthx,y,z
(any): Fire on any matching expression; can combine any number of any of the above expressions
@randomly(start_hour=0, end_hour=23, day_of_week="*", num_times_per_day=1)
start_hour
: the earliest a random task could fall.end_hour
: the latest hour a random task could fall (inclusive, so end_hour:59 is a possible time.)day_of_week
: valid days of the week, same expressions available as@periodic
num_times_per_day
: number of times this task should happen per day.
@route(routing_rule)
Routes a bottle request. Note that self.request
will contain the bottle request object
routing_rule
: A bottle routing rule. Args in the bottle rule are automatically passed into the function.
@rendered_template("template_name.html")
"template_name.html"
: the path to the template, relative to thetemplates
directory. Assumes the function returns a dictionary, to be used as the template context.
High-level chat methods
A note about multiple rooms: For all methods that include message=None, room=None
, both are optional, unless you have multiple chat rooms. If you have multiple rooms, you will need to specify either message
or room
. To reply to the room the message came from, use message
. To send to a specific room, use room
.
Typically, it's considered good form to pass message=message
along when you have it - it'll save you from needing to refactor when you do have multiple rooms!
self.say(content, message=None, room=None, html=False, color="green", notify=False)
Speak directly into a room or 1-1 message.
content
: the content you want to send to the room. HTML or plain text.message
: (optional) The incoming message objectroom
: (optional) The room object (from self.available_rooms) to send the message to.html
: if the message is HTML.True
orFalse
.color
: the hipchat color to send. "yellow", "red", "green", "purple", "gray", or "random". Default is "green".notify
: whether the message should trigger a 'ping' notification.True
orFalse
.
self.reply(message, content, html=False, color="green", notify=False)
Reply to a direct message, either @will
'd, or in a 1-1 room. Note: html is stripped for 1-1 messages
message
: The incoming message object. Requiredcontent
: the content you want to send to the room. HTML or plain text.html
: if the message is HTML.True
orFalse
.color
: the hipchat color to send. "yellow", "red", "green", "purple", "gray", or "random". Default is "green".notify
: whether the message should trigger a 'ping' notification.True
orFalse
.
self.set_topic(topic, message=None, room=None)
Set the room topic. Note: you can't set the topic of a 1-1 chat. Will will complain politely.
topic
: The string you want to set the topic tomessage
: (optional) The incoming message objectroom
: (optional) The room object (from self.available_rooms) to send the message to.
self.schedule_say(content, when, message=None, room=None, html=False, color="green", notify=False)
Schedule a .say()
for a future time
content
: the content you want to send to the room. HTML or plain text.when
: when you want the message to be said. Pythondatetime
object.message
: (optional) The incoming message objectroom
: (optional) The room object (from self.available_rooms) to send the message to.html
: if the message is HTML.True
orFalse
.color
: the hipchat color to send. "yellow", "red", "green", "purple", "gray", or "random". Default is "green".notify
: whether the message should trigger a 'ping' notification.True
orFalse
.
High-level helpers
self.parse_natural_time(time_string)
Parses a textual time string using parsedatetime
time_string
: the time string you want to parse.
self.to_natural_day_and_time(my_datetime)
Converts a python datetime
into a human-friendly string using natural, and a bit of extra code.
my_datetime
: the python datetime to convert
self.rendered_template(template_name, context={})
Renders a template using Jinja
template_name
: path to the template, relative to/templates
.context
: a dictionary to render the template with.
Advanced Topics
Multiple Chat Rooms
Will fully supports multiple chat rooms. To take advantage of them, you'll need to:
- Include both rooms, semicolon-separated in
WILL_ROOMS
- Make sure to include either
message
orroom
on any calls to.say()
,set_topic()
, orschedule_say()
you have a specific room in mind for, or don't want going to the default room.
Auto documentation
Will has two kinds of help: regular help, and programmer help.
Regular help lists all listen/reply methods that have docstrings. If the docstring includes a colon, i.e. "my command: does cool stuff", it will be formatted nicely as "my command: does cool stuff"
Programmer help lists the regexes for all listen/reply methods. It's significantly less friendly, but still useful for more buried/admin-type functions.
Installation
Starting a new will
-
pip install will
-
Install and configure redis
-
Set environment variables. The easiest way to do this is to put all of the following in your virtualenv's
bin/postactivate
file, so it's always there when youworkon will
, and things Just Work:# Required export WILL_USERNAME='[email protected]' export WILL_PASSWORD='asj2498q89dsf89a8df' export WILL_V2_TOKEN='asdfjl234jklajfa3azfasj3afa3jlkjiau' export WILL_ROOMS='Testing, Will Kahuna;GreenKahuna' # Semicolon-separated, so you can have commas in names. export WILL_NAME='William T. Kahuna' # Must be the *exact, case-sensitive* full name from hipchat. export WILL_HANDLE='will' # Must be the exact handle from hipchat. export WILL_REDIS_URL="redis://localhost:6379/7" # Optional export WILL_TOKEN='kjadfj89a34878adf78789a4fae3' # for v1 API. Must be an 'admin' token, not just notification. Optional, unless you have > ~30 rooms. export WILL_DEFAULT_ROOM='[email protected]' # Default room: (otherwise defaults to the first of WILL_ROOMS) export WILL_HANGOUT_URL='https://plus.google.com/hangouts/_/event/ceggfjm3q3jn8ktan7k861hal9o...' # For google hangouts: export WILL_DEFAULT_FROM_EMAIL="[email protected]" export WILL_MAILGUN_API_KEY="key-12398912329381" export WILL_MAILGUN_API_URL="example.com" export WILL_LOGLEVEL="DEBUG" # For Production: export WILL_HTTPSERVER_PORT="80" # Port to listen to (defaults to $PORT, then 80.) Set > 1024 to run without elevated permission. export WILL_URL="http://my-will.herokuapp.com" # If will isn't accessible at localhost (heroku, etc). No trailing slash.:
-
Run
generate_will_project
. This will create the following structure (you can also create it by hand):/plugins __init__.py hello.py /templates .gitignore run_will.py requirements.txt Procfile README.md
Where
run_will.py
is#!/usr/bin/env python from will.main import WillBot if __name__ == '__main__': bot = WillBot(plugins_dirs=["plugins",], template_dirs=["templates",]) bot.bootstrap()
-
Just run
./run_will.py
!
Deploying on Heroku
-
Create a new will, as above.
-
Set up your heroku app, and a redis addon.
heroku create our-will-name heroku addons:add rediscloud # Or redistogo, etc. Your call.
-
Add all the needed environment variables:
heroku config:set \ WILL_URL="http://our-will-name.herokuapp.com" \ WILL_USERNAME='[email protected]' \ WILL_PASSWORD='asj2498q89dsf89a8df' \ WILL_TOKEN='kjadfj89a34878adf78789a4fae3' \ WILL_V2_TOKEN='asdfjl234jklajfa3azfasj3afa3jlkjiau' \ WILL_ROOMS='Testing, Will Kahuna;GreenKahuna' \ WILL_NAME='William T. Kahuna' \ WILL_HANDLE='will' \ WILL_REDIS_URL="`heroku config:get REDISCLOUD_URL`" \ WILL_DEFAULT_ROOM='[email protected]' \ WILL_HANGOUT_URL='https://plus.google.com/hangouts/_/event/ceggfjm3q3jn8ktan7k861hal9o...' \ TZ="America/Los_Angeles" # Or whatever your time zone is.
-
git push heroku
-
heroku scale web=1
Hacking on will
Most of the time, you'll want to start a new will, as above, and add your functionality to your project. However, if you'd like to make improvements to will itself (PRs are welcome!), here's how to test.
- Fork this repo.
- Clone down a copy, set up redis and the env, as above.
- Run
./start_dev_will.py
to start up just core will.
The Shoulders of Giants
Will leverages some fantastic libraries. He wouldn't exist without them.
- Bottle for http handling
- Jinja for templating
- Sleekxmpp for listening to xmpp
- natural and parsedatetime for natural date parsing
- apscheduler for scheduled task parsing
- Requests to make http sane.
Will was originally written by Steven Skoczen at GreenKahuna. The rest of the GK team has also pitched in, including:
- ckcollab, and
- levithomason
Will's also has had help from lots of coders. Alphabetically:
- adamgilman gave you the friendly error messages when the hipchat key was invalid.
- bfhenderson removed dependence on the v1 token.
- crccheck gave you friendly error messages if your
WILL_ROOMS
was wrong. - dpoirier figured out how to properly ignore the initial catch-up messages, and gave you log-level control.
- jbeluch found a bug with
get_roster
not populating in time. - michaeljoseph suggested improvements to setup and requirements.txt format.
- quixeybrian wrote the awesome new help system and stopped the rate limit nightmare.
- rbp fixed a bug with
room
not being passed along properly to messages.
Releases
0.4.9 - May 28, 2014
- Passing a
room
to a.say()
now works properly, thanks to rbp. - New optional
WILL_LOGLEVEL
setting, thanks to dpoirier.
0.4.8 - May 21, 2014
- Will now ignores all previously sent messages properly, by passing in
bot
as the resource instead of an ugly time hack, thanks to dpoirier.
0.4.7 - May 15, 2014
- Will now prints a helpful message if one of your
WILL_ROOMS
is wrong, and continues starting, instead of crashing in a fiery ball, thanks to crccheck.
0.4.6 - May 5, 2014
@route
decorators now honor all bottle arguments, most helpfullymethod
!
0.4.5 - May 2, 2014
- Awesome new help system by quixeybrian.
- "@will help" now only displays functions with docstrings, and formats them nicely.
- Old help (regexes and all) is available at "@will programmer help"
0.4.4 - April 22, 2014
- Removes the dependence on the v1 token (though it still helps with rate-limiting), thanks to bfhenderson.
- Much friendlier error message on an invalid API key, thanks to adamgilman.
0.4.3 - ~ April 1, 2014
- Support for hundreds of users and rooms without hitting the API limit.
get_all_users
use of the bulk API added by quixeybrian. Thanks also to jbeluch and jdrukman for nudges in the right direction.- The start of some useful comments - the meat of will was hacked out by one person over a handful of days and it looks that way. Slowly but surely making this codebase more friendly to other contributions!
- Added a CONTRIBUTING.md file thanks to michaeljoseph.
- Proper releases in the docs, and an updated
AUTHORS
file. If you see something awry, send a PR!
0.4 - ~ March 2014
- Ye olden past before we started keeping this list. All contributions by GreenKahuna. Will did everything that's not in the release list above. That's called lazy retconning release lists!