skip to navigation
skip to content

Not Logged In

will 0.2.3

A friendly python hipchat bot

Latest Version: 0.4.3

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:
- [Examples](README.md#will-can)
- [High-level API:](README.md#high-level-api)
    - [Plugin method decorators](README.md#plugin-method-decorators)
    - [High-level helpers](README.md#high-level-helpers)
    - [Advanced Topics](README.md#advanced-topics)
- [Installation:](README.md#installation)
    - [Starting a new Will](README.md#starting-a-new-will)
    - [Deploying on Heroku](README.md#deploying-on-heroku)
    - [Hacking on Will](README.md#hacking-on-will)
- [The Shoulders of Giants](README.md#the-shoulders-of-giants)


# Will can:

#### Hear

```python
class CookiesPlugin(WillPlugin):

    @hear("cookies")
    def will_likes_cookies(self, message):
        self.say("I LOOOOVE COOOKIEEESS!!!")
```

#### Reply
```python
# 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.

```python
@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
```python
@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
```python
@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](https://pypi.python.org/pypi/dill) python object, even across reboots.

```python
self.save("my_key", "my_value")
self.load("my_key", "default value")
```

#### Respond to webhooks

```python
# 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

```python
@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
```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
```python
@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.
```

#### 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](https://github.com/greenkahuna/our-will).  He's open-source, handles our deploys and lots of fun things - enjoy!

# High-level API

### Plugin method decorators

###### @hear(regex, include_me=False, case_sensitive=False)

- `regex`: a regular expression to match.
- `include_me`: whether will should hear what he says
- `case_sensitive`: should the regex be case sensitive?

###### @respond_to(regex, include_me=False, case_sensitive=False)

- `regex`: a regular expression to match.
- `include_me`: whether will should hear what he says
- `case_sensitive`: should the regex be case sensitive?

###### @periodic(**periodic_args)

Args are parsed by [apscheduler](http://apscheduler.readthedocs.org/en/latest/cronschedule.html#available-fields).

- `year`: 4-digit year number
- `month`: 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 minimum
- `a-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 range
- `xth y` (day): Fire on the x -th occurrence of weekday y within the month
- `last x` (day): Fire on the last occurrence of weekday x within the month
- `last` (day): Fire on the last day within the month
- `x,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](http://bottlepy.org/docs/dev/api.html#the-request-object)

- `routing_rule`:  A [bottle routing rule](http://bottlepy.org/docs/dev/routing.html).  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 the `templates` 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 object
- `room`: (optional) The room object (from self.available_rooms) to send the message to.
- `html`: if the message is HTML. `True` or `False`.
- `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` or `False`.

##### 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.  Required
- `content`: the content you want to send to the room. HTML or plain text.
- `html`: if the message is HTML. `True` or `False`.
- `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` or `False`.

##### 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 to
- `message`: (optional) The incoming message object
- `room`: (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. Python `datetime` object.
- `message`: (optional) The incoming message object
- `room`: (optional) The room object (from self.available_rooms) to send the message to.
- `html`: if the message is HTML. `True` or `False`.
- `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` or `False`.


### High-level helpers

##### self.parse_natural_time(time_string)

Parses a textual time string using [parsedatetime](https://github.com/bear/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](https://github.com/tehmaze/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](http://jinja.pocoo.org/)

- `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:

1. Include both rooms, semicolon-separated in `WILL_ROOMS`
2. Make sure to include either `message` or `room` on any calls to `.say()`, `set_topic()`, or `schedule_say()` you have a specific room in mind for, or don't want going to the default room.


# Installation

## Starting a new will

1. `pip install will`
2. Install and configure redis
3. Set environment variables:

   ```
    # Required
    export WILL_USERNAME='12345_123456@chat.hipchat.com'
    export WILL_PASSWORD='asj2498q89dsf89a8df'
    export WILL_TOKEN='kjadfj89a34878adf78789a4fae3'
    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'
    export WILL_HANDLE='will'
    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:

    # 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.:
    ```

4. 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
    ```python
    #!/usr/bin/env python
    from will.main import WillBot

    if __name__ == '__main__':
        bot = WillBot(plugins_dirs=["plugins",], template_dirs=["templates",])
        bot.bootstrap()
    ```

5. Just run `./run_will.py`!


## Deploying on Heroku
1. Create a new will, as above.
2. Set up your heroku app, and a redis addon.

    ```bash
    heroku create our-will-name
    heroku addons:add rediscloud   # Or redistogo, etc. Your call.
    ```
3. Add all the needed environment variables:

    ```bash
    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.
    ```

4. `git push heroku`
5. `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.

1. Fork this repo.
2. Clone down a copy, set up redis and the env, as above.
3. 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](http://bottlepy.org/docs/dev/) for http handling
- [Jinja](http://jinja.pocoo.org/) for templating
- [Sleekxmpp](http://sleekxmpp.com/) for listening to xmpp
- [natural](https://github.com/tehmaze/natural) and [parsedatetime](https://github.com/bear/parsedatetime) for natural date parsing
- [apscheduler](http://apscheduler.readthedocs.org/en/latest/) for scheduled task parsing
- [Requests](http://requests.readthedocs.org/en/latest/) to make http sane.
 
File Type Py Version Uploaded on Size
will-0.2.3.tar.gz (md5) Source 2013-12-14 21KB
  • Downloads (All Versions):
  • 87 downloads in the last day
  • 573 downloads in the last week
  • 4214 downloads in the last month