Skip to content

Commit

Permalink
Add documentation, fix linting errors, make ready for first PyPI release
Browse files Browse the repository at this point in the history
  • Loading branch information
DonDebonair committed Aug 29, 2017
1 parent 89c0e37 commit 7b21341
Show file tree
Hide file tree
Showing 23 changed files with 241 additions and 37 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Daan Debie

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
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include README.rst LICENSE requirements.txt
include extra/logo.png
include run_dev.py
exclude local_settings.py
72 changes: 72 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
Slack Machine
=============

Slack Machine is a sexy, simple, yet powerful and extendable Slack bot. More than just a bot,
Slack Machine is a framework that helps you develop your Slack team into a ChatOps powerhouse.

.. image:: extra/logo.png

Features
--------

- Get started with mininal configuration
- Built on top of the `Slack RTM API`_ for smooth, real-time interactions
- Support for rich interactions using the `Slack Web API`_
- High-level API for maximum convenience when building plugins
- Low-level API for maximum flexibility
- Plugin API features:
- Listen and respond to any regular expression
- Capture parts of messages to use as variables in your functions
- Respond to messages in channels, groups and direct message conversations
- Respond with Emoji
- Respond in threads
- Send DMs to any user
- Support for `message attachments`_
- Listen and respond to any `Slack event`_ supported by the RTM API

.. _Slack RTM API: https://api.slack.com/rtm
.. _Slack Web API: https://api.slack.com/web
.. _message attachments: https://api.slack.com/docs/message-attachments
.. _Slack event: https://api.slack.com/events

Coming Soon
"""""""""""

- Schedule actions and messages
- Plugin-accesible storage
- Help texts for Plugins
- ... and much more

Installation
------------

You can install Slack Machine using pip:

.. code-block:: bash
$ pip install slack-machine
It is **strongly recommended** that you install ``slack-machine`` inside a `virtual environment`_!

.. _virtual environment: http://docs.python-guide.org/en/latest/dev/virtualenvs/

Usage
-----

#. Create a directory for your Slack bot: ``mkdir my-slack-bot && cd my-slack-bot``
#. Add a ``local_settings.py`` file to your bot directory: ``touch local_settings.py``
#. Create a Bot User for your Slack team: https://my.slack.com/services/new/bot (take note of your API token)
#. Add the Slack API token to your ``local_settings.py`` like this:

.. code-block:: python
SLACK_API_TOKEN = 'xox-my-slack-token'
#. Start the bot with ``slack-machine``
#. ...
#. Profit!

Writing plugins
---------------

*Coming Soon!*
Binary file added extra/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions machine/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
__all__ = [
'__title__', '__description__', '__uri__', '__version__', '__author__',
'__email__', '__license__', '__copyright__'
]

__title__ = "slack-machine"
__description__ = "A sexy, simple, yet powerful and extendable Slack bot"
__uri__ = "https://github.com/DandyDev/slack-machine"

__version__ = "0.1"

__author__ = "Daan Debie"
__email__ = "[email protected]"

__license__ = "MIT"
__copyright__ = "Copyright 2017 {}".format(__author__)
9 changes: 8 additions & 1 deletion machine/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
from .core import *
from .core import Machine
from .__about__ import (__title__, __description__, __uri__, __version__, __author__,
__email__, __license__, __copyright__)

__all__ = [
'__title__', '__description__', '__uri__', '__version__', '__author__', '__email__',
'__license__', '__copyright__', 'Machine'
]
Empty file added machine/bin/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions machine/bin/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import sys
import os

from machine import Machine


def main():
# When running this function as console entry point, the current working dir is not in the
# Python path, so we have to add it
sys.path.insert(0, os.getcwd())
bot = Machine()
try:
bot.run()
except (KeyboardInterrupt, SystemExit):
print("Thanks for playing!")
sys.exit(0)
3 changes: 1 addition & 2 deletions machine/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
class MessagingClient:

def __init__(self, slack_client):
self._slack_client = slack_client

Expand Down Expand Up @@ -61,4 +60,4 @@ def send_dm_webapi(self, user, text, attachments=None):
text=text,
attachments=attachments,
as_user=True
)
)
12 changes: 5 additions & 7 deletions machine/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

logger = logging.getLogger(__name__)


class Machine:
def __init__(self):
self._settings, found_local_settings = import_settings()
fmt = '[%(asctime)s][%(levelname)s] %(name)s %(filename)s:%(funcName)s:%(lineno)d | %(message)s'
fmt = '[%(asctime)s][%(levelname)s] %(name)s %(filename)s:%(funcName)s:%(lineno)d |' \
' %(message)s'
date_fmt = '%Y-%m-%d %H:%M:%S'
log_level = self._settings.get('LOGLEVEL', logging.ERROR)
logging.basicConfig(
Expand All @@ -23,7 +25,7 @@ def __init__(self):
)
if not found_local_settings:
logger.warning("No local_settings found! Are you sure this is what you want?")
if not 'SLACK_API_TOKEN' in self._settings:
if 'SLACK_API_TOKEN' not in self._settings:
logger.error("No SLACK_API_TOKEN found in settings! I need that to work...")
sys.exit(1)
self._client = SlackClient(self._settings['SLACK_API_TOKEN'])
Expand Down Expand Up @@ -79,8 +81,4 @@ def _register_plugin_actions(self, plugin, metadata, cls_instance, fn_name, fn):

def run(self):
self._client.rtm_connect()
try:
self._dispatcher.start()
except (KeyboardInterrupt, SystemExit):
print("Thanks for playing!")

self._dispatcher.start()
3 changes: 1 addition & 2 deletions machine/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

logger = logging.getLogger(__name__)

class EventDispatcher:

class EventDispatcher:
RESPOND_MATCHER = re.compile(r'^(?:\<@(?P<atuser>\w+)\>:?|(?P<username>\w+):) ?(?P<text>.*)$')

def __init__(self, client, plugin_actions):
Expand Down Expand Up @@ -41,7 +41,6 @@ def handle_event(self, event):
listeners = self._find_listeners('listen_to')
self._dispatch_listeners(listeners, event)


def _find_listeners(self, type):
return [action for action in self._plugin_actions[type].values()]

Expand Down
7 changes: 3 additions & 4 deletions machine/plugins/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
class MachineBasePlugin:

def __init__(self, settings, client):
self._client = client
self.settings = settings
Expand Down Expand Up @@ -27,8 +26,8 @@ def send_dm(self, user, text):
def send_dm_webapi(self, user, text, attachments=None):
self._client.send_dm_webapi(user, text, attachments)

class Message:

class Message:
def __init__(self, client, msg_event):
self._client = client
self._msg_event = msg_event
Expand Down Expand Up @@ -99,5 +98,5 @@ def thread_ts(self):
return thread_ts

def __str__(self):
return "Message '{}', sent by user @{} in channel #{}".format(self.text, self.sender.name, self.channel.name)

return "Message '{}', sent by user @{} in channel #{}".format(
self.text, self.sender.name, self.channel.name)
4 changes: 3 additions & 1 deletion machine/plugins/builtin/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

logger = logging.getLogger(__name__)


class EventLoggerPlugin(MachineBasePlugin):

def catch_all(self, event):
logger.debug("Event received: %s", event)


class EchoPlugin(MachineBasePlugin):

@process(event_type='message')
def echo_message(self, event):
logger.debug("Message received: %s", event)
self.send(event['channel'], event['text'])
self.send(event['channel'], event['text'])
4 changes: 2 additions & 2 deletions machine/plugins/builtin/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

logger = logging.getLogger(__name__)

class PingPongPlugin(MachineBasePlugin):

class PingPongPlugin(MachineBasePlugin):
@listen_to(r'^ping$')
def listen_to_ping(self, msg):
logger.debug("Ping received with msg: %s", msg)
Expand All @@ -16,8 +16,8 @@ def listen_to_pong(self, msg):
logger.debug("Pong received with msg: %s", msg)
msg.send("ping")

class HelloPlugin(MachineBasePlugin):

class HelloPlugin(MachineBasePlugin):
@respond_to(r'^(?P<greeting>hi|hello)')
def greet(self, msg, greeting):
logger.debug("Greeting '%s' received", greeting)
Expand Down
9 changes: 9 additions & 0 deletions machine/plugins/decorators.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
from functools import wraps
import re


def process(event_type):
def process_decorator(f):
@wraps(f)
def wrapped_f(*args, **kwargs):
return f(*args, **kwargs)

wrapped_f.metadata = getattr(f, "metadata", {})
wrapped_f.metadata['plugin_actions'] = wrapped_f.metadata.get('plugin_actions', {})
wrapped_f.metadata['plugin_actions']['process'] = {}
wrapped_f.metadata['plugin_actions']['process']['event_type'] = event_type
return wrapped_f

return process_decorator


def listen_to(regex, flags=re.IGNORECASE):
def listen_to_decorator(f):
@wraps(f)
def wrapped_f(*args, **kwargs):
return f(*args, **kwargs)

wrapped_f.metadata = getattr(f, "metadata", {})
wrapped_f.metadata['plugin_actions'] = wrapped_f.metadata.get('plugin_actions', {})
wrapped_f.metadata['plugin_actions']['listen_to'] = {}
wrapped_f.metadata['plugin_actions']['listen_to']['regex'] = re.compile(regex, flags)
return wrapped_f

return listen_to_decorator


def respond_to(regex, flags=re.IGNORECASE):
def respond_to_decorator(f):
@wraps(f)
def wrapped_f(*args, **kwargs):
return f(*args, **kwargs)

wrapped_f.metadata = getattr(f, "metadata", {})
wrapped_f.metadata['plugin_actions'] = wrapped_f.metadata.get('plugin_actions', {})
wrapped_f.metadata['plugin_actions']['respond_to'] = {}
wrapped_f.metadata['plugin_actions']['respond_to']['regex'] = re.compile(regex, flags)
return wrapped_f

return respond_to_decorator
7 changes: 4 additions & 3 deletions machine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

logger = logging.getLogger(__name__)


def import_settings():
default_settings = {
'PLUGINS': ['machine.plugins.builtin.general.PingPongPlugin',
Expand All @@ -13,16 +14,16 @@ def import_settings():
try:
import local_settings
found_local_settings = True
except ImportError:
except ImportError as e:
found_local_settings = False
else:
for k in dir(local_settings):
if not k.startswith('_'):
settings[k] = getattr(local_settings, k)

for k,v in os.environ.items():
for k, v in os.environ.items():
if k[:8] == 'MACHINE_':
k = k[8:]
settings[k] = v

return (settings, found_local_settings)
return (settings, found_local_settings)
4 changes: 2 additions & 2 deletions machine/utils/module_loading.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from importlib import import_module
import inspect


def import_string(dotted_path):
"""
Import all Classes from the module specified by
Expand All @@ -13,12 +14,11 @@ def import_string(dotted_path):
try:
module = import_module(dotted_path)
return inspect.getmembers(module, predicate=inspect.isclass)
except ModuleNotFoundError:
except ImportError:
try:
module_path, class_name = dotted_path.rsplit('.', 1)
module = import_module(module_path)
return [(class_name, getattr(module, class_name))]
except AttributeError:
msg = "{} doesn't look like a module or class".format(dotted_path)
raise ImportError(msg)

Loading

0 comments on commit 7b21341

Please sign in to comment.