From 04fed7e9a90cf59a940b4191a63c9526e32457f0 Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Fri, 27 Jan 2017 07:55:29 -0800 Subject: [PATCH 01/18] Fix an issue where multiple handlers weren't registering correctly for one regex --- mattermost_bot/bot.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mattermost_bot/bot.py b/mattermost_bot/bot.py index 26b7d69..d97669c 100644 --- a/mattermost_bot/bot.py +++ b/mattermost_bot/bot.py @@ -80,8 +80,8 @@ def _load_plugins(plugin): def get_plugins(self, category, text): has_matching_plugin = False - for matcher in self.commands[category]: - m = matcher.search(text) + for matcher in self.commands[category] + m = matcher.r.search(text) if m: has_matching_plugin = True yield self.commands[category][matcher], m.groups() @@ -90,9 +90,14 @@ def get_plugins(self, category, text): yield None, None +class Matcher(object): + """This allows us to map the same regex to multiple handlers.""" + def __init__(self, regex): + self.r = regex + def respond_to(regexp, flags=0): def wrapper(func): - PluginsManager.commands['respond_to'][re.compile(regexp, flags)] = func + PluginsManager.commands['respond_to'][Matcher(re.compile(regexp, flags))] = func logger.info( 'registered respond_to plugin "%s" to "%s"', func.__name__, regexp) return func @@ -102,7 +107,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'][Matcher(re.compile(regexp, flags))] = func logger.info( 'registered listen_to plugin "%s" to "%s"', func.__name__, regexp) return func From 6627ad588917992d45298cfeeefcdeca21fb93c1 Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Wed, 1 Feb 2017 13:22:52 -0800 Subject: [PATCH 02/18] Fix typo --- mattermost_bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mattermost_bot/bot.py b/mattermost_bot/bot.py index d97669c..5a1e829 100644 --- a/mattermost_bot/bot.py +++ b/mattermost_bot/bot.py @@ -80,7 +80,7 @@ def _load_plugins(plugin): def get_plugins(self, category, text): has_matching_plugin = False - for matcher in self.commands[category] + for matcher in self.commands[category]: m = matcher.r.search(text) if m: has_matching_plugin = True From 10ee27554df12712b6f70501db0d9c5afeb6022d Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Thu, 2 Feb 2017 20:29:16 -0800 Subject: [PATCH 03/18] Simplify by using DEBUG to bypass the cache --- mattermost_bot/bot.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mattermost_bot/bot.py b/mattermost_bot/bot.py index 5a1e829..d8ba7ce 100644 --- a/mattermost_bot/bot.py +++ b/mattermost_bot/bot.py @@ -81,7 +81,7 @@ def _load_plugins(plugin): def get_plugins(self, category, text): has_matching_plugin = False for matcher in self.commands[category]: - m = matcher.r.search(text) + m = matcher.search(text) if m: has_matching_plugin = True yield self.commands[category][matcher], m.groups() @@ -90,14 +90,9 @@ def get_plugins(self, category, text): yield None, None -class Matcher(object): - """This allows us to map the same regex to multiple handlers.""" - def __init__(self, regex): - self.r = regex - def respond_to(regexp, flags=0): def wrapper(func): - PluginsManager.commands['respond_to'][Matcher(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 @@ -107,7 +102,7 @@ def wrapper(func): def listen_to(regexp, flags=0): def wrapper(func): - PluginsManager.commands['listen_to'][Matcher(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 From 0c21121c490ceb7f897affb618b088c5fb8f45fd Mon Sep 17 00:00:00 2001 From: Weston Odom Date: Wed, 8 Mar 2017 17:48:28 -0800 Subject: [PATCH 04/18] ignore messages with ignore strings anywhere in message, not just the beginning --- mattermost_bot/dispatcher.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mattermost_bot/dispatcher.py b/mattermost_bot/dispatcher.py index 8e44e89..4f91aea 100644 --- a/mattermost_bot/dispatcher.py +++ b/mattermost_bot/dispatcher.py @@ -37,9 +37,8 @@ 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', []) From 8837abc493eb1e712ded56bec27b3de9df69e98f Mon Sep 17 00:00:00 2001 From: Alex Tzonkov Date: Fri, 5 Jan 2018 18:52:57 -0800 Subject: [PATCH 05/18] Adding APIv4 Support and a few other features - Adding APIv4 Support - Enabling local settings - Allow posts from multiple teams - Added missing default plugins --- mattermost_bot/bot.py | 31 +++++---- mattermost_bot/dispatcher.py | 56 ++++++++++----- mattermost_bot/mattermost.py | 96 +++++++++++++++++--------- mattermost_bot/mattermost_v4.py | 116 ++++++++++++++++++++++++++++++++ mattermost_bot/plugins/help.py | 9 +++ mattermost_bot/plugins/info.py | 15 +++++ mattermost_bot/plugins/sleep.py | 15 +++++ mattermost_bot/settings.py | 6 +- 8 files changed, 279 insertions(+), 65 deletions(-) create mode 100644 mattermost_bot/mattermost_v4.py create mode 100644 mattermost_bot/plugins/help.py create mode 100644 mattermost_bot/plugins/info.py create mode 100644 mattermost_bot/plugins/sleep.py diff --git a/mattermost_bot/bot.py b/mattermost_bot/bot.py index 26b7d69..908c1cb 100644 --- a/mattermost_bot/bot.py +++ b/mattermost_bot/bot.py @@ -15,17 +15,23 @@ from mattermost_bot import settings from mattermost_bot.dispatcher import MessageDispatcher from mattermost_bot.mattermost import MattermostClient +from mattermost_bot.mattermost_v4 import MattermostClientv4 logger = logging.getLogger(__name__) class Bot(object): def __init__(self): - self._client = MattermostClient( - settings.BOT_URL, settings.BOT_TEAM, - settings.BOT_LOGIN, settings.BOT_PASSWORD, - settings.SSL_VERIFY - ) + if settings.MATTERMOST_API_VERSION == 4: + self._client = MattermostClientv4( + settings.BOT_URL, settings.BOT_TEAM, + settings.BOT_LOGIN, settings.BOT_PASSWORD, + settings.SSL_VERIFY) + else: + self._client = MattermostClient( + settings.BOT_URL, settings.BOT_TEAM, + settings.BOT_LOGIN, settings.BOT_PASSWORD, + settings.SSL_VERIFY) logger.info('connected to mattermost') self._plugins = PluginsManager() self._dispatcher = MessageDispatcher(self._client, self._plugins) @@ -49,16 +55,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 diff --git a/mattermost_bot/dispatcher.py b/mattermost_bot/dispatcher.py index 8e44e89..25789cd 100644 --- a/mattermost_bot/dispatcher.py +++ b/mattermost_bot/dispatcher.py @@ -46,14 +46,19 @@ def is_mentioned(self, msg): 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] @@ -111,9 +116,12 @@ 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: @@ -121,14 +129,25 @@ def _default_reply(self, 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)) @@ -147,6 +166,7 @@ def __init__(self, client, body, pool): self._pool = pool def get_user_info(self, key, user_id=None): + channel_id = self._body['data']['post']['channel_id'] if key == 'username': sender_name = self._get_sender_name() if sender_name: @@ -154,7 +174,7 @@ def get_user_info(self, key, user_id=None): 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() + Message.users = self._client.get_users(channel_id) return Message.users[user_id].get(key) def get_username(self, user_id=None): diff --git a/mattermost_bot/mattermost.py b/mattermost_bot/mattermost.py index a177daa..008a543 100644 --- a/mattermost_bot/mattermost.py +++ b/mattermost_bot/mattermost.py @@ -15,7 +15,8 @@ def __init__(self, url, ssl_verify): self.url = url self.token = "" self.initial = None - self.team_id = None + self.default_team_id = None # the first team in API returned value + self.teams_channels_ids = None # struct: {team_id:[channel_id,...], ...} self.ssl_verify = ssl_verify if not ssl_verify: requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) @@ -54,27 +55,35 @@ def login(self, name, email, password): def load_initial_data(self): self.initial = self.get('/users/initial_load') - self.team_id = self.initial['teams'][0]['id'] + self.default_team_id = self.initial['teams'][0]['id'] + self.teams_channels_ids = {} + for team in self.initial['teams']: + self.teams_channels_ids[team['id']] = [] + # get all channels belonging to each team + for channel in self.get_channels(team['id']): + self.teams_channels_ids[team['id']].append(channel['id']) def create_post(self, user_id, channel_id, message, files=None, pid=""): create_at = int(time.time() * 1000) + team_id = self.get_team_id(channel_id) return self.post( - '/teams/%s/channels/%s/posts/create' % (self.team_id, channel_id), + '/teams/%s/channels/%s/posts/create' % (team_id, channel_id), { - 'user_id': user_id, - 'channel_id': channel_id, - 'message': message, - 'create_at': create_at, - 'filenames': files or [], - 'pending_post_id': user_id + ':' + str(create_at), - 'state': "loading", - 'parent_id': pid, - 'root_id': pid, - }) + 'user_id': user_id, + 'channel_id': channel_id, + 'message': message, + 'create_at': create_at, + 'filenames': files or [], + 'pending_post_id': user_id + ':' + str(create_at), + 'state': "loading", + 'parent_id': pid, + 'root_id': pid, + }) def update_post(self, message_id, user_id, channel_id, message, files=None, pid=""): + team_id = self.get_team_id(channel_id) return self.post( - '/teams/%s/channels/%s/posts/update' % (self.team_id, channel_id), + '/teams/%s/channels/%s/posts/update' % (team_id, channel_id), { 'id': message_id, 'channel_id': channel_id, @@ -82,24 +91,39 @@ def update_post(self, message_id, user_id, channel_id, message, files=None, pid= }) def channel(self, channel_id): - return self.get('/teams/%s/channels/%s/' % (self.team_id, channel_id)) + team_id = self.get_team_id(channel_id) + return self.get('/teams/%s/channels/%s/' % (team_id, channel_id)) + + def get_channels(self, team_id=None): + if team_id is None: + team_id = self.default_team_id + return self.get('/teams/%s/channels/' % team_id) - def get_channels(self): - return self.get('/teams/%s/channels/' % self.team_id) + def get_team_id(self, channel_id): + for team_id, channels in self.teams_channels_ids.items(): + if channel_id in channels: + return team_id + return None - def get_profiles(self, pagination_size=100): + def get_profiles(self,channel_id=None, pagination_size=100): profiles = {} + + if channel_id is not None: + team_id = self.get_team_id(channel_id) + else: + team_id = self.default_team_id + start = 0 end = start + pagination_size current_page = self.get('/teams/%s/users/0/%s' - % (self.team_id, pagination_size)) + % (team_id, pagination_size)) profiles.update(current_page) while len(current_page.keys()) == pagination_size: start = end end += pagination_size current_page = self.get('/teams/%s/users/%s/%s' - % (self.team_id, start, end)) + % (team_id, start, end)) profiles.update(current_page) return profiles @@ -110,11 +134,11 @@ def user(self, user_id): return self.get_profiles()[user_id] def hooks_list(self): - return self.get('/teams/%s/hooks/incoming/list' % self.team_id) + return self.get('/teams/%s/hooks/incoming/list' % self.default_team_id) def hooks_create(self, **kwargs): return self.post( - '/teams/%s/hooks/incoming/create' % self.team_id, kwargs) + '/teams/%s/hooks/incoming/create' % self.default_team_id, kwargs) @staticmethod def in_webhook(url, channel, text, username=None, as_user=None, @@ -163,15 +187,15 @@ def login(self, team, email, password): def channel_msg(self, channel, message, pid=""): c_id = self.channels.get(channel, {}).get("id") or channel - return self.api.create_post(self.user["id"], c_id, message, pid=pid) + return self.api.create_post(self.user["id"], c_id, "{}".format(message), pid=pid) def update_msg(self, message_id, channel, message, pid=""): c_id = self.channels.get(channel, {}).get("id") or channel return self.api.update_post(message_id, self.user["id"], c_id, message, pid=pid) - def get_users(self): - return self.api.get_profiles() + def get_users(self, channel_id): + return self.api.get_profiles(channel_id) def connect_websocket(self): host = self.api.url.replace('http', 'ws').replace('https', 'wss') @@ -188,7 +212,7 @@ def _connect_websocket(self, url, cookie_name): else ssl.CERT_NONE }) - def messages(self, ignore_own_msg=False, filter_action=None): + def messages(self, ignore_own_msg=False, filter_actions=[]): if not self.connect_websocket(): return while True: @@ -201,17 +225,23 @@ def messages(self, ignore_own_msg=False, filter_action=None): if data: try: post = json.loads(data) - if filter_action and post.get('event') != filter_action: + event_action = post.get('event') + if event_action not in filter_actions: continue - if post.get('data', {}).get('post'): - dp = json.loads(post['data']['post']) - if ignore_own_msg is True and dp.get("user_id"): - if self.user["id"] == dp["user_id"]: - continue - yield post + if event_action == 'posted': + if post.get('data', {}).get('post'): + dp = json.loads(post['data']['post']) + if ignore_own_msg is True and dp.get("user_id"): + if self.user["id"] == dp["user_id"]: + continue + yield post + elif event_action in ['added_to_team', 'leave_team', + 'user_added', 'user_removed']: + self.api.load_initial_data() # reload teams & channels except ValueError: pass def ping(self): self.websocket.ping() + diff --git a/mattermost_bot/mattermost_v4.py b/mattermost_bot/mattermost_v4.py new file mode 100644 index 0000000..ea14814 --- /dev/null +++ b/mattermost_bot/mattermost_v4.py @@ -0,0 +1,116 @@ +import json +import logging +import ssl +import time + +import requests +import websocket +import websocket._exceptions + +from mattermost_bot.mattermost import MattermostClient, MattermostAPI + +logger = logging.getLogger(__name__) + +class MattermostAPIv4(MattermostAPI): + + def login(self, team, account, password): + props = {'login_id': account, 'password': password} + response =requests.post( + self.url + '/users/login', + data = json.dumps(props), + verify=self.ssl_verify) + if response.status_code == 200: + self.token = response.headers["Token"] + self.load_initial_data() + self.user = json.loads(response.text) + return self.user + else: + response.raise_for_status() + + def load_initial_data(self): + self.teams = self.get('/teams') + self.default_team_id = self.teams[0]['id'] + self.teams_channels_ids = {} + for team in self.teams: + self.teams_channels_ids[team['id']] = [] + # get all channels belonging to each team + for channel in self.get_channels(team['id']): + self.teams_channels_ids[team['id']].append(channel['id']) + + def create_post(self, user_id, channel_id, message, files=None, pid=""): + create_at = int(time.time() * 1000) + return self.post( + '/posts', + { + 'channel_id': channel_id, + 'message': message, + 'filenames': files or [], + 'root_id': pid, + }) + def update_post(self, message_id, user_id, channel_id, message, files=None, pid=""): + return self.post( + '/posts/%s' % message_id, + { + 'message': message, + }) + + def channel(self, channel_id): + channel = {'channel': self.get('/channels/%s' % channel_id)} + return channel + + def get_channels(self, team_id=None): + if team_id is None: + team_id = self.default_team_id + return self.get('/users/me/teams/%s/channels' % team_id) + + def create_user_dict(self, v4_dict): + new_dict = {} + new_dict[v4_dict['id']]=v4_dict + return new_dict + + def get_profiles(self,channel_id=None, pagination_size=100): + profiles = {} + if channel_id is not None: + team_id = self.get_team_id(channel_id) + else: + team_id = self.default_team_id + + start = 0 + end = start + pagination_size + + current_page = self.get('/users?page=0&per_page={}&in_team={}'.format(pagination_size, team_id)) + for user in current_page: + profiles.update(self.create_user_dict(user)) + + while len(current_page) == pagination_size: + start = end + end += pagination_size + current_page = self.get('/users?page={}&per_page={}&in_team={}'.format(start, pagination_size, team_id)) + for user in current_page: + profiles.update(self.create_user_dict(user)) + return profiles + + +class MattermostClientv4(MattermostClient): + + def __init__(self, url, team, email, password, ssl_verify=True, login=1): + self.users = {} + self.channels = {} + self.mentions = {} + self.api = MattermostAPIv4(url, ssl_verify) + self.user = None + self.info = None + self.websocket = None + self.email = None + self.team = team + self.email = email + self.password = password + + if login: + self.login(team, email, password) + + def connect_websocket(self): + host = self.api.url.replace('http', 'ws').replace('https', 'wss') + url = host + '/websocket' + self._connect_websocket(url, cookie_name='MMAUTHTOKEN') + return self.websocket.getstatus() == 101 diff --git a/mattermost_bot/plugins/help.py b/mattermost_bot/plugins/help.py new file mode 100644 index 0000000..b9347a9 --- /dev/null +++ b/mattermost_bot/plugins/help.py @@ -0,0 +1,9 @@ +# -*- encoding: utf-8 -*- + +from mattermost_bot.bot import respond_to, listen_to + + +@respond_to('^\!help$') +@listen_to('^\!help$') +def help_request(message): + message.send(message.docs_reply()) diff --git a/mattermost_bot/plugins/info.py b/mattermost_bot/plugins/info.py new file mode 100644 index 0000000..a879ef2 --- /dev/null +++ b/mattermost_bot/plugins/info.py @@ -0,0 +1,15 @@ +# -*- encoding: utf-8 -*- + +from mattermost_bot.bot import respond_to, listen_to + + +@respond_to('^\!info$') +@listen_to('^\!info$') +def info_request(message): + message.send('TEAM-ID: `%s`' % message.get_team_id()) + message.send('USERNAME: `%s`' % message.get_username()) + message.send('EMAIL: `%s`' % message.get_user_mail()) + message.send('USER-ID: `%s`' % message.get_user_id()) + message.send('IS-DIRECT: %s' % repr(message.is_direct_message())) + message.send('MENTIONS: %s' % repr(message.get_mentions())) + message.send('MESSAGE: %s' % message.get_message()) diff --git a/mattermost_bot/plugins/sleep.py b/mattermost_bot/plugins/sleep.py new file mode 100644 index 0000000..915ef9a --- /dev/null +++ b/mattermost_bot/plugins/sleep.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +import time + +from mattermost_bot.bot import respond_to + + +@respond_to('sleep (.*)') +def sleep_reply(message, sec): + message.reply('Ok, I will be waiting %s sec' % sec) + time.sleep(int(sec)) + message.reply('done') + + +sleep_reply.__doc__ = "Sleep time" diff --git a/mattermost_bot/settings.py b/mattermost_bot/settings.py index 57eb224..d8c023b 100644 --- a/mattermost_bot/settings.py +++ b/mattermost_bot/settings.py @@ -9,7 +9,9 @@ ] PLUGINS_ONLY_DOC_STRING = False -BOT_URL = 'http://mm.example.com/api/v3' +# Default settings +MATTERMOST_API_VERSION = 4 +BOT_URL = 'http://mm.example.com/api/v4' BOT_LOGIN = 'bot@example.com' BOT_PASSWORD = None BOT_TEAM = 'devops' @@ -35,7 +37,7 @@ if key[:15] == 'MATTERMOST_BOT_': globals()[key[11:]] = os.environ[key] -settings_module = os.environ.get('MATTERMOST_BOT_SETTINGS_MODULE') +settings_module = os.environ['MATTERMOST_BOT_SETTINGS_MODULE'] if settings_module is not None: pwd = os.getcwd() From 61e697dcfb22769e2fabaafeecc92a92a3d444fa Mon Sep 17 00:00:00 2001 From: Alex Tzonkov Date: Wed, 10 Jan 2018 18:14:03 -0800 Subject: [PATCH 06/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dafacf6..81c899e 100644 --- a/README.md +++ b/README.md @@ -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 From cb4b3d68105f427676313943e4a25260c481e630 Mon Sep 17 00:00:00 2001 From: Alex Tzonkov Date: Thu, 11 Jan 2018 10:49:42 -0800 Subject: [PATCH 07/18] Minor fixes for APIv4 usage with multiple teams --- mattermost_bot/mattermost_v4.py | 5 ++--- mattermost_bot/settings.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mattermost_bot/mattermost_v4.py b/mattermost_bot/mattermost_v4.py index ea14814..bb36f95 100644 --- a/mattermost_bot/mattermost_v4.py +++ b/mattermost_bot/mattermost_v4.py @@ -8,6 +8,7 @@ import websocket._exceptions from mattermost_bot.mattermost import MattermostClient, MattermostAPI +from pprint import pprint logger = logging.getLogger(__name__) @@ -76,15 +77,13 @@ def get_profiles(self,channel_id=None, pagination_size=100): team_id = self.default_team_id start = 0 - end = start + pagination_size current_page = self.get('/users?page=0&per_page={}&in_team={}'.format(pagination_size, team_id)) for user in current_page: profiles.update(self.create_user_dict(user)) while len(current_page) == pagination_size: - start = end - end += pagination_size + start = start + 1 current_page = self.get('/users?page={}&per_page={}&in_team={}'.format(start, pagination_size, team_id)) for user in current_page: profiles.update(self.create_user_dict(user)) diff --git a/mattermost_bot/settings.py b/mattermost_bot/settings.py index d8c023b..2eee87d 100644 --- a/mattermost_bot/settings.py +++ b/mattermost_bot/settings.py @@ -17,7 +17,7 @@ BOT_TEAM = 'devops' SSL_VERIFY = True -IGNORE_NOTIFIES = ['@channel', '@all'] +IGNORE_NOTIFIES = ['@here', '@channel', '@all'] WORKERS_NUM = 10 DEFAULT_REPLY_MODULE = None From ae709d688c5d4b1921582188ac79fc724b9cccc8 Mon Sep 17 00:00:00 2001 From: Alex Tzonkov Date: Wed, 31 Jan 2018 10:41:30 -0800 Subject: [PATCH 08/18] Refactoring how user information is aquired and removing dead code --- mattermost_bot/dispatcher.py | 11 ++--------- mattermost_bot/mattermost.py | 29 ++++------------------------- mattermost_bot/mattermost_v4.py | 23 +++-------------------- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/mattermost_bot/dispatcher.py b/mattermost_bot/dispatcher.py index 5f527fc..a24f09b 100644 --- a/mattermost_bot/dispatcher.py +++ b/mattermost_bot/dispatcher.py @@ -165,16 +165,9 @@ def __init__(self, client, body, pool): self._pool = pool def get_user_info(self, key, user_id=None): - channel_id = self._body['data']['post']['channel_id'] - 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(channel_id) - 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) diff --git a/mattermost_bot/mattermost.py b/mattermost_bot/mattermost.py index 008a543..9c6941b 100644 --- a/mattermost_bot/mattermost.py +++ b/mattermost_bot/mattermost.py @@ -105,33 +105,15 @@ def get_team_id(self, channel_id): return team_id return None - def get_profiles(self,channel_id=None, pagination_size=100): - profiles = {} - - if channel_id is not None: - team_id = self.get_team_id(channel_id) - else: - team_id = self.default_team_id - - start = 0 - end = start + pagination_size - - current_page = self.get('/teams/%s/users/0/%s' - % (team_id, pagination_size)) - profiles.update(current_page) - while len(current_page.keys()) == pagination_size: - start = end - end += pagination_size - current_page = self.get('/teams/%s/users/%s/%s' - % (team_id, start, end)) - profiles.update(current_page) - return profiles + def get_user_info(self, user_id): + user_info = self.post('/users/ids',[user_id]) + return user_info[user_id] def me(self): return self.get('/users/me') def user(self, user_id): - return self.get_profiles()[user_id] + return self.get_user_info(user_id) def hooks_list(self): return self.get('/teams/%s/hooks/incoming/list' % self.default_team_id) @@ -194,9 +176,6 @@ def update_msg(self, message_id, channel, message, pid=""): return self.api.update_post(message_id, self.user["id"], c_id, message, pid=pid) - def get_users(self, channel_id): - return self.api.get_profiles(channel_id) - def connect_websocket(self): host = self.api.url.replace('http', 'ws').replace('https', 'wss') url = host + '/users/websocket' diff --git a/mattermost_bot/mattermost_v4.py b/mattermost_bot/mattermost_v4.py index bb36f95..c78d31d 100644 --- a/mattermost_bot/mattermost_v4.py +++ b/mattermost_bot/mattermost_v4.py @@ -48,6 +48,7 @@ def create_post(self, user_id, channel_id, message, files=None, pid=""): 'filenames': files or [], 'root_id': pid, }) + def update_post(self, message_id, user_id, channel_id, message, files=None, pid=""): return self.post( '/posts/%s' % message_id, @@ -69,26 +70,8 @@ def create_user_dict(self, v4_dict): new_dict[v4_dict['id']]=v4_dict return new_dict - def get_profiles(self,channel_id=None, pagination_size=100): - profiles = {} - if channel_id is not None: - team_id = self.get_team_id(channel_id) - else: - team_id = self.default_team_id - - start = 0 - - current_page = self.get('/users?page=0&per_page={}&in_team={}'.format(pagination_size, team_id)) - for user in current_page: - profiles.update(self.create_user_dict(user)) - - while len(current_page) == pagination_size: - start = start + 1 - current_page = self.get('/users?page={}&per_page={}&in_team={}'.format(start, pagination_size, team_id)) - for user in current_page: - profiles.update(self.create_user_dict(user)) - return profiles - + def get_user_info(self, user_id): + return self.get('/users/{}'.format(user_id)) class MattermostClientv4(MattermostClient): From f004283de50628676ba8175459747b7489faf6b4 Mon Sep 17 00:00:00 2001 From: seLain Date: Thu, 8 Feb 2018 19:03:19 +0800 Subject: [PATCH 09/18] fixed the issue which only open (to join) teams gets loaded at login this was caused by misuse of APIv4, the '/teams' endpoint returns only open teams. we need to use '/users/{user_id}/teams' to get all teams referred discussion: https://forum.mattermost.org/t/solved-mattermost-v3-to-v4-upgrade-team-api-v4/4156/6 --- mattermost_bot/mattermost_v4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mattermost_bot/mattermost_v4.py b/mattermost_bot/mattermost_v4.py index c78d31d..2af5e95 100644 --- a/mattermost_bot/mattermost_v4.py +++ b/mattermost_bot/mattermost_v4.py @@ -29,7 +29,7 @@ def login(self, team, account, password): response.raise_for_status() def load_initial_data(self): - self.teams = self.get('/teams') + self.teams = self.get('/users/me/teams') self.default_team_id = self.teams[0]['id'] self.teams_channels_ids = {} for team in self.teams: From 2e4322dbed7c99da7eeca4bb123296ca4c92c5b9 Mon Sep 17 00:00:00 2001 From: Alex Tzonkov Date: Fri, 9 Feb 2018 11:25:50 -0800 Subject: [PATCH 10/18] Update codacy link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81c899e..bf09152 100644 --- a/README.md +++ b/README.md @@ -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) From c498428d709fb7a562c230b8b4f62b349ccdfbe8 Mon Sep 17 00:00:00 2001 From: seLain Date: Sat, 24 Feb 2018 08:46:05 +0800 Subject: [PATCH 11/18] add tests --- .gitignore | 1 + README.md | 53 +++++ tests/__init__.py | 0 tests/behavior_tests/bot_settings.py | 11 + tests/behavior_tests/driver.py | 194 ++++++++++++++++++ tests/behavior_tests/driver_settings.py | 11 + tests/behavior_tests/run_bot.py | 38 ++++ tests/behavior_tests/test_behaviors.py | 89 ++++++++ tests/unit_tests/local_plugins/__init__.py | 0 tests/unit_tests/local_plugins/busy.py | 14 ++ tests/unit_tests/local_plugins/hello.py | 58 ++++++ tests/unit_tests/single_plugin/__init__.py | 0 tests/unit_tests/single_plugin/mock_plugin.py | 9 + tests/unit_tests/test_pluginmanager.py | 41 ++++ 14 files changed, 519 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/behavior_tests/bot_settings.py create mode 100644 tests/behavior_tests/driver.py create mode 100644 tests/behavior_tests/driver_settings.py create mode 100644 tests/behavior_tests/run_bot.py create mode 100644 tests/behavior_tests/test_behaviors.py create mode 100644 tests/unit_tests/local_plugins/__init__.py create mode 100644 tests/unit_tests/local_plugins/busy.py create mode 100644 tests/unit_tests/local_plugins/hello.py create mode 100644 tests/unit_tests/single_plugin/__init__.py create mode 100644 tests/unit_tests/single_plugin/mock_plugin.py create mode 100644 tests/unit_tests/test_pluginmanager.py diff --git a/.gitignore b/.gitignore index 6dd7dcd..3929762 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist build *.egg-info* .build +.pytest_cache \ No newline at end of file diff --git a/README.md b/README.md index bf09152..c13d48a 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/behavior_tests/bot_settings.py b/tests/behavior_tests/bot_settings.py new file mode 100644 index 0000000..65ebc4a --- /dev/null +++ b/tests/behavior_tests/bot_settings.py @@ -0,0 +1,11 @@ +PLUGINS = [ +] + +BOT_URL = 'http://SERVER_HOST_DN/api/v4' +BOT_LOGIN = 'testbot@mail' +BOT_NAME = 'testbot_name' +BOT_PASSWORD = 'testbot_password' +BOT_TEAM = 'default_team_name' # 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 \ No newline at end of file diff --git a/tests/behavior_tests/driver.py b/tests/behavior_tests/driver.py new file mode 100644 index 0000000..ca4488d --- /dev/null +++ b/tests/behavior_tests/driver.py @@ -0,0 +1,194 @@ +import time, re, six, threading, logging, sys, json +from six.moves import _thread +from mattermost_bot.bot import Bot, PluginsManager +from mattermost_bot.mattermost_v4 import MattermostClientv4 +from mattermost_bot.dispatcher import MessageDispatcher +import driver_settings, bot_settings + +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +class DriverBot(Bot): + + def __init__(self): + self._client = MattermostClientv4( + driver_settings.BOT_URL, driver_settings.BOT_TEAM, + driver_settings.BOT_LOGIN, driver_settings.BOT_PASSWORD, + driver_settings.SSL_VERIFY + ) + self._plugins = PluginsManager() + self._plugins.init_plugins() + self._dispatcher = MessageDispatcher(self._client, self._plugins) + + +class Driver(object): + + def __init__(self): + self.bot = DriverBot() + self.bot_username = driver_settings.BOT_NAME + self.bot_userid = None + self.testbot_username = bot_settings.BOT_NAME + self.testbot_userid = None + self.dm_chan = None # direct message channel + self.team_name = driver_settings.BOT_TEAM + self.cm_name = driver_settings.BOT_CHANNEL + self.cm_chan = None # common public channel + self.gm_name = driver_settings.BOT_PRIVATE_CHANNEL + self.gm_chan = None # private channel + self.events = [] + self._events_lock = threading.Lock() + + def start(self): + self._rtm_connect() + self._retrieve_bot_user_ids() + self._create_dm_channel() + self._retrieve_cm_channel() + self._retrieve_gm_channel() + + def _rtm_connect(self): + self.bot._client.connect_websocket() + self._websocket = self.bot._client.websocket + self._websocket.sock.setblocking(0) + _thread.start_new_thread(self._rtm_read_forever, tuple()) + + def _websocket_safe_read(self): + """Returns data if available, otherwise ''. Newlines indicate multiple messages """ + data = '' + while True: + # accumulated received data until no more events received + # then exception triggered, then returns the accumulated data + try: + data += '{0}\n'.format(self._websocket.recv()) + except: + return data.rstrip() + + def _rtm_read_forever(self): + while True: + json_data = self._websocket_safe_read() + if json_data != '': + with self._events_lock: + self.events.extend([json.loads(d) for d in json_data.split('\n')]) + time.sleep(1) + + def _retrieve_bot_user_ids(self): + # get bot user info + self.users_info = self.bot._client.api.post('/users/usernames', + [driver_settings.BOT_NAME, bot_settings.BOT_NAME]) + # get user ids + for user in self.users_info: + if user['username'] == self.bot_username: + self.bot_userid = user['id'] + elif user['username'] == self.testbot_username: + self.testbot_userid = user['id'] + + def _create_dm_channel(self): + """create direct channel and get id""" + response = self.bot._client.api.post('/channels/direct', + [self.bot_userid, self.testbot_userid]) + self.dm_chan = response['id'] + + def _retrieve_cm_channel(self): + """create direct channel and get id""" + response = self.bot._client.api.get('/teams/name/%s/channels/name/%s' % (self.team_name, self.cm_name)) + self.cm_chan = response['id'] + + def _retrieve_gm_channel(self): + """create direct channel and get id""" + response = self.bot._client.api.get('/teams/name/%s/channels/name/%s' % (self.team_name, self.gm_name)) + self.gm_chan = response['id'] + + def _format_message(self, msg, tobot=True, colon=True, space=True): + colon = ':' if colon else '' + space = ' ' if space else '' + if tobot: + msg = u'@{}{}{}{}'.format(self.testbot_username, colon, space, msg) + return msg + + def _send_message_to_bot(self, channel, msg): + self.clear_events() + self._start_ts = time.time() + self.bot._client.channel_msg(channel, msg) + + def send_direct_message(self, msg, tobot=False, colon=True): + msg = self._format_message(msg, tobot=tobot, colon=colon) + self._send_message_to_bot(self.dm_chan, msg) + + def validate_bot_direct_message(self, match): + posts = self.bot._client.api.get('/channels/%s/posts' % self.dm_chan) + last_response = posts['posts'][posts['order'][0]] + if re.search(match, last_response['message']): + return + else: + raise AssertionError('expected to get message like "{}", but got nothing'.format(match)) + + def wait_for_bot_direct_message(self, match): + self._wait_for_bot_message(self.dm_chan, match, tosender=False) + + def _wait_for_bot_message(self, channel, match, maxwait=10, tosender=True, thread=False): + for _ in range(maxwait): + time.sleep(1) + if self._has_got_message_rtm(channel, match, tosender, thread=thread): + break + else: + raise AssertionError('expected to get message like "{}", but got nothing'.format(match)) + + def _has_got_message_rtm(self, channel, match, tosender=True, thread=False): + if tosender is True: + match = six.text_type(r'@{}: {}').format(self.bot_username, match) + with self._events_lock: + for event in self.events: + if 'event' not in event or (event['event'] == 'posted' and 'data' not in event): + print('Unusual event received: ' + repr(event)) + if event['event'] == 'posted': + post_data = json.loads(event['data']['post']) + if re.match(match, post_data['message'], re.DOTALL): + return True + return False + + def _send_channel_message(self, chan, msg, **kwargs): + msg = self._format_message(msg, **kwargs) + self._send_message_to_bot(chan, msg) + + def send_channel_message(self, msg, **kwargs): + self._send_channel_message(self.cm_chan, msg, **kwargs) + + def validate_bot_channel_message(self, match): + posts = self.bot._client.api.get('/channels/%s/posts' % self.cm_chan) + last_response = posts['posts'][posts['order'][0]] + if re.search(match, last_response['message']): + return + else: + raise AssertionError('expected to get message like "{}", but got nothing'.format(match)) + + def wait_for_bot_channel_message(self, match, tosender=True): + self._wait_for_bot_message(self.cm_chan, match, tosender=tosender) + + def send_private_channel_message(self, msg, **kwargs): + self._send_channel_message(self.gm_chan, msg, **kwargs) + + def wait_for_bot_private_channel_message(self, match, tosender=True): + self._wait_for_bot_message(self.gm_chan, match, tosender=tosender) + + def wait_for_bot_online(self): + self._wait_for_bot_presense(True) + # sleep to allow bot connection to stabilize + time.sleep(2) + + def _wait_for_bot_presense(self, online): + for _ in range(10): + time.sleep(2) + if online and self._is_testbot_online(): + break + if not online and not self._is_testbot_online(): + break + else: + raise AssertionError('test bot is still {}'.format('offline' if online else 'online')) + + # [ToDo] implement this method by checking via MM API + def _is_testbot_online(self): + # check by asking MM through API + return True + + def clear_events(self): + with self._events_lock: + self.events = [] diff --git a/tests/behavior_tests/driver_settings.py b/tests/behavior_tests/driver_settings.py new file mode 100644 index 0000000..0c7189d --- /dev/null +++ b/tests/behavior_tests/driver_settings.py @@ -0,0 +1,11 @@ +PLUGINS = [ +] + +BOT_URL = 'http://SERVER_HOST_DN/api/v4' +BOT_LOGIN = 'driverbot@mail' +BOT_NAME = 'driverbot_name' +BOT_PASSWORD = 'driverbot_password' +BOT_TEAM = 'default_team_name' # 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 \ No newline at end of file diff --git a/tests/behavior_tests/run_bot.py b/tests/behavior_tests/run_bot.py new file mode 100644 index 0000000..a17d6f7 --- /dev/null +++ b/tests/behavior_tests/run_bot.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import sys +import logging +import logging.config + +from mattermost_bot.bot import Bot, PluginsManager +from mattermost_bot.mattermost_v4 import MattermostClientv4 +from mattermost_bot.dispatcher import MessageDispatcher +import bot_settings + +class LocalBot(Bot): + + def __init__(self): + self._client = MattermostClientv4( + bot_settings.BOT_URL, bot_settings.BOT_TEAM, + bot_settings.BOT_LOGIN, bot_settings.BOT_PASSWORD, + bot_settings.SSL_VERIFY + ) + self._plugins = PluginsManager() + self._plugins.init_plugins() + self._dispatcher = MessageDispatcher(self._client, self._plugins) + +def main(): + ''' + kw = { + 'format': '[%(asctime)s] %(message)s', + 'datefmt': '%m/%d/%Y %H:%M:%S', + 'level': logging.DEBUG if settings.DEBUG else logging.INFO, + 'stream': sys.stdout, + } + logging.basicConfig(**kw) + logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING) + ''' + bot = LocalBot() + bot.run() + +if __name__ == '__main__': + main() diff --git a/tests/behavior_tests/test_behaviors.py b/tests/behavior_tests/test_behaviors.py new file mode 100644 index 0000000..6d5bc19 --- /dev/null +++ b/tests/behavior_tests/test_behaviors.py @@ -0,0 +1,89 @@ +import os, subprocess, time +from os.path import basename +import pytest +from driver import Driver + +WAIT_SECS = 10 + +''' +Function to run a bot for testing in subprocess +''' +def _start_bot_process(): + args = ['python', 'tests/behavior_tests/run_bot.py',] + return subprocess.Popen(args) + +@pytest.fixture(scope='module') +def driver(): + driver = Driver() + driver.start() + p = _start_bot_process() + driver.wait_for_bot_online() + yield driver + p.terminate() + +''' +Empty test to ensure fixture start_test will be executed +''' +def test_bot_get_online(driver): + pass + +######################################################### +# Actual test cases bellow +######################################################### + +def test_bot_respond_to_simple_message(driver): + driver.send_direct_message('hello') + driver.wait_for_bot_direct_message('hello sender!') + +def test_bot_respond_to_simple_message_with_formatting(driver): + driver.send_direct_message('hello_formatting') + driver.wait_for_bot_direct_message('_hello_ sender!') + +def test_bot_respond_to_simple_message_case_insensitive(driver): + driver.send_direct_message('hEllO') + driver.wait_for_bot_direct_message('hello sender!') + +def test_bot_direct_message_with_at_prefix(driver): + driver.send_direct_message('hello', tobot=True) + driver.wait_for_bot_direct_message('hello sender!') + driver.send_direct_message('hello', tobot=True, colon=False) + driver.wait_for_bot_direct_message('hello sender!') + +# [ToDo] Implement this test together with the file upload function +def test_bot_upload_file(driver): + pass + +# [ToDo] Needs to find a better way in validating file upload by URL +def test_bot_upload_file_from_link(driver): + #url = 'http://www.mattermost.org/wp-content/uploads/2016/03/logoHorizontal_WS.png' + #fname = basename(url) + #driver.send_direct_message('upload %s' % url) + pass + +def test_bot_reply_to_channel_message(driver): + driver.send_channel_message('hello') + driver.wait_for_bot_channel_message('hello sender!') + driver.send_channel_message('hello', colon=False) + driver.wait_for_bot_channel_message('hello sender!') + driver.send_channel_message('hello', space=False) + driver.wait_for_bot_channel_message('hello sender!') + driver.send_channel_message('hello', colon=False, space=False) + driver.wait_for_bot_channel_message('hello channel!', tosender=False) + +def test_bot_listen_to_channel_message(driver): + driver.send_channel_message('hello', tobot=False) + driver.wait_for_bot_channel_message('hello channel!', tosender=False) + +def test_bot_reply_to_message_multiple_decorators(driver): + driver.send_channel_message('hello_decorators') + driver.wait_for_bot_channel_message('hello!', tosender=False) + driver.send_channel_message('hello_decorators', tobot=False) + driver.wait_for_bot_channel_message('hello!', tosender=False) + driver.send_direct_message('hello_decorators') + driver.wait_for_bot_direct_message('hello!') + +def test_bot_reply_to_private_channel_message(driver): + driver.send_private_channel_message('hello') + driver.wait_for_bot_private_channel_message('hello sender!') + driver.send_private_channel_message('hello', colon=False) + driver.wait_for_bot_private_channel_message('hello sender!') \ No newline at end of file diff --git a/tests/unit_tests/local_plugins/__init__.py b/tests/unit_tests/local_plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests/local_plugins/busy.py b/tests/unit_tests/local_plugins/busy.py new file mode 100644 index 0000000..8557d9e --- /dev/null +++ b/tests/unit_tests/local_plugins/busy.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +import re + +from mattermost_bot.bot import respond_to + + +@respond_to('^busy|jobs$', re.IGNORECASE) +def busy_reply(message): + busy = message.get_busy_workers() - 1 + message.reply('Num of busy workers is `%d`' % busy) + + +busy_reply.__doc__ = "Show num of busy workers" diff --git a/tests/unit_tests/local_plugins/hello.py b/tests/unit_tests/local_plugins/hello.py new file mode 100644 index 0000000..af8df77 --- /dev/null +++ b/tests/unit_tests/local_plugins/hello.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +import re + +from mattermost_bot.bot import listen_to +from mattermost_bot.bot import respond_to + + +@respond_to('hello$', re.IGNORECASE) +def hello_reply(message): + message.reply('hello sender!') + + +@respond_to('hello_formatting') +@listen_to('hello_formatting$') +def hello_reply_formatting(message): + # Format message with italic style + message.reply('_hello_ sender!') + + +@listen_to('hello$') +def hello_send(message): + message.send('hello channel!') + +@listen_to('hello$') +def hello_send_alternative(message): + message.send('hello channel!') + +@listen_to('hello_decorators') +@respond_to('hello_decorators') +def hello_decorators(message): + message.send('hello!') + + +@respond_to('hello_web_api', re.IGNORECASE) +def web_api_reply(message): + attachments = [{ + 'fallback': 'Fallback text', + 'author_name': 'Author', + 'author_link': 'http://www.github.com', + 'text': 'Some text here ...', + 'color': '#59afe1' + }] + message.reply_webapi( + 'Attachments example', attachments, + username='Mattermost-Bot', + icon_url='https://goo.gl/OF4DBq', + ) + + +@listen_to('hello_comment', re.IGNORECASE) +def hello_comment(message): + message.comment('some comments ...') + + +@listen_to('hello_react', re.IGNORECASE) +def hello_react(message): + message.react(':+1:') diff --git a/tests/unit_tests/single_plugin/__init__.py b/tests/unit_tests/single_plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests/single_plugin/mock_plugin.py b/tests/unit_tests/single_plugin/mock_plugin.py new file mode 100644 index 0000000..ab7f7e9 --- /dev/null +++ b/tests/unit_tests/single_plugin/mock_plugin.py @@ -0,0 +1,9 @@ +from mattermost_bot.utils import allow_only_direct_message +from mattermost_bot.utils import allowed_users +from mattermost_bot.bot import respond_to + +@respond_to('^admin$') +@allow_only_direct_message() +@allowed_users('admin', 'root') +def mock_users_access(message): + message.reply('Access allowed!') \ No newline at end of file diff --git a/tests/unit_tests/test_pluginmanager.py b/tests/unit_tests/test_pluginmanager.py new file mode 100644 index 0000000..e7f00cf --- /dev/null +++ b/tests/unit_tests/test_pluginmanager.py @@ -0,0 +1,41 @@ +import sys, logging +import pytest +from mattermost_bot.bot import PluginsManager + +#logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +logger = logging.getLogger(__name__) + +def test_load_single_plugin(): + assert 'single_plugin' not in sys.modules + PluginsManager()._load_plugins('single_plugin') + assert 'single_plugin' in sys.modules + assert 'single_plugin.mock_plugin' in sys.modules + +def test_load_init_plugins(): + PluginsManager().init_plugins() + assert 'mattermost_bot.plugins' in sys.modules + +def test_load_local_plugins(): + assert 'local_plugins' not in sys.modules + PluginsManager(plugins=['local_plugins']).init_plugins() + assert 'local_plugins' in sys.modules + assert 'local_plugins.hello' in sys.modules + assert 'local_plugins.busy' in sys.modules + +def test_get_plugins(): + manager = PluginsManager(plugins=['single_plugin', 'local_plugins']) + manager.init_plugins() + matched_func_names = set() + # test: has_matching_plugin + for func, args in manager.get_plugins('listen_to', 'hello'): + if func: + matched_func_names.add(func.__name__) + assert 'hello_send' in matched_func_names + assert 'hello_send_alternative' in matched_func_names + # test: not has_matching_plugin + matched_func_names = set() + for func, args in manager.get_plugins('listen_to', 'hallo'): + if func: + matched_func_names.add(func.__name__) + assert 'hello_send' not in matched_func_names + assert 'hello_send_alternative' not in matched_func_names From 693e09708c59a74c991e0ffd98d87598c43103c8 Mon Sep 17 00:00:00 2001 From: seLain Date: Sat, 24 Feb 2018 10:28:41 +0800 Subject: [PATCH 12/18] fix codacy reported issues --- tests/behavior_tests/driver.py | 31 ++++++------------- tests/behavior_tests/run_bot.py | 14 --------- tests/behavior_tests/test_behaviors.py | 11 +------ tests/unit_tests/test_pluginmanager.py | 43 +++++++++++++++++--------- 4 files changed, 38 insertions(+), 61 deletions(-) diff --git a/tests/behavior_tests/driver.py b/tests/behavior_tests/driver.py index ca4488d..07e9ab5 100644 --- a/tests/behavior_tests/driver.py +++ b/tests/behavior_tests/driver.py @@ -1,5 +1,6 @@ import time, re, six, threading, logging, sys, json from six.moves import _thread +from websocket._exceptions import WebSocketConnectionClosedException, WebSocketTimeoutException from mattermost_bot.bot import Bot, PluginsManager from mattermost_bot.mattermost_v4 import MattermostClientv4 from mattermost_bot.dispatcher import MessageDispatcher @@ -19,7 +20,6 @@ def __init__(self): self._plugins = PluginsManager() self._plugins.init_plugins() self._dispatcher = MessageDispatcher(self._client, self._plugins) - class Driver(object): @@ -59,7 +59,11 @@ def _websocket_safe_read(self): # then exception triggered, then returns the accumulated data try: data += '{0}\n'.format(self._websocket.recv()) - except: + except WebSocketConnectionClosedException: + return data.rstrip() + except WebSocketTimeoutException: + return data.rstrip() + except Exception: return data.rstrip() def _rtm_read_forever(self): @@ -72,7 +76,7 @@ def _rtm_read_forever(self): def _retrieve_bot_user_ids(self): # get bot user info - self.users_info = self.bot._client.api.post('/users/usernames', + self.users_info = self.bot._client.api.post('/users/usernames', \ [driver_settings.BOT_NAME, bot_settings.BOT_NAME]) # get user ids for user in self.users_info: @@ -83,7 +87,7 @@ def _retrieve_bot_user_ids(self): def _create_dm_channel(self): """create direct channel and get id""" - response = self.bot._client.api.post('/channels/direct', + response = self.bot._client.api.post('/channels/direct', \ [self.bot_userid, self.testbot_userid]) self.dm_chan = response['id'] @@ -170,24 +174,7 @@ def wait_for_bot_private_channel_message(self, match, tosender=True): self._wait_for_bot_message(self.gm_chan, match, tosender=tosender) def wait_for_bot_online(self): - self._wait_for_bot_presense(True) - # sleep to allow bot connection to stabilize - time.sleep(2) - - def _wait_for_bot_presense(self, online): - for _ in range(10): - time.sleep(2) - if online and self._is_testbot_online(): - break - if not online and not self._is_testbot_online(): - break - else: - raise AssertionError('test bot is still {}'.format('offline' if online else 'online')) - - # [ToDo] implement this method by checking via MM API - def _is_testbot_online(self): - # check by asking MM through API - return True + time.sleep(4) def clear_events(self): with self._events_lock: diff --git a/tests/behavior_tests/run_bot.py b/tests/behavior_tests/run_bot.py index a17d6f7..9ef7cb3 100644 --- a/tests/behavior_tests/run_bot.py +++ b/tests/behavior_tests/run_bot.py @@ -1,8 +1,4 @@ #!/usr/bin/env python -import sys -import logging -import logging.config - from mattermost_bot.bot import Bot, PluginsManager from mattermost_bot.mattermost_v4 import MattermostClientv4 from mattermost_bot.dispatcher import MessageDispatcher @@ -21,16 +17,6 @@ def __init__(self): self._dispatcher = MessageDispatcher(self._client, self._plugins) def main(): - ''' - kw = { - 'format': '[%(asctime)s] %(message)s', - 'datefmt': '%m/%d/%Y %H:%M:%S', - 'level': logging.DEBUG if settings.DEBUG else logging.INFO, - 'stream': sys.stdout, - } - logging.basicConfig(**kw) - logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING) - ''' bot = LocalBot() bot.run() diff --git a/tests/behavior_tests/test_behaviors.py b/tests/behavior_tests/test_behaviors.py index 6d5bc19..7b8bfd4 100644 --- a/tests/behavior_tests/test_behaviors.py +++ b/tests/behavior_tests/test_behaviors.py @@ -1,10 +1,7 @@ -import os, subprocess, time -from os.path import basename +import subprocess, time import pytest from driver import Driver -WAIT_SECS = 10 - ''' Function to run a bot for testing in subprocess ''' @@ -21,12 +18,6 @@ def driver(): yield driver p.terminate() -''' -Empty test to ensure fixture start_test will be executed -''' -def test_bot_get_online(driver): - pass - ######################################################### # Actual test cases bellow ######################################################### diff --git a/tests/unit_tests/test_pluginmanager.py b/tests/unit_tests/test_pluginmanager.py index e7f00cf..549cc4d 100644 --- a/tests/unit_tests/test_pluginmanager.py +++ b/tests/unit_tests/test_pluginmanager.py @@ -1,28 +1,36 @@ import sys, logging -import pytest +from importlib import reload from mattermost_bot.bot import PluginsManager #logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) logger = logging.getLogger(__name__) def test_load_single_plugin(): - assert 'single_plugin' not in sys.modules - PluginsManager()._load_plugins('single_plugin') - assert 'single_plugin' in sys.modules - assert 'single_plugin.mock_plugin' in sys.modules + reload(sys) + PluginsManager()._load_plugins('single_plugin') + if 'single_plugin' not in sys.modules: + raise AssertionError() + if 'single_plugin.mock_plugin' not in sys.modules: + raise AssertionError() def test_load_init_plugins(): + reload(sys) PluginsManager().init_plugins() - assert 'mattermost_bot.plugins' in sys.modules + if 'mattermost_bot.plugins' not in sys.modules: + raise AssertionError() def test_load_local_plugins(): - assert 'local_plugins' not in sys.modules + reload(sys) PluginsManager(plugins=['local_plugins']).init_plugins() - assert 'local_plugins' in sys.modules - assert 'local_plugins.hello' in sys.modules - assert 'local_plugins.busy' in sys.modules + if 'local_plugins' not in sys.modules: + raise AssertionError() + if 'local_plugins.hello' not in sys.modules: + raise AssertionError() + if 'local_plugins.busy' not in sys.modules: + raise AssertionError() def test_get_plugins(): + reload(sys) manager = PluginsManager(plugins=['single_plugin', 'local_plugins']) manager.init_plugins() matched_func_names = set() @@ -30,12 +38,17 @@ def test_get_plugins(): for func, args in manager.get_plugins('listen_to', 'hello'): if func: matched_func_names.add(func.__name__) - assert 'hello_send' in matched_func_names - assert 'hello_send_alternative' in matched_func_names - # test: not has_matching_plugin + if 'hello_send' not in matched_func_names: + raise AssertionError() + if 'hello_send_alternative' not in matched_func_names: + raise AssertionError() + # test: not has_matching_plugin (there is no such plugin `hallo`) + reload(sys) matched_func_names = set() for func, args in manager.get_plugins('listen_to', 'hallo'): if func: matched_func_names.add(func.__name__) - assert 'hello_send' not in matched_func_names - assert 'hello_send_alternative' not in matched_func_names + if 'hello_send' in matched_func_names: + raise AssertionError() + if 'hello_send_alternative' in matched_func_names: + raise AssertionError() From 457869467fab5b818c8ec4090e5e7246f475ceb8 Mon Sep 17 00:00:00 2001 From: seLain Date: Sat, 24 Feb 2018 10:44:02 +0800 Subject: [PATCH 13/18] fix codacy reported issues --- tests/behavior_tests/driver.py | 3 ++- tests/behavior_tests/test_behaviors.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/behavior_tests/driver.py b/tests/behavior_tests/driver.py index 07e9ab5..78ff48a 100644 --- a/tests/behavior_tests/driver.py +++ b/tests/behavior_tests/driver.py @@ -37,7 +37,7 @@ def __init__(self): self.gm_chan = None # private channel self.events = [] self._events_lock = threading.Lock() - + def start(self): self._rtm_connect() self._retrieve_bot_user_ids() @@ -173,6 +173,7 @@ def send_private_channel_message(self, msg, **kwargs): def wait_for_bot_private_channel_message(self, match, tosender=True): self._wait_for_bot_message(self.gm_chan, match, tosender=tosender) + @classmethod def wait_for_bot_online(self): time.sleep(4) diff --git a/tests/behavior_tests/test_behaviors.py b/tests/behavior_tests/test_behaviors.py index 7b8bfd4..c379494 100644 --- a/tests/behavior_tests/test_behaviors.py +++ b/tests/behavior_tests/test_behaviors.py @@ -2,10 +2,10 @@ import pytest from driver import Driver -''' -Function to run a bot for testing in subprocess -''' def _start_bot_process(): + """ + Function to run a bot for testing in subprocess + """ args = ['python', 'tests/behavior_tests/run_bot.py',] return subprocess.Popen(args) From b3edfb6bbb0eae362b3ca80fd857e0319f43ad41 Mon Sep 17 00:00:00 2001 From: seLain Date: Sat, 24 Feb 2018 10:54:12 +0800 Subject: [PATCH 14/18] remvoe trailing whitespace --- tests/behavior_tests/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/behavior_tests/driver.py b/tests/behavior_tests/driver.py index 78ff48a..204413c 100644 --- a/tests/behavior_tests/driver.py +++ b/tests/behavior_tests/driver.py @@ -44,7 +44,7 @@ def start(self): self._create_dm_channel() self._retrieve_cm_channel() self._retrieve_gm_channel() - + def _rtm_connect(self): self.bot._client.connect_websocket() self._websocket = self.bot._client.websocket From d8454da0570c966c414872af7bb6df6776c6b12a Mon Sep 17 00:00:00 2001 From: seLain Date: Thu, 1 Mar 2018 14:32:01 +0800 Subject: [PATCH 15/18] updated to support APIv4 only - revise test cases - merge mattermost_v4 to mattermost, remove all APIv3 endpoints - the existing webhook-related part was revised to APIv4 endpoints, but not fully tested yet. --- mattermost_bot/mattermost.py | 69 ++++++++++------------- mattermost_bot/mattermost_v4.py | 98 --------------------------------- mattermost_bot/settings.py | 2 +- tests/behavior_tests/driver.py | 6 +- tests/behavior_tests/run_bot.py | 4 +- 5 files changed, 36 insertions(+), 143 deletions(-) delete mode 100644 mattermost_bot/mattermost_v4.py diff --git a/mattermost_bot/mattermost.py b/mattermost_bot/mattermost.py index 9c6941b..c62946b 100644 --- a/mattermost_bot/mattermost.py +++ b/mattermost_bot/mattermost.py @@ -40,24 +40,25 @@ def post(self, request, data=None): verify=self.ssl_verify ).text) - def login(self, name, email, password): - props = {'name': name, 'login_id': email, 'password': password} - p = requests.post( - self.url + '/users/login', data=json.dumps(props), - verify=self.ssl_verify - ) - if p.status_code == 200: - self.token = p.headers["Token"] + def login(self, team, account, password): + props = {'login_id': account, 'password': password} + response =requests.post( + self.url + '/users/login', + data = json.dumps(props), + verify=self.ssl_verify) + if response.status_code == 200: + self.token = response.headers["Token"] self.load_initial_data() - return json.loads(p.text) + self.user = json.loads(response.text) + return self.user else: - p.raise_for_status() + response.raise_for_status() def load_initial_data(self): - self.initial = self.get('/users/initial_load') - self.default_team_id = self.initial['teams'][0]['id'] + self.teams = self.get('/users/me/teams') + self.default_team_id = self.teams[0]['id'] self.teams_channels_ids = {} - for team in self.initial['teams']: + for team in self.teams: self.teams_channels_ids[team['id']] = [] # get all channels belonging to each team for channel in self.get_channels(team['id']): @@ -65,39 +66,30 @@ def load_initial_data(self): def create_post(self, user_id, channel_id, message, files=None, pid=""): create_at = int(time.time() * 1000) - team_id = self.get_team_id(channel_id) return self.post( - '/teams/%s/channels/%s/posts/create' % (team_id, channel_id), - { - 'user_id': user_id, - 'channel_id': channel_id, - 'message': message, - 'create_at': create_at, - 'filenames': files or [], - 'pending_post_id': user_id + ':' + str(create_at), - 'state': "loading", - 'parent_id': pid, - 'root_id': pid, - }) + '/posts', + { + 'channel_id': channel_id, + 'message': message, + 'filenames': files or [], + 'root_id': pid, + }) def update_post(self, message_id, user_id, channel_id, message, files=None, pid=""): - team_id = self.get_team_id(channel_id) return self.post( - '/teams/%s/channels/%s/posts/update' % (team_id, channel_id), + '/posts/%s' % message_id, { - 'id': message_id, - 'channel_id': channel_id, 'message': message, }) def channel(self, channel_id): - team_id = self.get_team_id(channel_id) - return self.get('/teams/%s/channels/%s/' % (team_id, channel_id)) + channel = {'channel': self.get('/channels/%s' % channel_id)} + return channel def get_channels(self, team_id=None): if team_id is None: team_id = self.default_team_id - return self.get('/teams/%s/channels/' % team_id) + return self.get('/users/me/teams/%s/channels' % team_id) def get_team_id(self, channel_id): for team_id, channels in self.teams_channels_ids.items(): @@ -106,8 +98,7 @@ def get_team_id(self, channel_id): return None def get_user_info(self, user_id): - user_info = self.post('/users/ids',[user_id]) - return user_info[user_id] + return self.get('/users/{}'.format(user_id)) def me(self): return self.get('/users/me') @@ -116,11 +107,11 @@ def user(self, user_id): return self.get_user_info(user_id) def hooks_list(self): - return self.get('/teams/%s/hooks/incoming/list' % self.default_team_id) + return self.get('hooks/incoming', + {'team_id': self.default_team_id}) def hooks_create(self, **kwargs): - return self.post( - '/teams/%s/hooks/incoming/create' % self.default_team_id, kwargs) + return self.post('hooks/incoming', **kwargs) @staticmethod def in_webhook(url, channel, text, username=None, as_user=None, @@ -178,7 +169,7 @@ def update_msg(self, message_id, channel, message, pid=""): def connect_websocket(self): host = self.api.url.replace('http', 'ws').replace('https', 'wss') - url = host + '/users/websocket' + url = host + '/websocket' self._connect_websocket(url, cookie_name='MMAUTHTOKEN') return self.websocket.getstatus() == 101 diff --git a/mattermost_bot/mattermost_v4.py b/mattermost_bot/mattermost_v4.py deleted file mode 100644 index 2af5e95..0000000 --- a/mattermost_bot/mattermost_v4.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import logging -import ssl -import time - -import requests -import websocket -import websocket._exceptions - -from mattermost_bot.mattermost import MattermostClient, MattermostAPI -from pprint import pprint - -logger = logging.getLogger(__name__) - -class MattermostAPIv4(MattermostAPI): - - def login(self, team, account, password): - props = {'login_id': account, 'password': password} - response =requests.post( - self.url + '/users/login', - data = json.dumps(props), - verify=self.ssl_verify) - if response.status_code == 200: - self.token = response.headers["Token"] - self.load_initial_data() - self.user = json.loads(response.text) - return self.user - else: - response.raise_for_status() - - def load_initial_data(self): - self.teams = self.get('/users/me/teams') - self.default_team_id = self.teams[0]['id'] - self.teams_channels_ids = {} - for team in self.teams: - self.teams_channels_ids[team['id']] = [] - # get all channels belonging to each team - for channel in self.get_channels(team['id']): - self.teams_channels_ids[team['id']].append(channel['id']) - - def create_post(self, user_id, channel_id, message, files=None, pid=""): - create_at = int(time.time() * 1000) - return self.post( - '/posts', - { - 'channel_id': channel_id, - 'message': message, - 'filenames': files or [], - 'root_id': pid, - }) - - def update_post(self, message_id, user_id, channel_id, message, files=None, pid=""): - return self.post( - '/posts/%s' % message_id, - { - 'message': message, - }) - - def channel(self, channel_id): - channel = {'channel': self.get('/channels/%s' % channel_id)} - return channel - - def get_channels(self, team_id=None): - if team_id is None: - team_id = self.default_team_id - return self.get('/users/me/teams/%s/channels' % team_id) - - def create_user_dict(self, v4_dict): - new_dict = {} - new_dict[v4_dict['id']]=v4_dict - return new_dict - - def get_user_info(self, user_id): - return self.get('/users/{}'.format(user_id)) - -class MattermostClientv4(MattermostClient): - - def __init__(self, url, team, email, password, ssl_verify=True, login=1): - self.users = {} - self.channels = {} - self.mentions = {} - self.api = MattermostAPIv4(url, ssl_verify) - self.user = None - self.info = None - self.websocket = None - self.email = None - self.team = team - self.email = email - self.password = password - - if login: - self.login(team, email, password) - - def connect_websocket(self): - host = self.api.url.replace('http', 'ws').replace('https', 'wss') - url = host + '/websocket' - self._connect_websocket(url, cookie_name='MMAUTHTOKEN') - return self.websocket.getstatus() == 101 diff --git a/mattermost_bot/settings.py b/mattermost_bot/settings.py index 2eee87d..ff41b53 100644 --- a/mattermost_bot/settings.py +++ b/mattermost_bot/settings.py @@ -37,7 +37,7 @@ if key[:15] == 'MATTERMOST_BOT_': globals()[key[11:]] = os.environ[key] -settings_module = os.environ['MATTERMOST_BOT_SETTINGS_MODULE'] +settings_module = os.environ.get('MATTERMOST_BOT_SETTINGS_MODULE') if settings_module is not None: pwd = os.getcwd() diff --git a/tests/behavior_tests/driver.py b/tests/behavior_tests/driver.py index 204413c..5f4c0b4 100644 --- a/tests/behavior_tests/driver.py +++ b/tests/behavior_tests/driver.py @@ -2,17 +2,17 @@ from six.moves import _thread from websocket._exceptions import WebSocketConnectionClosedException, WebSocketTimeoutException from mattermost_bot.bot import Bot, PluginsManager -from mattermost_bot.mattermost_v4 import MattermostClientv4 +from mattermost_bot.mattermost import MattermostClient from mattermost_bot.dispatcher import MessageDispatcher import driver_settings, bot_settings logger = logging.getLogger(__name__) -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +#logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) class DriverBot(Bot): def __init__(self): - self._client = MattermostClientv4( + self._client = MattermostClient( driver_settings.BOT_URL, driver_settings.BOT_TEAM, driver_settings.BOT_LOGIN, driver_settings.BOT_PASSWORD, driver_settings.SSL_VERIFY diff --git a/tests/behavior_tests/run_bot.py b/tests/behavior_tests/run_bot.py index 9ef7cb3..4772fef 100644 --- a/tests/behavior_tests/run_bot.py +++ b/tests/behavior_tests/run_bot.py @@ -1,13 +1,13 @@ #!/usr/bin/env python from mattermost_bot.bot import Bot, PluginsManager -from mattermost_bot.mattermost_v4 import MattermostClientv4 +from mattermost_bot.mattermost import MattermostClient from mattermost_bot.dispatcher import MessageDispatcher import bot_settings class LocalBot(Bot): def __init__(self): - self._client = MattermostClientv4( + self._client = MattermostClient( bot_settings.BOT_URL, bot_settings.BOT_TEAM, bot_settings.BOT_LOGIN, bot_settings.BOT_PASSWORD, bot_settings.SSL_VERIFY From c1b9533d05084f9942f398c970aa956563f5d762 Mon Sep 17 00:00:00 2001 From: seLain Date: Thu, 1 Mar 2018 14:42:15 +0800 Subject: [PATCH 16/18] update README api v3 -> v4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c13d48a..7d4618f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ mattermost_bot_settings.py: ```python SSL_VERIFY = True # Whether to perform SSL cert verification -BOT_URL = 'http:///api/v3' # with 'http://' and with '/api/v3' path. without trailing slash. '/api/v1' - for version < 3.0 +BOT_URL = 'http:///api/v4' # with 'http://' and with '/api/v4' path. without trailing slash. '/api/v1' - for version < 3.0 BOT_LOGIN = '' BOT_PASSWORD = '' BOT_TEAM = '' # possible in lowercase From cedb273c23824d891afaddd0dc9995ac760ac5e1 Mon Sep 17 00:00:00 2001 From: seLain Date: Sat, 3 Mar 2018 00:21:39 +0800 Subject: [PATCH 17/18] fix: forget to remove redundant v4 from bot.py --- mattermost_bot/bot.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/mattermost_bot/bot.py b/mattermost_bot/bot.py index 0645196..7e690f6 100644 --- a/mattermost_bot/bot.py +++ b/mattermost_bot/bot.py @@ -15,23 +15,16 @@ from mattermost_bot import settings from mattermost_bot.dispatcher import MessageDispatcher from mattermost_bot.mattermost import MattermostClient -from mattermost_bot.mattermost_v4 import MattermostClientv4 logger = logging.getLogger(__name__) class Bot(object): def __init__(self): - if settings.MATTERMOST_API_VERSION == 4: - self._client = MattermostClientv4( - settings.BOT_URL, settings.BOT_TEAM, - settings.BOT_LOGIN, settings.BOT_PASSWORD, - settings.SSL_VERIFY) - else: - self._client = MattermostClient( - settings.BOT_URL, settings.BOT_TEAM, - settings.BOT_LOGIN, settings.BOT_PASSWORD, - settings.SSL_VERIFY) + self._client = MattermostClient( + settings.BOT_URL, settings.BOT_TEAM, + settings.BOT_LOGIN, settings.BOT_PASSWORD, + settings.SSL_VERIFY) logger.info('connected to mattermost') self._plugins = PluginsManager() self._dispatcher = MessageDispatcher(self._client, self._plugins) From 3fde7aee6fbaa30d802c2abd0ec7edf9858bed8e Mon Sep 17 00:00:00 2001 From: seLain Date: Fri, 9 Mar 2018 12:58:12 +0800 Subject: [PATCH 18/18] deal with redirected login request --- mattermost_bot/mattermost.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mattermost_bot/mattermost.py b/mattermost_bot/mattermost.py index c62946b..91bf9be 100644 --- a/mattermost_bot/mattermost.py +++ b/mattermost_bot/mattermost.py @@ -42,10 +42,20 @@ def post(self, request, data=None): def login(self, team, account, password): props = {'login_id': account, 'password': password} - response =requests.post( + response = requests.post( self.url + '/users/login', data = json.dumps(props), - verify=self.ssl_verify) + verify=self.ssl_verify, + allow_redirects=False) + if response.status_code in [301, 302, 307]: + # reset self.url to the new URL + self.url = response.headers['Location'].replace('/users/login', '') + # re-try login if redirected + response = requests.post( + self.url + '/users/login', + data = json.dumps(props), + verify=self.ssl_verify, + allow_redirects=False) if response.status_code == 200: self.token = response.headers["Token"] self.load_initial_data()