diff --git a/hypha/apply/activity/messaging.py b/hypha/apply/activity/messaging.py index 07d56b18c26a36e4fae32d564a002514b118a4d0..944532c958d333b4c6fa9f495278155764469b39 100644 --- a/hypha/apply/activity/messaging.py +++ b/hypha/apply/activity/messaging.py @@ -2,7 +2,6 @@ import json import logging from collections import defaultdict -import requests from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model @@ -10,6 +9,7 @@ from django.db import models from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import gettext as _ +from django_slack import slack_message from hypha.apply.projects.models.payment import ( APPROVED_BY_FINANCE_1, @@ -467,7 +467,7 @@ class SlackAdapter(AdapterBase): def __init__(self): super().__init__() - self.destination = settings.SLACK_DESTINATION_URL + self.destination = settings.SLACK_ENDPOINT_URL self.target_room = settings.SLACK_DESTINATION_ROOM self.comments_room = settings.SLACK_DESTINATION_ROOM_COMMENTS self.comments_type = settings.SLACK_TYPE_COMMENTS @@ -716,23 +716,26 @@ class SlackAdapter(AdapterBase): def send_message(self, message, recipient, source, **kwargs): target_rooms = self.slack_channels(source, **kwargs) - if not self.destination or not any(target_rooms): + if not any(target_rooms) or not settings.SLACK_TOKEN: errors = list() - if not self.destination: - errors.append('Destination URL') if not target_rooms: errors.append('Room ID') + if not settings.SLACK_TOKEN: + errors.append('Slack Token') return 'Missing configuration: {}'.format(', '.join(errors)) message = ' '.join([recipient, message]).strip() data = { - "room": target_rooms, "message": message, } - response = requests.post(self.destination, json=data) - - return str(response.status_code) + ': ' + response.content.decode() + for room in target_rooms: + try: + slack_message('messages/slack_message.html', data, channel=room) + except Exception as e: + logger.exception(e) + return '400: Bad Request' + return '200: OK' class EmailAdapter(AdapterBase): diff --git a/hypha/apply/activity/templates/messages/slack_message.html b/hypha/apply/activity/templates/messages/slack_message.html new file mode 100644 index 0000000000000000000000000000000000000000..1974748d10599f1f3950bac5009d9f22736b1ab2 --- /dev/null +++ b/hypha/apply/activity/templates/messages/slack_message.html @@ -0,0 +1,6 @@ +{% extends django_slack %} +<!--Template required for django-slack. We can customize it for channels, endpoint_url, etc as per the requirements.--> + +{% block text %} +{{ message|safe }} +{% endblock %} diff --git a/hypha/apply/activity/tests/test_messaging.py b/hypha/apply/activity/tests/test_messaging.py index bd716ec6256dc611edab3c147bb88b170a403055..e6f90c0c6a1029cb41bd185e4ae514586a8ea1c1 100644 --- a/hypha/apply/activity/tests/test_messaging.py +++ b/hypha/apply/activity/tests/test_messaging.py @@ -7,6 +7,7 @@ import responses from django.contrib.messages import get_messages from django.core import mail from django.test import TestCase, override_settings +from django_slack.utils import get_backend from hypha.apply.funds.tests.factories import ( ApplicationSubmissionFactory, @@ -330,99 +331,101 @@ class TestActivityAdapter(TestCase): class TestSlackAdapter(AdapterMixin, TestCase): source_factory = ApplicationSubmissionFactory + backend = 'django_slack.backends.TestBackend' target_url = 'https://my-slack-backend.com/incoming/my-very-secret-key' target_room = '#<ROOM ID>' + token = 'fake-token' @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=None, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_cant_send_with_no_room(self): + error_message = "Missing configuration: Room ID" adapter = SlackAdapter() submission = ApplicationSubmissionFactory() - adapter.send_message('my message', '', source=submission) - self.assertEqual(len(responses.calls), 0) + messages = adapter.send_message('my message', '', source=submission) + self.assertEqual(messages, error_message) @override_settings( - SLACK_DESTINATION_URL=None, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=None, ) - @responses.activate - def test_cant_send_with_no_url(self): + def test_cant_send_with_no_token(self): + error_message = "Missing configuration: Slack Token" adapter = SlackAdapter() submission = ApplicationSubmissionFactory() - adapter.send_message('my message', '', source=submission) - self.assertEqual(len(responses.calls), 0) + messages = adapter.send_message('my message', '', source=submission) + self.assertEqual(messages, error_message) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_correct_payload(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') + backend = get_backend() + backend.reset_messages() submission = ApplicationSubmissionFactory() adapter = SlackAdapter() message = 'my message' adapter.send_message(message, '', source=submission) - self.assertEqual(len(responses.calls), 1) - self.assertDictEqual( - json.loads(responses.calls[0].request.body), - { - 'room': [self.target_room], - 'message': message, - } - ) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 1) + message_payload = json.loads(messages[0]['payload']) + self.assertEqual(message_payload['text'], message) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_fund_custom_slack_channel(self): + backend = get_backend() + backend.reset_messages() responses.add(responses.POST, self.target_url, status=200, body='OK') submission = ApplicationSubmissionFactory(round__parent__slack_channel='dummy') adapter = SlackAdapter() message = 'my message' adapter.send_message(message, '', source=submission) - self.assertEqual(len(responses.calls), 1) - self.assertDictEqual( - json.loads(responses.calls[0].request.body), - { - 'room': ['#dummy'], - 'message': message, - } - ) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 1) + message_payload = json.loads(messages[0]['payload']) + self.assertEqual(message_payload['text'], message) + self.assertEqual(message_payload['channel'], '#dummy') @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_fund_multiple_custom_slack_channel(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') + backend = get_backend() + backend.reset_messages() submission = ApplicationSubmissionFactory(round__parent__slack_channel='dummy1, dummy2') adapter = SlackAdapter() message = 'my message' adapter.send_message(message, '', source=submission) - self.assertEqual(len(responses.calls), 1) - self.assertDictEqual( - json.loads(responses.calls[0].request.body), - { - 'room': ['#dummy1', '#dummy2'], - 'message': message, - } - ) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 2) + for index, sent_message in enumerate(messages): + message_payload = json.loads(sent_message['payload']) + self.assertEqual(message_payload['text'], message) + self.assertEqual(message_payload['channel'], '#dummy' + str(index + 1)) - @responses.activate def test_gets_lead_if_slack_set(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory() recipients = adapter.recipients(MESSAGES.COMMENT, source=submission, related=None) self.assertTrue(submission.lead.slack in recipients[0]) - @responses.activate def test_gets_blank_if_slack_not_set(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory(lead__slack='') @@ -430,13 +433,12 @@ class TestSlackAdapter(AdapterMixin, TestCase): self.assertTrue(submission.lead.slack in recipients[0]) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_message_with_good_response(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') - self.adapter = SlackAdapter() self.adapter_process(MESSAGES.NEW_SUBMISSION) self.assertEqual(Message.objects.count(), 1) @@ -445,19 +447,21 @@ class TestSlackAdapter(AdapterMixin, TestCase): self.assertEqual(sent_message.status, '200: OK') @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate - def test_message_with_bad_response(self): - responses.add(responses.POST, self.target_url, status=400, body='Bad Request') - - self.adapter = SlackAdapter() - self.adapter_process(MESSAGES.NEW_SUBMISSION) - self.assertEqual(Message.objects.count(), 1) - sent_message = Message.objects.first() - self.assertEqual(sent_message.content[0:10], self.adapter.messages[MESSAGES.NEW_SUBMISSION][0:10]) - self.assertEqual(sent_message.status, '400: Bad Request') + def test_400_bad_request(self): + backend = get_backend() + backend.reset_messages() + submission = ApplicationSubmissionFactory() + adapter = SlackAdapter() + message = '' + message_status = adapter.send_message(message, '', source=submission) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 0) + self.assertEqual(message_status, '400: Bad Request') @override_settings(SEND_MESSAGES=True) @@ -592,8 +596,10 @@ class TestAdaptersForProject(AdapterMixin, TestCase): activity = ActivityAdapter source_factory = ProjectFactory # Slack + backend = 'django_slack.backends.TestBackend' target_url = 'https://my-slack-backend.com/incoming/my-very-secret-key' target_room = '#<ROOM ID>' + token = 'fake-token' def test_activity_lead_change(self): old_lead = UserFactory() @@ -635,12 +641,14 @@ class TestAdaptersForProject(AdapterMixin, TestCase): self.assertEqual(project.submission, activity.related_object) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_slack_created(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') + backend = get_backend() + backend.reset_messages() project = self.source_factory() user = UserFactory() self.adapter_process( @@ -650,18 +658,21 @@ class TestAdaptersForProject(AdapterMixin, TestCase): source=project, related=project.submission, ) - self.assertEqual(len(responses.calls), 1) - data = json.loads(responses.calls[0].request.body) - self.assertIn(str(user), data['message']) - self.assertIn(str(project), data['message']) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 1) + message_payload = json.loads(messages[0]['payload']) + self.assertIn(str(user), message_payload['text']) + self.assertIn(str(project), message_payload['text']) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_slack_lead_change(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') + backend = get_backend() + backend.reset_messages() project = self.source_factory() user = UserFactory() self.adapter_process( @@ -671,19 +682,21 @@ class TestAdaptersForProject(AdapterMixin, TestCase): source=project, related=project.submission, ) - self.assertEqual(len(responses.calls), 1) - data = json.loads(responses.calls[0].request.body) - self.assertIn(str(user), data['message']) - self.assertIn(str(project), data['message']) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 1) + message_payload = json.loads(messages[0]['payload']) + self.assertIn(str(user), message_payload['text']) + self.assertIn(str(project), message_payload['text']) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_slack_applicant_update_invoice(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') - + backend = get_backend() + backend.reset_messages() project = self.source_factory() invoice = InvoiceFactory(project=project) applicant = ApplicantFactory() @@ -695,21 +708,22 @@ class TestAdaptersForProject(AdapterMixin, TestCase): source=project, related=invoice, ) + messages = backend.retrieve_messages() - self.assertEqual(len(responses.calls), 1) - - data = json.loads(responses.calls[0].request.body) - self.assertIn(str(applicant), data['message']) - self.assertIn(str(project), data['message']) + self.assertEqual(len(messages), 1) + message_payload = json.loads(messages[0]['payload']) + self.assertIn(str(applicant), message_payload['text']) + self.assertIn(str(project), message_payload['text']) @override_settings( - SLACK_DESTINATION_URL=target_url, + SLACK_ENDPOINT_URL=target_url, SLACK_DESTINATION_ROOM=target_room, + SLACK_BACKEND=backend, + SLACK_TOKEN=token, ) - @responses.activate def test_slack_staff_update_invoice(self): - responses.add(responses.POST, self.target_url, status=200, body='OK') - + backend = get_backend() + backend.reset_messages() project = self.source_factory() invoice = InvoiceFactory(project=project) staff = StaffFactory() @@ -721,8 +735,8 @@ class TestAdaptersForProject(AdapterMixin, TestCase): source=project, related=invoice, ) - - self.assertEqual(len(responses.calls), 1) + messages = backend.retrieve_messages() + self.assertEqual(len(messages), 1) @override_settings(SEND_MESSAGES=True) def test_email_staff_update_invoice(self): diff --git a/hypha/apply/utils/notifications.py b/hypha/apply/utils/notifications.py index 13e403e4b1800de4ea94de5e90580cdcdbaf2e30..8b6930af1cf1e7004055709910b3726915c729aa 100644 --- a/hypha/apply/utils/notifications.py +++ b/hypha/apply/utils/notifications.py @@ -1,11 +1,15 @@ -import requests +import logging + from django.conf import settings +from django_slack import slack_message + +logger = logging.getLogger(__name__) class SlackNotifications(): def __init__(self): - self.destination = settings.SLACK_DESTINATION_URL + self.destination = settings.SLACK_ENDPOINT_URL self.target_room = settings.SLACK_DESTINATION_ROOM def __call__(self, *args, recipients=None, related=None, **kwargs): @@ -29,12 +33,14 @@ class SlackNotifications(): return f'<{link}|{title}>' def send_message(self, message, request, recipients=None, related=None, **kwargs): - if not self.destination or not self.target_room: + if not self.destination or not self.target_room or not settings.SLACK_TOKEN: errors = list() if not self.destination: errors.append('Destination URL') if not self.target_room: errors.append('Room ID') + if not settings.SLACK_TOKEN: + errors.append('Slack Token') return 'Missing configuration: {}'.format(', '.join(errors)) slack_users = self.slack_users(recipients) if recipients else '' @@ -44,12 +50,14 @@ class SlackNotifications(): message = ' '.join([slack_users, message, slack_link]).strip() data = { - "room": self.target_room, "message": message, } - response = requests.post(self.destination, json=data) - - return str(response.status_code) + ': ' + response.content.decode() + try: + slack_message('messages/slack_message.html', data, channel=self.target_room) + return '200: OK' + except Exception as e: + logger.exception(e) + return '400: Bad Request' slack_notify = SlackNotifications() diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 10f87df0d0df4900b15d8b49a12ca971721d0a1d..07e5af2b9dc97fad7fbfc133c753e5d03903e143 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -105,6 +105,7 @@ INSTALLED_APPS = [ 'django_bleach', 'django_fsm', 'django_pwned_passwords', + 'django_slack', 'django_otp', 'django_otp.plugins.otp_totp', 'django_otp.plugins.otp_static', @@ -524,7 +525,13 @@ if not SEND_MESSAGES: SEND_READY_FOR_REVIEW = env.bool('SEND_READY_FOR_REVIEW', True) -SLACK_DESTINATION_URL = env.str('SLACK_DESTINATION_URL', None) +# Django Slack settings +SLACK_TOKEN = env.str('SLACK_TOKEN', None) +SLACK_USERNAME = env.str('SLACK_USERNAME', 'Hypha') +SLACK_BACKEND = 'django_slack.backends.CeleryBackend' # UrllibBackend can be used for sync +SLACK_ENDPOINT_URL = env.str('SLACK_ENDPOINT_URL', 'https://slack.com/api/chat.postMessage') + +# Slack settings SLACK_DESTINATION_ROOM = env.str('SLACK_DESTINATION_ROOM', None) SLACK_DESTINATION_ROOM_COMMENTS = env.str('SLACK_DESTINATION_ROOM_COMMENTS', None) SLACK_TYPE_COMMENTS = env.list('SLACK_TYPE_COMMENTS', []) diff --git a/requirements.txt b/requirements.txt index c66d1540300e2172c40cbf54f95da9a7aad5f82a..bad1dafa298265c3a425c49f12fa95efe39a375b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ django-redis==5.1.0 django-referrer-policy==1.0 django-salesforce==4.0 django-select2==7.9.0 +django-slack==5.17.7 django-storages==1.12.3 django-tables2==2.4.1 django-tinymce==3.4.0