From c959cf2b41e577562b42cead998840d40d087431 Mon Sep 17 00:00:00 2001 From: Ivan Tivonenko Date: Fri, 27 Apr 2018 18:07:50 +0300 Subject: [PATCH] Add slack bot integration (#955) * - add slack settings tab on user's settings page - send same messages to user's slack as to gitcoin's slack Fixes: https://github.com/gitcoinco/web/issues/259 * Update models.py * Fix indent on docstring * Fix docstrings * no len, switch to fstring * better description for slack integration Fixes: #259 * translate button value * added placeholder for slack channel input box * remove migration, needs to be regenerated * remove need to own repo to get slack notifications * Update bad reverse * Make url names unique --- app/app/urls.py | 3 +- app/dashboard/helpers.py | 3 + app/dashboard/models.py | 21 +++++- app/dashboard/notifications.py | 69 +++++++++++++++++-- app/dashboard/views.py | 5 +- app/marketing/views.py | 87 ++++++++++++++++++------ app/retail/templates/settings/slack.html | 28 ++++++++ 7 files changed, 187 insertions(+), 29 deletions(-) create mode 100644 app/retail/templates/settings/slack.html diff --git a/app/app/urls.py b/app/app/urls.py index 214d1a9bf56..561cd0f4f15 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -201,7 +201,8 @@ re_path(r'^settings/privacy/?', marketing.views.privacy_settings, name='privacy_settings'), re_path(r'^settings/matching/?', marketing.views.matching_settings, name='matching_settings'), re_path(r'^settings/feedback/?', marketing.views.feedback_settings, name='feedback_settings'), - re_path(r'^settings/(.*)?', marketing.views.email_settings, name='feedback_settings'), + re_path(r'^settings/slack/?', marketing.views.slack_settings, name='slack_settings'), + re_path(r'^settings/(.*)?', marketing.views.email_settings, name='settings'), # marketing views url(r'^leaderboard/(.*)', marketing.views.leaderboard, name='leaderboard'), diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 66e669d52a7..3552145e972 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -33,6 +33,7 @@ from dashboard.models import Bounty, BountyFulfillment, BountySyncRequest, UserAction from dashboard.notifications import ( maybe_market_to_email, maybe_market_to_github, maybe_market_to_slack, maybe_market_to_twitter, + maybe_market_to_user_slack, ) from economy.utils import convert_amount from github.utils import _AUTH @@ -561,6 +562,7 @@ def process_bounty_changes(old_bounty, new_bounty): print("============ posting ==============") did_post_to_twitter = maybe_market_to_twitter(new_bounty, event_name) did_post_to_slack = maybe_market_to_slack(new_bounty, event_name) + did_post_to_user_slack = maybe_market_to_user_slack(new_bounty, event_name) did_post_to_github = maybe_market_to_github(new_bounty, event_name, profile_pairs) did_post_to_email = maybe_market_to_email(new_bounty, event_name) print("============ done posting ==============") @@ -571,6 +573,7 @@ def process_bounty_changes(old_bounty, new_bounty): 'did_post_to_email': did_post_to_email, 'did_post_to_github': did_post_to_github, 'did_post_to_slack': did_post_to_slack, + 'did_post_to_user_slack': did_post_to_user_slack, 'did_post_to_twitter': did_post_to_twitter, } diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 33f086733b2..7b8b0f69da5 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -26,8 +26,8 @@ from django.contrib.auth.models import User from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.humanize.templatetags.humanize import naturalday, naturaltime -from django.contrib.postgres.fields import JSONField from django.contrib.staticfiles.templatetags.staticfiles import static +from django.contrib.postgres.fields import ArrayField, JSONField from django.db import models from django.db.models.signals import m2m_changed, post_delete, post_save, pre_save from django.dispatch import receiver @@ -861,6 +861,9 @@ class Profile(SuperModel): email = models.CharField(max_length=255, blank=True, db_index=True) github_access_token = models.CharField(max_length=255, blank=True, db_index=True) pref_lang_code = models.CharField(max_length=2, choices=settings.LANGUAGES) + slack_repos = ArrayField(models.CharField(max_length=200), blank=True, default=[]) + slack_token = models.CharField(max_length=255, default='') + slack_channel = models.CharField(max_length=255, default='') suppress_leaderboard = models.BooleanField( default=False, help_text='If this option is chosen, we will remove your profile information from the leaderboard', @@ -1021,6 +1024,22 @@ def username(self): handle = self.handle return handle + def has_repo(self, full_name): + """Check if user has access to repo. + + Args: + full_name (str): Repository name, like gitcoin/web. + + Returns: + bool: Whether or not user has access to repository. + + """ + for repo in self.repos_data: + if repo['full_name'] == full_name: + return True + return False + + def is_github_token_valid(self): """Check whether or not a Github OAuth token is valid. diff --git a/app/dashboard/notifications.py b/app/dashboard/notifications.py index e595c7b81a9..e7081ad5f67 100644 --- a/app/dashboard/notifications.py +++ b/app/dashboard/notifications.py @@ -171,6 +171,31 @@ def maybe_market_to_slack(bounty, event_name): if bounty.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK: return False + msg = build_message_for_slack(bounty, event_name) + if not msg: + return False + + try: + channel = 'notif-gitcoin' + sc = SlackClient(settings.SLACK_TOKEN) + sc.api_call("chat.postMessage", channel=channel, text=msg) + except Exception as e: + print(e) + return False + return True + + +def build_message_for_slack(bounty, event_name): + """Build message to be posted to slack. + + Args: + bounty (dashboard.models.Bounty): The Bounty to be marketed. + event_name (str): The name of the event. + + Returns: + str: Message to post to slack. + + """ conv_details = "" usdt_details = "" try: @@ -178,20 +203,54 @@ def maybe_market_to_slack(bounty, event_name): usdt_details = f"({bounty.value_in_usdt_now} USD {conv_details} " except Exception: pass # no USD conversion rate + title = bounty.title if bounty.title else bounty.github_url msg = f"{event_name.replace('bounty', 'funded_issue')} worth {round(bounty.get_natural_value(), 4)} {bounty.token_name} " \ f"{usdt_details}" \ f"{bounty.token_name}: {title} \n\n{bounty.get_absolute_url()}" + return msg + + +def maybe_market_to_user_slack(bounty, event_name): + """Send a Slack message to the user's slack channel for the specified Bounty. + Args: + bounty (dashboard.models.Bounty): The Bounty to be marketed. + event_name (str): The name of the event. + + Returns: + bool: Whether or not the Slack notification was sent successfully. + + """ + from dashboard.models import Profile + if bounty.get_natural_value() < 0.0001: + return False + if bounty.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK: + return False + + msg = build_message_for_slack(bounty, event_name) + if not msg: + return False + + url = bounty.github_url + uri = parse(url).path + uri_array = uri.split('/') + sent = False try: - channel = 'notif-gitcoin' - sc = SlackClient(settings.SLACK_TOKEN) - sc.api_call("chat.postMessage", channel=channel, text=msg) + repo = uri_array[1] + '/' + uri_array[2] + subscribers = Profile.objects.filter(slack_repos__contains=[repo]) + subscribers = subscribers & Profile.objects.exclude(slack_token='', slack_channel='') + for subscriber in subscribers: + try: + sc = SlackClient(subscriber.slack_token) + sc.api_call("chat.postMessage", channel=subscriber.slack_channel, text=msg) + sent = True + except Exception as e: + print(e) except Exception as e: print(e) - return False - return True + return sent def maybe_market_tip_to_email(tip, emails): """Send an email for the specified Tip. diff --git a/app/dashboard/views.py b/app/dashboard/views.py index d395444a8e2..f0a01ebfc9d 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -42,7 +42,7 @@ ) from dashboard.notifications import ( maybe_market_tip_to_email, maybe_market_tip_to_github, maybe_market_tip_to_slack, maybe_market_to_slack, - maybe_market_to_twitter, + maybe_market_to_twitter, maybe_market_to_user_slack, ) from dashboard.utils import get_bounty, get_bounty_id, has_tx_mined, web3_process_bounty from gas.utils import conf_time_spread, eth_usd_conv_rate, recommend_min_gas_price_to_confirm_in_time @@ -115,6 +115,7 @@ def create_new_interest_helper(bounty, user): bounty.interested.add(interest) record_user_action(user, 'start_work', interest) maybe_market_to_slack(bounty, 'start_work') + maybe_market_to_user_slack(bounty, 'start_work') maybe_market_to_twitter(bounty, 'start_work') return interest @@ -231,6 +232,7 @@ def remove_interest(request, bounty_id): bounty.interested.remove(interest) interest.delete() maybe_market_to_slack(bounty, 'stop_work') + maybe_market_to_user_slack(bounty, 'stop_work') maybe_market_to_twitter(bounty, 'stop_work') except Interest.DoesNotExist: return JsonResponse({ @@ -280,6 +282,7 @@ def uninterested(request, bounty_id, profile_id): interest = Interest.objects.get(profile_id=profile_id, bounty=bounty) bounty.interested.remove(interest) maybe_market_to_slack(bounty, 'stop_work') + maybe_market_to_user_slack(bounty, 'stop_work') interest.delete() except Interest.DoesNotExist: return JsonResponse({ diff --git a/app/marketing/views.py b/app/marketing/views.py index ca2de75704c..66316a9e509 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -403,24 +403,24 @@ def funnel(request): } return TemplateResponse(request, 'funnel.html', params) -settings_navs = [ - { + +def get_settings_navs(): + return [{ 'body': 'Email', - 'href': '/settings/email', - }, - { + 'href': reverse('email_settings', args=('', )) + }, { 'body': 'Privacy', - 'href': '/settings/privacy', - }, - { + 'href': reverse('privacy_settings'), + }, { 'body': 'Matching', - 'href': '/settings/matching', - }, - { + 'href': reverse('matching_settings'), + }, { 'body': 'Feedback', - 'href': '/settings/feedback', - }, -] + 'href': reverse('feedback_settings'), + }, { + 'body': 'Slack', + 'href': reverse('slack_settings'), + }] def settings_helper_get_auth(request, key=None): @@ -489,7 +489,7 @@ def privacy_settings(request): 'nav': 'internal', 'active': '/settings/privacy', 'title': _('Privacy Settings'), - 'navs': settings_navs, + 'navs': get_settings_navs(), 'is_logged_in': is_logged_in, 'msg': msg, } @@ -517,7 +517,7 @@ def matching_settings(request): else: es.metadata['ip'].append(ip) es.save() - msg = "Updated your preferences. " + msg = _('Updated your preferences.') context = { 'keywords': ",".join(es.keywords), @@ -527,7 +527,7 @@ def matching_settings(request): 'nav': 'internal', 'active': '/settings/matching', 'title': _('Matching Settings'), - 'navs': settings_navs, + 'navs': get_settings_navs(), 'msg': msg, } return TemplateResponse(request, 'settings/matching.html', context) @@ -554,13 +554,13 @@ def feedback_settings(request): else: es.metadata['ip'].append(ip) es.save() - msg = "We've received your feedback. " + msg = _('We\'ve received your feedback.') context = { 'nav': 'internal', 'active': '/settings/feedback', 'title': _('Feedback'), - 'navs': settings_navs, + 'navs': get_settings_navs(), 'msg': msg, } return TemplateResponse(request, 'settings/feedback.html', context) @@ -617,19 +617,64 @@ def email_settings(request, key): else: es.metadata['ip'].append(ip) es.save() - msg = "Updated your preferences. " + msg = _('Updated your preferences.') context = { 'nav': 'internal', 'active': '/settings/email', 'title': _('Email Settings'), 'es': es, 'msg': msg, - 'navs': settings_navs, + 'navs': get_settings_navs(), 'preferred_language': pref_lang } return TemplateResponse(request, 'settings/email.html', context) +def slack_settings(request): + """Displays and saves user's slack settings. + + Returns: + TemplateResponse: The user's slack settings template response. + + """ + # setup + profile, es, user, is_logged_in = settings_helper_get_auth(request) + if not es: + login_redirect = redirect('/login/github?next=' + request.get_full_path()) + return login_redirect + + msg = '' + + if request.POST and request.POST.get('submit'): + token = request.POST.get('token', '') + repos = request.POST.get('repos').split(',') + channel = request.POST.get('channel', '') + profile.slack_token = token + profile.slack_repos = repos + profile.slack_channel = channel + ip = get_ip(request) + if not es.metadata.get('ip', False): + es.metadata['ip'] = [ip] + else: + es.metadata['ip'].append(ip) + es.save() + profile.save() + msg = _('Updated your preferences.') + + context = { + 'repos': ",".join(profile.slack_repos), + 'is_logged_in': is_logged_in, + 'nav': 'internal', + 'active': '/settings/slack', + 'title': _('Slack Settings'), + 'navs': get_settings_navs(), + 'es': es, + 'profile': profile, + 'msg': msg, + } + return TemplateResponse(request, 'settings/slack.html', context) + + def _leaderboard(request): return leaderboard(request, '') diff --git a/app/retail/templates/settings/slack.html b/app/retail/templates/settings/slack.html new file mode 100644 index 00000000000..726dcb921c7 --- /dev/null +++ b/app/retail/templates/settings/slack.html @@ -0,0 +1,28 @@ +{% extends 'settings/settings.html' %} +{% load i18n static %} +{% block settings_content %} +
+
+
{% trans "Slack Integration" %}
+

+ {% blocktrans %} Gitcoin can post updates of bounties statuses of your repositories to your team's slack. + To do this create bot + here + and provide slack API token in the field below. {% endblocktrans %} +

+ + +
+
+ + +
+
+ + +
+ {% csrf_token %} + +
+ +{% endblock %}