Update to rtmbot 0.3.0

Merge tag '0.3.0'
This commit is contained in:
Eryn Wells 2016-09-11 12:19:01 -04:00
commit 80527cbe6e
23 changed files with 256 additions and 27 deletions

33
.github/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,33 @@
# Contributing at Slack
![Header Image](contributing_header_slack.png)
## Before Contributing
Before contributing, please read our [Code of Conduct](./CODE_OF_CONDUCT.md). We take it very seriously, and expect that you will as well.
## New Issues
Before opening a new issue, please consider:
- Reading [the documentation](https://github.com/slackhq/python-rtmbot/blob/master/README.md) and [the changelog](https://github.com/slackhq/python-rtmbot/blob/master/CHANGELOG.md) first.
- Searching for any related issues and avoid creating duplicated issues.
- Adding details, diagnoses, screenshots or any type of useful information in existing issues, even if they are marked as closed. The team will still review it.
- Trying out the examples [provided in this repository](https://github.com/slackhq/python-rtmbot/tree/master/examples).
- Taking the time to think of a solution and [**open a pull request**](#new-pull-requests) for either improving the documentation, fixing a bug or suggesting a feature.
- Finally, **[open an issue]**(https://github.com/slackhq/python-rtmbot/issues/new) to report a bug, ask for help or suggest a feature. The more information you give, the better people can help you.
## New Pull Requests
We love pull requests and we are generally very receptive to contributions. Things to keep in mind:
- [Fork the repository](https://github.com/slackhq/python-rtmbot) and make sure to work on a branch up to date with origin master.
- Do your thing!
- Be mindful about doing atomic commits, adding documentation to your changes, not refactoring too much.
- Add tests covering the new code or functionality you are adding.
- Add a descriptive title and add any useful information for the reviewer. If your contribution is a user facing thing, please attach a screenshot and/or screencast (gif preferrably).
- Read and agree to our [Contributor License Agreement (CLA)](https://docs.google.com/a/slack-corp.com/forms/d/1q_w8rlJG_x_xJOoSUMNl7R35rkpA7N6pUkKhfHHMD9c/viewform). _We cannot accept your PR without your agreement to our CLA_.
- Create your pull request (yay!). If it is in relation to an existing issue, please mention it on the title or description.
[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z)

29
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,29 @@
* [ ] I've read and understood the [Contributing guidelines](./CONTRIBUTING.md) and have done my best effort to follow them.
* [ ] I've read and agree to the [Code of Conduct](./CODE_OF_CONDUCT.md).
* [ ] I've searched for any related issues and avoided creating a duplicate issue.
#### Description
> e.g. Description of the bug or feature
#### Reproducible in:
* [ ] This is reproducible in the sample project.
RTMBot version:
Python version:
OS Version:
#### Steps to reproduce:
1.
2.
3.
### Expected result:
> e.g. What you expected to happen
### Actual result:
> e.g. What actually happened
### Attachments:
> e.g. Logs, screenshots, screencast, sample project, funny gif, etc.

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,15 @@
* [ ] I've read and understood the [Contributing guidelines](./CONTRIBUTING.md) and have done my best effort to follow them.
* [ ] I've read and agree to the [Code of Conduct](./CODE_OF_CONDUCT.md).
* [ ] I've been mindful about doing atomic commits, adding documentation to my changes, not refactoring too much.
* [ ] I've a descriptive title and added any useful information for the reviewer. Where appropriate, I've attached a screenshot and/or screencast (gif preferably).
* [ ] I've written tests to cover the new code and functionality included in this PR.
* [ ] I've read, agree to, and signed the [Contributor License Agreement (CLA)](https://docs.google.com/a/slack-corp.com/forms/d/1q_w8rlJG_x_xJOoSUMNl7R35rkpA7N6pUkKhfHHMD9c/viewform).
#### PR Summary
> e.g. New functionality for producing whatsits.
#### Related Issues
> e.g. Fixes #206 and closes #230
#### Test strategy
> e.g. Add tests around whatsit production.

BIN
.github/contributing_header_slack.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

9
CHANGELOG.md Normal file
View file

@ -0,0 +1,9 @@
### v0.2.0 (2016-03-25)
* Fixed unicode handling to make things work in python 2.7 and 3.5.
* Changed versioning scheme to Semver.
### v0.10 (2016-02-28)
* Refactored the RTMBot Class to make it more isolated from the script that runs the bot

34
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,34 @@
# Slack open source code of conduct
## Introduction
Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand.
Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic.
This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members.
## Expected Behavior
* Be welcoming.
* Be kind.
* Look out for each other.
## Unacceptable Behavior
* Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature.
* Unwelcome, suggestive, derogatory or inappropriate nicknames or terms.
* Disrespect towards others. (Jokes, innuendo, dismissive attitudes.)
* Intimidation or harassment (online or in-person). Please read the [Citizen Code of Conduct](http://citizencodeofconduct.org/) for how we interpret harassment.
* Disrespect towards differences of opinion.
* Inappropriate attention or contact. Be aware of how your actions affect others. If it makes someone uncomfortable, stop.
* Not understanding the differences between constructive criticism and disparagement.
* Sustained disruptions.
* Violence, threats of violence or violent language.
## Enforcement
Understand that speech and actions have consequences, and unacceptable behavior will not be tolerated.
If you are the subject of, or witness to any violations of this Code of Conduct, please contact us by submitting a form [here](https://docs.google.com/a/slack-corp.com/forms/d/1NVqj2S2Q49XVIOT5N3L6Tx1oihvk9CpMa_UX8T_6ESo/viewform), or email conduct@slack.com.
If violations occur, organizers will take any action they deem appropriate for the infraction, up to and including expulsion.
_Thanks to the [Django Code of Conduct](https://www.djangoproject.com/conduct/), [The Citizen Code of Conduct](http://citizencodeofconduct.org/), [The Rust Code of Conduct](https://www.rust-lang.org/conduct.html) and [The Ada Initiative](http://adainitiative.org/2014/02/18/howto-design-a-code-of-conduct-for-your-community/)._

View file

@ -1,3 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015-2016 Slack Technologies, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights

View file

@ -39,7 +39,7 @@ Installation
3. Configure rtmbot (https://api.slack.com/bot-users)
cp doc/example-config/rtmbot.conf .
cp docs/example-config/rtmbot.conf .
vi rtmbot.conf
SLACK_TOKEN: "xoxb-11111111111-222222222222222"
@ -53,7 +53,7 @@ Plugins can be installed as .py files in the ```plugins/``` directory OR as a .p
To install the example 'repeat' plugin
mkdir plugins/repeat
cp doc/example-plugins/repeat.py plugins/repeat
cp docs/example-plugins/repeat.py plugins/repeat/
The repeat plugin will now be loaded by the bot on startup.
@ -72,6 +72,8 @@ This will print the incoming message json (dict) to the screen where the bot is
Plugins having a method defined as ```catch_all(data)``` will receive ALL events from the websocket. This is useful for learning the names of events and debugging.
Note: If you're using Python 2.x, the incoming data should be a unicode string, be careful you don't coerce it into a normal str object as it will cause errors on output. You can add `from __future__ import unicode_literals` to your plugin file to avoid this.
####Outgoing data
Plugins can send messages back to any channel, including direct messages. This is done by appending a two item array to the outputs global array. The first item in the array is the channel ID and the second is the message text. Example that writes "hello world" when the plugin is started:
@ -92,5 +94,18 @@ Plugins can also run methods on a schedule. This allows a plugin to poll for upd
####Plugin misc
The data within a plugin persists for the life of the rtmbot process. If you need persistent data, you should use something like sqlite or the python pickle libraries.
####Direct API Calls
You can directly call the Slack web API in your plugins by including the following import:
from client import slack_client
You can also rename the client on import so it can be easily referenced like shown below:
from client import slack_client as sc
Direct API calls can be called in your plugins in the following form:
sc.api_call("API.method", "parameters")
####Todo:
Some rtm data should be handled upstream, such as channel and user creation. These should create the proper objects on-the-fly.

10
client.py Normal file
View file

@ -0,0 +1,10 @@
from rtmbot import RtmBot
slack_client = None
def init(config):
global slack_client
bot = RtmBot(config)
slack_client = bot.slack_client
return bot

View file

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# don't convert to ascii in py2.7 when creating string to return
import time
outputs = []

View file

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# don't convert to ascii in py2.7 when creating string to return
import time
crontable = []
outputs = []

View file

@ -0,0 +1,11 @@
from __future__ import unicode_literals
from client import slack_client as sc
def process_message(data):
'''If a user passes 'print users' in a message, print the users in the slack
team to the console. (Don't run this in production probably)'''
if 'print users' in data['text']:
for user in sc.api_call("users.list")["members"]:
print(user["name"], user["id"])

View file

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# don't convert to ascii in py2.7 when creating string to return
crontable = []
outputs = []

View file

@ -1,4 +1,7 @@
from __future__ import print_function
from __future__ import unicode_literals
# don't convert to ascii in py2.7 when creating string to return
import os
import pickle

View file

@ -1,6 +1,7 @@
coveralls==1.1
ipdb==0.9.3
ipython==4.1.2
mock==2.0.0
pdbpp==0.8.3
pytest>=2.8.2
pytest-cov==2.2.1

View file

@ -1,11 +1,13 @@
#!/usr/bin/env python
import sys
from argparse import ArgumentParser
import sys
import os
import yaml
from rtmbot import RtmBot
import client
import service
sys.path.append(os.getcwd())
def parse_args():
parser = ArgumentParser()
@ -23,7 +25,7 @@ config = None
with open(args.config or 'rtmbot.conf', 'r') as f:
config = yaml.load(f)
service.slack = service.SlackService(config.get('SLACK_TOKEN'))
bot = RtmBot(config)
bot = client.init(config)
try:
bot.start()
except KeyboardInterrupt:

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python
from __future__ import unicode_literals
import sys
import glob
import os
@ -30,10 +31,10 @@ class RtmBot(object):
self.token = config.get('SLACK_TOKEN')
# set working directory for loading plugins or other files
working_directory = os.path.dirname(sys.argv[0])
working_directory = os.path.abspath(os.path.dirname(sys.argv[0]))
self.directory = self.config.get('BASE_PATH', working_directory)
if not self.directory.startswith('/'):
path = '{}/{}'.format(os.getcwd(), self.directory)
path = os.path.join(os.getcwd(), self.directory)
self.directory = os.path.abspath(path)
# establish logging
@ -47,7 +48,7 @@ class RtmBot(object):
# initialize stateful fields
self.last_ping = 0
self.bot_plugins = []
self.slack_client = None
self.slack_client = SlackClient(self.token)
def _dbg(self, debug_string):
if self.debug:
@ -55,7 +56,6 @@ class RtmBot(object):
def connect(self):
"""Convenience method that creates Server instance"""
self.slack_client = SlackClient(self.token)
self.slack_client.rtm_connect()
def _start(self):
@ -110,13 +110,14 @@ class RtmBot(object):
plugin.do_jobs()
def load_plugins(self):
for plugin in glob.glob(self.directory + '/plugins/*'):
plugin_dir = os.path.join(self.directory, 'plugins')
for plugin in glob.glob(os.path.join(plugin_dir, '*')):
sys.path.insert(0, plugin)
sys.path.insert(0, self.directory + '/plugins/')
for plugin in glob.glob(self.directory + '/plugins/*.py') + \
glob.glob(self.directory + '/plugins/*/*.py'):
sys.path.insert(0, plugin_dir)
for plugin in glob.glob(os.path.join(plugin_dir, '*.py')) + \
glob.glob(os.path.join(plugin_dir, '*', '*.py')):
logging.info(plugin)
name = plugin.split('/')[-1][:-3]
name = plugin.split(os.sep)[-1][:-3]
if name in self.config:
logging.info("config found for: " + name)
plugin_config = self.config.get(name, {})

View file

@ -4,7 +4,7 @@ from distutils.core import setup
setup(
name='rtmbot',
version='0.10',
version='0.2.0',
description='A Slack bot written in python that connects via the RTM API.',
author='Ryan Huber',
author_email='rhuber@gmail.com',

View file

@ -1,2 +0,0 @@
def test_example():
assert True

View file

@ -1,15 +1,26 @@
# -*- coding: utf-8 -*-
try:
from unittest.mock import Mock, create_autospec
except ImportError:
from mock import Mock, create_autospec
from testfixtures import LogCapture
from rtmbot.core import RtmBot
from slackclient import SlackClient, _channel, _server, _util
from rtmbot.core import RtmBot, Plugin
def test_init():
with LogCapture() as l:
def init_rtmbot():
''' Initializes an instance of RTMBot with some default values '''
rtmbot = RtmBot({
'SLACK_TOKEN': 'test-12345',
'BASE_PATH': '/tmp/',
'LOGFILE': '/tmp/rtmbot.log',
'DEBUG': True
})
return rtmbot
def test_init():
with LogCapture() as l:
rtmbot = init_rtmbot()
assert rtmbot.token == 'test-12345'
assert rtmbot.directory == '/tmp/'
@ -18,3 +29,47 @@ def test_init():
l.check(
('root', 'INFO', 'Initialized in: /tmp/')
)
def test_output():
''' Test that sending a message behaves as expected '''
rtmbot = init_rtmbot()
# Mock the slack_client object with Server, Channel objects and needed methods
slackclient_mock = create_autospec(SlackClient)
server_mock = create_autospec(_server.Server)
# Mock Server with channels method and correct return value
slackclient_mock.server = server_mock
searchlist_mock = create_autospec(_util.SearchList)
server_mock.channels = searchlist_mock
channel_mock = create_autospec(_channel.Channel)
slackclient_mock.server.channels.find.return_value = channel_mock
rtmbot.slack_client = slackclient_mock
# mock the plugin object to return a sample response
plugin_mock = create_autospec(Plugin)
plugin_mock.do_output.return_value = [['C12345678', 'test message']]
rtmbot.bot_plugins.append(plugin_mock)
rtmbot.output()
# test that the output matches the expected value
channel_mock.send_message.assert_called_with('test message')
# test that emoji messages work as expected
channel_mock.reset_mock()
plugin_mock.reset_mock()
plugin_mock.do_output.return_value = [['C12345678', '🚀 testing']]
rtmbot.output()
channel_mock.send_message.assert_called_with('🚀 testing')
# test that unicode messages work as expected
channel_mock.reset_mock()
plugin_mock.reset_mock()
plugin_mock.do_output.return_value = [['C12345678', 'ù hœø3ö']]
rtmbot.output()
channel_mock.send_message.assert_called_with('ù hœø3ö')

View file

@ -30,4 +30,4 @@ commands=
{toxinidir}/rtmbot.py \
{toxinidir}/rtmbot/core.py \
{toxinidir}/setup.py \
{toxinidir}/doc/example-plugins
{toxinidir}/docs/example-plugins