From 3e0f48acbf380de2d787cab407558812d262acfc Mon Sep 17 00:00:00 2001 From: Christof Damian Date: Wed, 14 Apr 2021 12:27:26 +0200 Subject: [PATCH] Use bot & user token for full functionality with newer bots Allow passing both the bot and user token to the scripts via the environment variables DESTALINATOR_API_BOT_TOKEN and DESTALINATOR_API_USER_TOKEN. They have different permissions and the bot token is only used to post messags, all other calls use the user token. Fixes #194. This also includes #210 and fixes #209 by moving the tokens into the header instead of using query parameters. Don't pass `as_user` to Slack, as this has been removed. --- README.md | 6 +++--- executor.py | 2 +- slacker.py | 25 +++++++++++++++---------- tests/mocks.py | 2 +- tests/test_destalinator.py | 24 ++++++++++++------------ 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d759476..d1f5880 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,9 @@ All configs in `configuration.yaml` are overrideable through environment variabl 1. Make sure [the Slackbot app](https://slack.com/apps/A0F81R8ET-slackbot) is installed for your Slack 2. Add a Slackbot integration, and copy the `token` parameter from the URL provided -#### `DESTALINATOR_API_TOKEN` (Required) +#### `DESTALINATOR_API_BOT_TOKEN` and `DESTALINATOR_API_USER_TOKEN`(Required) -The best way to get an `API_TOKEN` is to [create a new Slack App](https://api.slack.com/apps/new). +The best way to get the `API BOT_TOKEN` and `API_USER_TOKEN` is to [create a new Slack App](https://api.slack.com/apps/new). Once you create and name your app on your team, go to "OAuth & Permissions" to give it the following permission scopes: @@ -104,7 +104,7 @@ Once you create and name your app on your team, go to "OAuth & Permissions" to g - `emoji:read` - `users:read` -After saving, you can copy the OAuth Access Token value from the top of the same screen. It probably starts with `xox`. +After saving, you can copy the OAuth Access Tokens value from the top of the same screen. It probably starts with `xox`. #### `DESTALINATOR_ACTIVATED` (Required) diff --git a/executor.py b/executor.py index a05b638..b933f45 100755 --- a/executor.py +++ b/executor.py @@ -17,7 +17,7 @@ def __init__(self, slackbot_injected=None, slacker_injected=None): self.logger.debug("activated is %s", self.config.activated) - self.slacker = slacker_injected or slacker.Slacker(self.config.slack_name, token=self.config.api_token) + self.slacker = slacker_injected or slacker.Slacker(self.config.slack_name, bot_token=self.config.api_bot_token, user_token=self.config.api_user_token) self.ds = destalinator.Destalinator(slacker=self.slacker, slackbot=self.slackbot, diff --git a/slacker.py b/slacker.py index 65febca..07ff86a 100755 --- a/slacker.py +++ b/slacker.py @@ -12,17 +12,23 @@ class Slacker(WithLogger, WithConfig): - def __init__(self, slack_name, token, init=True): + def __init__(self, slack_name, bot_token, user_token, init=True): """ slack name is the short name of the slack (preceding '.slack.com') - token should be a Slack API Token. + bot_token should be a Slack API Bot Token. + user_token should be a Slack API User Token. """ self.slack_name = slack_name - self.token = token - assert self.token, "Token should not be blank" self.url = self.api_url() - self.session = requests.Session() - self.session.headers.update({"Authorization": "Bearer " + token}) + + assert bot_token, "Bot Token should not be blank" + self.bot_session = requests.Session() + self.bot_session.headers.update({"Authorization": "Bearer " + bot_token}) + + assert user_token, "User Token should not be blank" + self.user_session = requests.Session() + self.user_session.headers.update({"Authorization": "Bearer " + user_token}) + if init: self.get_users() self.get_channels() @@ -57,7 +63,7 @@ def get_with_retry_to_json(self, url): max_retry_attempts = 10 payload = None while not payload: - response = self.session.get(url) + response = self.user_session.get(url) try: response.raise_for_status() @@ -292,7 +298,7 @@ def archive(self, channel_name): url_template = self.url + "conversations.archive?channel={}" cid = self.get_channelid(channel_name) url = url_template.format(cid) - request = self.session.post(url) + request = self.user_session.post(url) payload = request.json() return payload @@ -315,7 +321,6 @@ def post_message(self, channel, message, message_type=None): bot_name = self.config.bot_name bot_avatar_url = self.config.bot_avatar_url if bot_name or bot_avatar_url: - post_data['as_user'] = False if bot_name: post_data['username'] = bot_name if bot_avatar_url: @@ -324,5 +329,5 @@ def post_message(self, channel, message, message_type=None): if message_type: post_data['attachments'] = json.dumps([{'fallback': message_type}]) - p = self.session.post(self.url + "chat.postMessage", data=post_data) + p = self.bot_session.post(self.url + "chat.postMessage", data=post_data) return p.json() diff --git a/tests/mocks.py b/tests/mocks.py index ff1553f..9b224f1 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -12,7 +12,7 @@ def mocked_slackbot_object(): def mocked_slacker_object(channels_list=None, users_list=None, messages_list=None, emoji_list=None): - slacker_obj = slacker.Slacker(get_config().slack_name, token='token', init=False) + slacker_obj = slacker.Slacker(get_config().slack_name, bot_token='bot_token', user_token='user_token', init=False) slacker_obj.get_all_channel_objects = mock.MagicMock(return_value=channels_list or []) slacker_obj.get_channels() diff --git a/tests/test_destalinator.py b/tests/test_destalinator.py index 5188040..fea58d4 100644 --- a/tests/test_destalinator.py +++ b/tests/test_destalinator.py @@ -110,7 +110,7 @@ def get_channels(self): class DestalinatorChannelMarkupTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -156,7 +156,7 @@ def test_add_slack_channel_markup_ignore_screaming(self, mock_slacker): class DestalinatorChannelMinimumAgeTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -184,7 +184,7 @@ def test_channel_is_young(self, mock_slacker): class DestalinatorGetEarliestArchiveDateTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") # TODO: This test (and others) would be redundant with solid testing around config directly. @@ -210,7 +210,7 @@ def test_falls_back_to_past_date(self): class DestalinatorGetMessagesTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -245,7 +245,7 @@ def test_with_limited_included_subtypes(self, mock_slacker): class DestalinatorIgnoreChannelTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch.object(get_config(), 'ignore_channels', ['stalinists']) @@ -288,7 +288,7 @@ def test_with_empty_ignore_channel_config(self): class DestalinatorPostMarkedUpMessageTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") def test_with_a_string_having_a_channel(self): @@ -319,7 +319,7 @@ def test_with_a_string_having_no_channels(self): class DestalinatorStaleTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -379,7 +379,7 @@ def test_with_only_an_attachment_message(self, mock_slacker): class DestalinatorArchiveTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch.object(get_config(), 'ignore_channels', ['stalinists']) @@ -442,7 +442,7 @@ def test_handles_a_bad_archive_api_response(self, mock_slacker): class DestalinatorSafeArchiveTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -477,7 +477,7 @@ def test_calls_archive_method(self, mock_slacker): class DestalinatorSafeArchiveAllTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -534,7 +534,7 @@ def fake_stale(channel, days): class DestalinatorWarnTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock') @@ -603,7 +603,7 @@ def test_does_not_warn_when_previous_warning_with_changed_text_found(self, mock_ class DestalinatorWarnAllTestCase(unittest.TestCase): def setUp(self): - self.slacker = SlackerMock("testing", "token") + self.slacker = SlackerMock("testing", "bot_token", "user_token") self.slackbot = slackbot.Slackbot("testing", "token") @mock.patch('tests.test_destalinator.SlackerMock')