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:
class CookiesPlugin(WillPlugin):
@hear("cookies")
def will_likes_cookies(self, message):
self.say("I LOOOOVE COOOKIEEESS!!!")
# 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)
@periodic(hour='10', minute='0', day_of_week="mon-fri")
def standup(self):
self.say("@all Standup! %s" % settings.WILL_HANGOUT_URL)
@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!")
@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)
Will can remember almost any python object, even across reboots.
self.save("my_key", "my_value")
self.load("my_key", "default value")
# 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)
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>
@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)
# e.g.
# @will remind me to take out the trash at 6pm tomorrow
# > take out the trash tomorrow at 6pm. Got it.
# or
# @will remind me to take out the trash at 6pm monday
# > take out the trash December 16 at 6pm. Got it.
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!
regex
: a regular expression to match.include_me
: whether will should hear what he sayscase_sensitive
: should the regex be case sensitive?
regex
: a regular expression to match.include_me
: whether will should hear what he sayscase_sensitive
: should the regex be case sensitive?
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
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.
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.
"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.
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!
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
.
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
.
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.
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
.
Parses a textual time string using parsedatetime
time_string
: the time string you want to parse.
Converts a python datetime
into a human-friendly string using natural, and a bit of extra code.
my_datetime
: the python datetime to convert
Renders a template using Jinja
template_name
: path to the template, relative to/templates
.context
: a dictionary to render the template with.
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.
-
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='12345_123456@chat.hipchat.com' export WILL_PASSWORD='asj2498q89dsf89a8df' export WILL_TOKEN='kjadfj89a34878adf78789a4fae3' # Must be an 'admin' token, not just notification. 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_DEFAULT_ROOM='12345_room1@conf.hipchat.com' # 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="will@example.com" export WILL_MAILGUN_API_KEY="key-12398912329381" export WILL_MAILGUN_API_URL="example.com" # For Production: export WILL_HTTPSERVER_PORT="80" # Port to listen to (defaults to $PORT, then 80.) 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
!
-
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='12345_123456@chat.hipchat.com' \ 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='12345_room1@conf.hipchat.com' \ 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
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.
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:
- jbeluch found a bug with
get_roster
not populating in time. - michaeljoseph suggested improvements to setup and requirements.txt format.
- 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!
- 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!