Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use bot & user token for full functionality with newer bots #211

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:2.7
FROM python:3.9
WORKDIR /destalinator
ADD requirements.txt .
RUN pip install -r requirements.txt
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,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:

Expand All @@ -102,7 +102,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)

Expand Down
3 changes: 2 additions & 1 deletion destalinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def stale(self, channel_name, days):
# the message is not from an ignored user
x.get("user") not in self.config.ignore_users \
and x.get("username") not in self.config.ignore_users \
and x.get("bot_profile", {}).get("name") not in self.config.ignore_users \
and (
# the message must have text that doesn't include ignored words
(x.get("text") and b":dolphin:" not in x.get("text").encode('utf-8', 'ignore')) \
Expand Down Expand Up @@ -244,7 +245,7 @@ def warn_in_general(self, stale_channels):
being = "is"
there = "it"
message = "Hey, heads up -- the following {} {} stale and will be "
message += "archived if no one participates in {} over the next 30 days: "
message += "archived if no one participates in {} over the next days: "
message += ", ".join(["#" + x for x in stale_channels])
message = message.format(channel, being, there)
if self.config.activated:
Expand Down
2 changes: 1 addition & 1 deletion executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 31 additions & 26 deletions slacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,29 @@

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()

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()

def get_emojis(self):
url = self.url + "emoji.list?token={}".format(self.token)
url = self.url + "emoji.list"
return self.get_with_retry_to_json(url)

def get_users(self):
Expand Down Expand Up @@ -56,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()
Expand All @@ -82,7 +89,7 @@ def get_messages_in_time_range(self, oldest, cid, latest=None):
messages = []
done = False
while not done:
murl = self.url + "conversations.history?oldest={}&token={}&channel={}".format(oldest, self.token, cid)
murl = self.url + "conversations.history?oldest={}&channel={}".format(oldest, cid)
if latest:
murl += "&latest={}".format(latest)
else:
Expand Down Expand Up @@ -183,8 +190,8 @@ def get_channel_members_ids(self, channel_name):
if not member_count:
return members # should be an empty set

url_template = self.url + "conversations.members?token={}&channel={}"
url = url_template.format(self.token, cid)
url_template = self.url + "conversations.members?channel={}"
url = url_template.format(cid)

while True:
ret = self.get_with_retry_to_json(url)
Expand All @@ -198,8 +205,8 @@ def get_channel_members_ids(self, channel_name):

# once through the loop once, update the url to call to include the cursor
if ret['response_metadata']['next_cursor']:
url_template = self.url + "conversations.members?token={}&channel={}&cursor={}"
url = url_template.format(self.token, cid, ret['response_metadata']['next_cursor'])
url_template = self.url + "conversations.members?channel={}&cursor={}"
url = url_template.format(cid, ret['response_metadata']['next_cursor'])
# no more members to iterate over
else:
break
Expand Down Expand Up @@ -231,10 +238,10 @@ def get_channel_info(self, channel_name):
returns JSON with channel information. Adds 'age' in seconds to JSON
"""
# ensure include_num_members is available for get_channel_member_count()
url_template = self.url + "conversations.info?token={}&channel={}&include_num_members=true"
url_template = self.url + "conversations.info?channel={}&include_num_members=true"
cid = self.get_channelid(channel_name)
now = int(time.time())
url = url_template.format(self.token, cid)
url = url_template.format(cid)
ret = self.get_with_retry_to_json(url)
if ret['ok'] is not True:
m = "Attempted get_channel_info() for {}, but return was {}"
Expand All @@ -259,8 +266,8 @@ def get_all_channel_objects(self, exclude_archived=True):
else:
exclude_archived = 0

url_template = self.url + "conversations.list?exclude_archived={}&token={}"
url = url_template.format(exclude_archived, self.token)
url_template = self.url + "conversations.list?exclude_archived={}"
url = url_template.format(exclude_archived)

while True:
ret = self.get_with_retry_to_json(url)
Expand All @@ -274,8 +281,8 @@ def get_all_channel_objects(self, exclude_archived=True):
# after going through the loop once, update the url to call to
# include the pagination cursor
if ret['response_metadata']['next_cursor']:
url_template = self.url + "conversations.list?exclude_archived={}&token={}&cursor={}"
url = url_template.format(exclude_archived, self.token, ret['response_metadata']['next_cursor'])
url_template = self.url + "conversations.list?exclude_archived={}&cursor={}"
url = url_template.format(exclude_archived, ret['response_metadata']['next_cursor'])

# no more channels to iterate over
else:
Expand All @@ -284,7 +291,7 @@ def get_all_channel_objects(self, exclude_archived=True):
return channels

def get_all_user_objects(self):
url = self.url + "users.list?token=" + self.token
url = self.url + "users.list"
response = self.get_with_retry_to_json(url)
try:
return response['members']
Expand All @@ -293,10 +300,10 @@ def get_all_user_objects(self):
raise e

def archive(self, channel_name):
url_template = self.url + "conversations.archive?token={}&channel={}"
url_template = self.url + "conversations.archive?channel={}"
cid = self.get_channelid(channel_name)
url = url_template.format(self.token, cid)
request = self.session.post(url)
url = url_template.format(cid)
request = self.user_session.post(url)
payload = request.json()
return payload

Expand All @@ -312,22 +319,20 @@ def post_message(self, channel, message, message_type=None):
channel = channel[1:]

post_data = {
'token': self.token,
'channel': channel,
'text': message.encode('utf-8')
}

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:
post_data['icon_url'] = bot_avatar_url

if message_type:
post_data['attachments'] = json.dumps([{'fallback': message_type}], encoding='utf-8')
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()
3 changes: 2 additions & 1 deletion tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from config import get_config

channels = [
{
Expand Down Expand Up @@ -39,7 +40,7 @@
{
'id': 'C0133272',
'creator': 'U012742',
'name': 'zmeta-new-channels',
'name': get_config().announce_channel,
'purpose': {'value': "New channel annoucements."},
'created': int(time.time()) - 86400 * 90
}
Expand Down
2 changes: 1 addition & 1 deletion tests/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
24 changes: 12 additions & 12 deletions tests/test_destalinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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.
Expand All @@ -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')
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down