├── .gitignore ├── LICENSE ├── README.md ├── converse ├── converse.py ├── creds.cfg.sample ├── requirements.txt └── topics.json ├── example-api-calls.md └── simple-post ├── creds.cfg.sample ├── post.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeff Kramer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyBot 2 | 3 | Resources for Slack Bots in Python. Whipped up by [@jeffk](https://twitter.com/jeffk) and presented at PyTexas 2015 and the ATX Bot Talk meetup. [Slides from PyTexas](http://www.slideshare.net/jeffkramer1/hello-pybot) and [ATX Bot Talk, v2](http://www.slideshare.net/jeffkramer1/atx-bot-talk-hello-pybot) are available, and [a video of the 20 minute PyTexas presentation is on YouTube](https://www.youtube.com/watch?v=7jwwhk5W56A). 4 | 5 | ## simple-post/post.py 6 | 7 | A simple command line script to post a message to a channel as a bot user. The bot user integration must be created in Slack first, and the bot user invited to the channel. 8 | 9 | ## converse/converse.py 10 | 11 | A small text matching and response bot, with triggers and responses stored in a JSON file. 12 | 13 | # Misc Resources 14 | 15 | * [The Slack API Docs](https://api.slack.com/web) 16 | * [Slack's List of Python Slack Tools](https://api.slack.com/community#python) 17 | * Ben Brown's RefreshAustin Presentation - [Your Friendly Robot Companions](https://vimeo.com/133520585) 18 | * Slack's [python-slackclient](https://github.com/slackhq/python-slackclient) API wrapper and [python-rtmbot](https://github.com/slackhq/python-rtmbot) plugin-enabled bot framework 19 | * [dev4slack.xoxco.com](https://dev4slack.xoxco.com), XOXCO's Slackbot Developer Slack 20 | * [The Jack Principles](http://demos.jellyvisionlab.com/downloads/The_Jack_Principles.pdf) - Thoughts on creating conversational interfaces 21 | * [When One App Rules Them All: The Case of WeChat and Mobile in China](https://a16z.com/2015/08/06/wechat-china-mobile-first/) on messaging as a platform 22 | -------------------------------------------------------------------------------- /converse/converse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse, ConfigParser, sys, json, os 4 | from slackclient import SlackClient 5 | from time import sleep 6 | 7 | class Converser: 8 | topics = {} 9 | client = None 10 | debug = False 11 | my_user_name = '' 12 | 13 | def connect(self, token): 14 | self.client = SlackClient(token) 15 | self.client.rtm_connect() 16 | self.my_user_name = self.client.server.username 17 | print("Connected to Slack.") 18 | 19 | def listen(self): 20 | while True: 21 | try: 22 | input = self.client.rtm_read() 23 | if input: 24 | for action in input: 25 | if self.debug: 26 | print(action) 27 | if 'type' in action and action['type'] == "message": 28 | # Uncomment to only respond to messages addressed to us. 29 | # if 'text' in action 30 | # and action['text'].lower().startswith(self.my_user_name): 31 | self.process_message(action) 32 | else: 33 | sleep(1) 34 | except Exception as e: 35 | print("Exception: ", e.message) 36 | 37 | def process_message(self, message): 38 | for topic in self.topics.keys(): 39 | if topic.lower() in message['text'].lower(): 40 | response = self.topics[topic].format(**message) 41 | if response.startswith("sys:"): 42 | response = os.popen(response[4:]).read() 43 | print("Posting to [%s]: %s" % (message['channel'], response)) 44 | self.post(message['channel'], response) 45 | 46 | def post(self, channel, message): 47 | chan = self.client.server.channels.find(channel) 48 | 49 | if not chan: 50 | raise Exception("Channel %s not found." % channel) 51 | 52 | return chan.send_message(message) 53 | 54 | if __name__ == "__main__": 55 | 56 | parser = argparse.ArgumentParser( 57 | formatter_class=argparse.RawDescriptionHelpFormatter, 58 | description=''' 59 | This script posts responses to trigger phrases. 60 | Run with: 61 | converse.py topics.json 62 | ''', 63 | epilog='''''' ) 64 | parser.add_argument('-d', action='store_true', help="Print debug output.") 65 | parser.add_argument('topics_file', type=str, nargs=1, 66 | help='JSON of phrases/responses to read.') 67 | args = parser.parse_args() 68 | 69 | # Create a new Converser 70 | conv = Converser() 71 | 72 | if args.d: 73 | conv.debug = True 74 | 75 | # Read our token and connect with it 76 | config = ConfigParser.RawConfigParser() 77 | config.read('creds.cfg') 78 | token = config.get("Slack","token") 79 | 80 | conv.connect(token) 81 | 82 | # Add our topics to the converser 83 | with open(args.topics_file[0]) as data_file: 84 | conv.topics = json.load(data_file) 85 | 86 | # Run our conversation loop. 87 | conv.listen() 88 | -------------------------------------------------------------------------------- /converse/creds.cfg.sample: -------------------------------------------------------------------------------- 1 | [Slack] 2 | 3 | token = 12345 4 | -------------------------------------------------------------------------------- /converse/requirements.txt: -------------------------------------------------------------------------------- 1 | slackclient -------------------------------------------------------------------------------- /converse/topics.json: -------------------------------------------------------------------------------- 1 | { 2 | "what time is it": "Whatever time it needs to be.", 3 | "who am I": "You are human meatbag identifier `{user}`.", 4 | "uptime": "sys:uptime" 5 | } 6 | -------------------------------------------------------------------------------- /example-api-calls.md: -------------------------------------------------------------------------------- 1 | ### Incoming Web Hooks 2 | 3 | ``` 4 | curl -X POST --data-urlencode \ 5 | 'payload={"text": "This is posted to <#general> and comes from *monkey-bot*.", \ 6 | "channel": "#general", "username": "monkey-bot", "icon_emoji": ":monkey_face:"}' \ 7 | https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX 8 | ``` 9 | 10 | ### Outgoing Web Hooks 11 | 12 | Post from Slack: 13 | 14 | ``` 15 | token=XXXXXXXXXXXXXXXXXX 16 | team_id=T0001 17 | team_domain=example 18 | channel_id=C2147483705 19 | channel_name=test 20 | timestamp=1355517523.000005 21 | user_id=U2147483697 22 | user_name=Steve 23 | text=googlebot: What is the air-speed velocity of an unladen swallow? 24 | trigger_word=googlebot: 25 | ``` 26 | 27 | Response: 28 | 29 | ``` 30 | { 31 | "text": "African or European?" 32 | } 33 | ``` 34 | 35 | ### Slash Commands 36 | 37 | Respond with plain text to be shown to user. 38 | -------------------------------------------------------------------------------- /simple-post/creds.cfg.sample: -------------------------------------------------------------------------------- 1 | [Slack] 2 | 3 | token = 12345 4 | -------------------------------------------------------------------------------- /simple-post/post.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse, ConfigParser, sys 4 | from slackclient import SlackClient 5 | 6 | def post(client, channel, message): 7 | channel = channel.lstrip("#") 8 | chan = client.server.channels.find(channel) 9 | 10 | if not chan: 11 | raise Exception("Channel %s not found." % channel) 12 | 13 | my_user_name = client.server.username 14 | my_user = None 15 | 16 | for user in client.server.users: 17 | if user.name == my_user_name: 18 | my_user = user 19 | break 20 | 21 | if not my_user: 22 | raise Exception("User %s missing" % my_user_name) 23 | 24 | if my_user.id not in chan.members: 25 | raise Exception("%s not in channel %s, please /invite them." % (my_user_name, channel)) 26 | 27 | return chan.send_message(message) 28 | 29 | if __name__ == "__main__": 30 | 31 | parser = argparse.ArgumentParser( 32 | formatter_class=argparse.RawDescriptionHelpFormatter, 33 | description=''' 34 | This script posts a message into a slack channel. 35 | Sample commands: 36 | post.py mychannel "This is a message." 37 | ''', 38 | epilog='''''' ) 39 | parser.add_argument('channel', type=str, nargs=1, 40 | help='Channel to post to') 41 | parser.add_argument('message', type=str, nargs=1, 42 | help='Message to post') 43 | args = parser.parse_args() 44 | 45 | config = ConfigParser.RawConfigParser() 46 | config.read('creds.cfg') 47 | 48 | token = config.get("Slack","token") 49 | 50 | client = SlackClient(token) 51 | client.rtm_connect() 52 | 53 | try: 54 | post(client, args.channel[0], args.message[0]) 55 | except Exception as e: 56 | sys.exit("Error: %s" % e.message) 57 | 58 | -------------------------------------------------------------------------------- /simple-post/requirements.txt: -------------------------------------------------------------------------------- 1 | slackclient --------------------------------------------------------------------------------