diff --git a/__init__.py b/__init__.py index 0515cec..dc53c38 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,4 @@ +import aiohttp import asyncio from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import datetime @@ -228,18 +229,6 @@ async def receive_slack_msg(**payload): if 'files' in data: files = data['files'] - # TODO: When real support for 'files' is implemented, - # it should probably be in the format_attachments_for_zulip - # call. - - # if 'files' in data: - # for file in data['files']: - # web_client.files_sharedPublicURL(id=file['id']) - # if msg == '': - # msg = file['permalink_public'] - # else: - # msg += '\n' + file['permalink_public'] - formatted_attachments = \ await slack_reformat.format_attachments_from_slack( msg, attachments, @@ -248,8 +237,13 @@ async def receive_slack_msg(**payload): # Assumes that both markdown and plaintext need a newline together. needs_leading_newline = \ (len(msg) > 0 or len(formatted_attachments['markdown']) > 0) - formatted_files = slack_reformat.format_files_from_slack( - files, needs_leading_newline, SLACK_TOKEN, self.zulip_client) + + # TODO: We might not want a new client session for every message. + async with aiohttp.ClientSession() as session: + formatted_files = await slack_reformat.format_files_from_slack( + files, needs_leading_newline, + session, SLACK_TOKEN, + ZULIP_URL, aiohttp.BasicAuth(ZULIP_BOT_EMAIL, ZULIP_API_KEY)) zulip_message_text = \ msg + formatted_attachments['markdown'] + formatted_files['markdown'] diff --git a/requirements.txt b/requirements.txt index 54121d2..304a9f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +aiohttp==3.6.2 redis==3.2.1 requests==2.22.0 slackclient==2.5.0 diff --git a/slack_reformat.py b/slack_reformat.py index d020b3c..b5d3df2 100644 --- a/slack_reformat.py +++ b/slack_reformat.py @@ -5,7 +5,6 @@ import logging import re import sys -import requests import traceback from io import BytesIO @@ -153,14 +152,17 @@ async def replace_markdown_link(m): _SLACK_LINK_MATCH, replace_markdown_link) - -def format_files_from_slack(files, needs_leading_newline, - slack_bearer_token=None, zulip_client=None): +async def format_files_from_slack(files, needs_leading_newline, + aiohttp_session=None, + slack_bearer_token=None, + zulip_url=None, + aiohttp_zulip_basic_auth=None): '''Given a list of files from the slack API, return both a markdown and plaintext string representation of those files. - Assuming a bearer token and zulip client are provided, the files are mirrored to zulip - and those links are included in the markdown result. + Assuming a aiohttp Client Session, slack bearer token, root zulip URL, and an + aiohttp.BasicAuth for zulip are provided, the files are mirrored to zulip and + those links are included in the markdown result (but not the plaintext result). This method only uses the passed in message text to determine how to format its output caller must append as appropriate.''' @@ -182,24 +184,41 @@ def format_files_from_slack(files, needs_leading_newline, if 'name' in file and file['name']: rendered_markdown_name = file['name'] - if slack_bearer_token and zulip_client and 'url_private' in file and file['url_private']: + if (aiohttp_session and slack_bearer_token + and zulip_url and aiohttp_zulip_basic_auth + and 'url_private' in file and file['url_private']): file_private_url = file['url_private'] - r = requests.get(file_private_url, - headers={"Authorization": f"Bearer {slack_bearer_token}"}) - if r.status_code == 200: - if file_private_url != r.url: + r = await aiohttp_session.get(file_private_url, + headers={"Authorization": f"Bearer {slack_bearer_token}"}) + if r.status == 200: + if str(r.url) != file_private_url: # we were redirected! _LOGGER.info( - f'Apparent slack redirect from {file_private_url} to {r.url} when bridging file. Skipping.') + f'Apparent slack redirect from {file_private_url} to {str(r.url)} when bridging file. Skipping.') else: - uploadable_file = BytesIO(r.content) + uploadable_file = BytesIO(await r.content.read()) uploadable_file.name = file['name'] - response = zulip_client.upload_file(uploadable_file) - if 'uri' in response and response['uri']: + file_dict = {'file': uploadable_file} + + # Because we want to use async io for this potentially long running request, + # we can't use the zulip client library. Instead, REST/OpenAPI it is. + upload_response = await aiohttp_session.post( + f'{zulip_url}/api/v1/user_uploads', + data=file_dict, + auth=aiohttp_zulip_basic_auth) + + response = {} + if upload_response.status == 200: + response = await upload_response.json() + else: + _LOGGER.info( + f"Upload to Zulip Failed for {file['name']}. Code {upload_response.status}.") + + if upload_response.status == 200 and 'uri' in response and response['uri']: rendered_markdown_name = f"[{file['name']}]({response['uri']})" else: - _LOGGER.info('Got bad response when uploading to zulip: {}'.format(response)) + _LOGGER.info(f"Got bad response uploading to zulip for {file['name']}.. Body: {await upload_response.text()}") else: _LOGGER.info(f"Got code {r.status_code} when fetching {file_private_url} from slack.") diff --git a/slack_reformat_test.py b/slack_reformat_test.py index 07825ee..4b6a952 100644 --- a/slack_reformat_test.py +++ b/slack_reformat_test.py @@ -199,17 +199,17 @@ def test_format_files_from_slack(self): # path for files. # None case - output = slack_reformat.format_files_from_slack(None, False) + output = do_await(slack_reformat.format_files_from_slack(None, False)) self.assertEqual(output['plaintext'], '') self.assertEqual(output['markdown'], '') # None case, leading newline - output = slack_reformat.format_files_from_slack(None, True) + output = do_await(slack_reformat.format_files_from_slack(None, True)) self.assertEqual(output['plaintext'], '') self.assertEqual(output['markdown'], '') # Base case - output = slack_reformat.format_files_from_slack([], False) + output = do_await(slack_reformat.format_files_from_slack([], False)) self.assertEqual(output['plaintext'], '') self.assertEqual(output['markdown'], '') @@ -236,17 +236,17 @@ def test_format_files_from_slack(self): "url_private": "https://files.slack.com/files-pri/T0000000-F0000000/filename.jpg" # ... and many omitted fields } - output = slack_reformat.format_files_from_slack([test_file], True) + output = do_await(slack_reformat.format_files_from_slack([test_file], True)) self.assertEqual(output['plaintext'], '\n(Bridged Message included file: filename.jpg)') self.assertEqual(output['markdown'], '\n*(Bridged Message included file: filename.jpg)*') # Same test, no leading newline. - output = slack_reformat.format_files_from_slack([test_file], False) + output = do_await(slack_reformat.format_files_from_slack([test_file], False)) self.assertEqual(output['plaintext'], '(Bridged Message included file: filename.jpg)') self.assertEqual(output['markdown'], '*(Bridged Message included file: filename.jpg)*') # Multiple files. - output = slack_reformat.format_files_from_slack([test_file, test_file], False) + output = do_await(slack_reformat.format_files_from_slack([test_file, test_file], False)) self.assertEqual(output['plaintext'], '(Bridged Message included file: filename.jpg)\n(Bridged Message included file: filename.jpg)') self.assertEqual(output['markdown'], @@ -254,13 +254,13 @@ def test_format_files_from_slack(self): # If we have a title that matches the filename, it should not be displayed. test_file['title'] = test_filename - output = slack_reformat.format_files_from_slack([test_file], True) + output = do_await(slack_reformat.format_files_from_slack([test_file], True)) self.assertEqual(output['plaintext'], '\n(Bridged Message included file: filename.jpg)') self.assertEqual(output['markdown'], '\n*(Bridged Message included file: filename.jpg)*') # Add a distinct title to the above: test_file['title'] = 'File Title' - output = slack_reformat.format_files_from_slack([test_file], True) + output = do_await(slack_reformat.format_files_from_slack([test_file], True)) self.assertEqual(output['plaintext'], '\n(Bridged Message included file: filename.jpg: \'File Title\')') self.assertEqual(output['markdown'], '\n*(Bridged Message included file: filename.jpg: \'File Title\')*') @@ -269,7 +269,7 @@ def test_format_files_from_slack(self): "id": "U0000000", "mode": "tombstone", } - output = slack_reformat.format_files_from_slack([test_file], False) + output = do_await(slack_reformat.format_files_from_slack([test_file], False)) self.assertEqual(output['plaintext'], '') self.assertEqual(output['markdown'], '')