Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
04fed7e
Fix an issue where multiple handlers weren't registering correctly fo…
blampe Jan 27, 2017
6627ad5
Fix typo
blampe Feb 1, 2017
10ee275
Simplify by using DEBUG to bypass the cache
blampe Feb 3, 2017
0c21121
ignore messages with ignore strings anywhere in message, not just the…
Mar 9, 2017
8837abc
Adding APIv4 Support and a few other features
Jan 6, 2018
6134cb1
Merge pull request #1 from attzonko/working_apiv4
attzonko Jan 6, 2018
e4e8f72
Merge pull request #2 from wtodom/ignore-anywhere
attzonko Jan 9, 2018
f76c96e
Merge pull request #3 from attzonko/working_apiv4
attzonko Jan 11, 2018
a8d6c52
Merge pull request #4 from blampe/multiple-plugins
attzonko Jan 11, 2018
34d3345
Merge pull request #5 from attzonko/working_apiv4
attzonko Jan 11, 2018
61e697d
Update README.md
attzonko Jan 11, 2018
cb4b3d6
Minor fixes for APIv4 usage with multiple teams
Jan 11, 2018
1f23a8f
Merge pull request #6 from attzonko/working_apiv4
attzonko Jan 11, 2018
ae709d6
Refactoring how user information is aquired and removing dead code
Jan 31, 2018
f004283
fixed the issue which only open (to join) teams gets loaded at login
seLain Feb 8, 2018
2e4322d
Update codacy link
attzonko Feb 9, 2018
c498428
add tests
seLain Feb 24, 2018
693e097
fix codacy reported issues
seLain Feb 24, 2018
4578694
fix codacy reported issues
seLain Feb 24, 2018
b3edfb6
remvoe trailing whitespace
seLain Feb 24, 2018
d8454da
updated to support APIv4 only
seLain Mar 1, 2018
c1b9533
update README api v3 -> v4
seLain Mar 1, 2018
cedb273
fix: forget to remove redundant v4 from bot.py
seLain Mar 2, 2018
3fde7ae
deal with redirected login request
seLain Mar 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist
build
*.egg-info*
.build
.pytest_cache
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![PyPI](https://badge.fury.io/py/mattermost_bot.svg)](https://pypi.python.org/pypi/mattermost_bot)
[![Codacy](https://api.codacy.com/project/badge/grade/b06f3af1d8a04c6faa9a76a4ae3cb483)](https://www.codacy.com/app/gotlium/mattermost_bot)
[![Codacy](https://api.codacy.com/project/badge/grade/b06f3af1d8a04c6faa9a76a4ae3cb483)](https://www.codacy.com/app/attzonko/mattermost_bot)
[![Code Health](https://landscape.io/github/LPgenerator/mattermost_bot/master/landscape.svg?style=flat)](https://landscape.io/github/LPgenerator/mattermost_bot/master)
[![Python Support](https://img.shields.io/badge/python-2.7,3.5-blue.svg)](https://pypi.python.org/pypi/mattermost_bot/)
[![Mattermost](https://img.shields.io/badge/mattermost-1.4+-blue.svg)](http://www.mattermost.org)
Expand All @@ -14,7 +14,7 @@ A chat bot for [Mattermost](http://www.mattermost.org).

## Features

* Based on Mattermost [WebSocket API](https://api.mattermost.com)
* Based on Mattermost [WebSocket API](https://api.mattermost.com) - Updated to work with APIv4.0
* Simple plugins mechanism
* Messages can be handled concurrently
* Automatically reconnect to Mattermost when connection is lost
Expand Down Expand Up @@ -53,7 +53,7 @@ mattermost_bot_settings.py:

```python
SSL_VERIFY = True # Whether to perform SSL cert verification
BOT_URL = 'http://<mm.example.com>/api/v3' # with 'http://' and with '/api/v3' path. without trailing slash. '/api/v1' - for version < 3.0
BOT_URL = 'http://<mm.example.com>/api/v4' # with 'http://' and with '/api/v4' path. without trailing slash. '/api/v1' - for version < 3.0
BOT_LOGIN = '<bot-email-address>'
BOT_PASSWORD = '<bot-password>'
BOT_TEAM = '<your-team>' # possible in lowercase
Expand Down Expand Up @@ -193,3 +193,56 @@ PLUGINS = [
If you are migrating from `Slack` to the `Mattermost`, and previously you are used `SlackBot`,
you can use this battery without any problem. On most cases your plugins will be working properly
if you are used standard API or with minimal modifications.

## Run the tests

You will need a Mattermost server to run test cases.

* Create two user accounts for bots to login, ex. `driverbot` and `testbot`
* Create a team, ex. `test-team`, and add `driverbot` and `testbot` into the team
* Make sure the default public channel `off-topic` exists
* Create a private channel (ex. `test`) in team `test-team`, and add `driverbot` and `testbot` into the private channel

Install `PyTest` in development environment.

```
pip install -U pytest
```

There are two test categories in `mattermost_bot\tests`: __unit_tests__ and __behavior_tests__. The __behavior_tests__ is done by interactions between a __DriverBot__ and a __TestBot__.

To run the __behavior_tests__, you have to configure `behavior_tests\bot_settings.py` and `behavior_tests\driver_settings.py`. Example configuration:

__driver_settings.py__:
```python
PLUGINS = [
]

BOT_URL = 'http://mymattermost.server/api/v4'
BOT_LOGIN = 'driverbot@mymail'
BOT_NAME = 'driverbot'
BOT_PASSWORD = 'password'
BOT_TEAM = 'test-team' # this team name should be the same as in bot_settings
BOT_CHANNEL = 'off-topic' # default public channel name
BOT_PRIVATE_CHANNEL = 'test' # a private channel in BOT_TEAM
SSL_VERIFY = True
```

__bot_settings.py__:
```python
PLUGINS = [
]

BOT_URL = 'http://mymattermost.server/api/v4'
BOT_LOGIN = 'testbot@mymail'
BOT_NAME = 'testbot'
BOT_PASSWORD = 'password'
BOT_TEAM = 'test-team' # this team name should be the same as in driver_settings
BOT_CHANNEL = 'off-topic' # default public channel name
BOT_PRIVATE_CHANNEL = 'test' # a private channel in BOT_TEAM
SSL_VERIFY = True
```

Please notice that `BOT_URL`, `BOT_TEAM`, `BOT_CHANNEL`, and `BOT_PRIVATE_CHANNEL` must be the same in both setting files.

After the settings files are done, switch to root dir of mattermost, and run `pytest` to execute test cases.
22 changes: 11 additions & 11 deletions mattermost_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ def __init__(self):
self._client = MattermostClient(
settings.BOT_URL, settings.BOT_TEAM,
settings.BOT_LOGIN, settings.BOT_PASSWORD,
settings.SSL_VERIFY
)
settings.SSL_VERIFY)
logger.info('connected to mattermost')
self._plugins = PluginsManager()
self._dispatcher = MessageDispatcher(self._client, self._plugins)
Expand All @@ -49,16 +48,17 @@ class PluginsManager(object):
'listen_to': {}
}

def __init__(self):
pass
def __init__(self, plugins=[]):
self.plugins = plugins

def init_plugins(self):
if hasattr(settings, 'PLUGINS'):
plugins = settings.PLUGINS
else:
plugins = 'mattermost_bot.plugins'
if self.plugins == []:
if hasattr(settings, 'PLUGINS'):
self.plugins = settings.PLUGINS
else:
self.plugins = 'mattermost_bot.plugins'

for plugin in plugins:
for plugin in self.plugins:
self._load_plugins(plugin)

@staticmethod
Expand Down Expand Up @@ -92,7 +92,7 @@ def get_plugins(self, category, text):

def respond_to(regexp, flags=0):
def wrapper(func):
PluginsManager.commands['respond_to'][re.compile(regexp, flags)] = func
PluginsManager.commands['respond_to'][re.compile(regexp, flags | re.DEBUG)] = func
logger.info(
'registered respond_to plugin "%s" to "%s"', func.__name__, regexp)
return func
Expand All @@ -102,7 +102,7 @@ def wrapper(func):

def listen_to(regexp, flags=0):
def wrapper(func):
PluginsManager.commands['listen_to'][re.compile(regexp, flags)] = func
PluginsManager.commands['listen_to'][re.compile(regexp, flags | re.DEBUG)] = func
logger.info(
'registered listen_to plugin "%s" to "%s"', func.__name__, regexp)
return func
Expand Down
68 changes: 40 additions & 28 deletions mattermost_bot/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,27 @@ def get_message(msg):

def ignore(self, _msg):
msg = self.get_message(_msg)
for prefix in settings.IGNORE_NOTIFIES:
if msg.startswith(prefix):
return True
if any(item in msg for item in settings.IGNORE_NOTIFIES):
return True

def is_mentioned(self, msg):
mentions = msg.get('data', {}).get('mentions', [])
return self._client.user['id'] in mentions

def is_personal(self, msg):
channel_id = msg['data']['post']['channel_id']
if channel_id in self._channel_info:
channel_type = self._channel_info[channel_id]
else:
channel = self._client.api.channel(channel_id)
channel_type = channel['channel']['type']
self._channel_info[channel_id] = channel_type
return channel_type == 'D'
try:
channel_id = msg['data']['post']['channel_id']
if channel_id in self._channel_info:
channel_type = self._channel_info[channel_id]
else:
channel = self._client.api.channel(channel_id)
channel_type = channel['channel']['type']
self._channel_info[channel_id] = channel_type
return channel_type == 'D'
except KeyError as err:
logger.info('Once time workpool exception caused by \
bot [added to/leave] [team/channel].')
return False

def dispatch_msg(self, msg):
category = msg[0]
Expand Down Expand Up @@ -111,24 +115,38 @@ def load_json(self):
self.event['data']['mentions'])

def loop(self):
for self.event in self._client.messages(True, 'posted'):
self.load_json()
self._on_new_message(self.event)
for self.event in self._client.messages(True,
['posted', 'added_to_team', 'leave_team', \
'user_added', 'user_removed']):
if self.event:
self.load_json()
self._on_new_message(self.event)

def _default_reply(self, msg):
if settings.DEFAULT_REPLY:
return self._client.channel_msg(
msg['data']['post']['channel_id'], settings.DEFAULT_REPLY)

default_reply = [
u'Bad command "%s", You can ask me one of the '
u'following questions:\n' % self.get_message(msg),
u'Bad command "%s", Here is what I currently know '
u'how to do:\n' % self.get_message(msg),
]
docs_fmt = u'{1}' if settings.PLUGINS_ONLY_DOC_STRING else u'`{0}` {1}'

default_reply += [
docs_fmt.format(p.pattern, v.__doc__ or "")
for p, v in iteritems(self._plugins.commands['respond_to'])]
# create dictionary organizing commands by plugin
modules = {}
for p, v in iteritems(self._plugins.commands['respond_to']):
key = v.__module__.title().split('.')[1]
if not key in modules:
modules[key] = []
modules[key].append((p.pattern,v.__doc__))

docs_fmt = u'\t{1}' if settings.PLUGINS_ONLY_DOC_STRING else u'\t`{0}` - {1}'

for module,commands in modules.items():
default_reply += [u'Plugin: **{}**'.format(module)]
commands.sort(key=lambda x: x[0])
for pattern,description in commands:
default_reply += [docs_fmt.format(pattern,description)]

self._client.channel_msg(
msg['data']['post']['channel_id'], '\n'.join(default_reply))
Expand All @@ -147,15 +165,9 @@ def __init__(self, client, body, pool):
self._pool = pool

def get_user_info(self, key, user_id=None):
if key == 'username':
sender_name = self._get_sender_name()
if sender_name:
return sender_name

user_id = user_id or self._body['data']['post']['user_id']
if not Message.users or user_id not in Message.users:
Message.users = self._client.get_users()
return Message.users[user_id].get(key)
user_info = self._client.api.get_user_info(user_id)
return user_info[key]

def get_username(self, user_id=None):
return self.get_user_info('username', user_id)
Expand Down
Loading