Skip to content

Commit

Permalink
Add slack bot integration (#955)
Browse files Browse the repository at this point in the history
* - add slack settings tab on user's settings page
- send same messages to user's slack as to gitcoin's slack

Fixes: #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
  • Loading branch information
darkdarkdragon authored and mbeacom committed Apr 27, 2018
1 parent c3e0441 commit c959cf2
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 29 deletions.
3 changes: 2 additions & 1 deletion app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
3 changes: 3 additions & 0 deletions app/dashboard/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ==============")
Expand All @@ -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,
}

Expand Down
21 changes: 20 additions & 1 deletion app/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Expand Down
69 changes: 64 additions & 5 deletions app/dashboard/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,27 +171,86 @@ 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:
conv_details = f"@ (${round(convert_token_to_usdt(bounty.token_name),2)}/{bounty.token_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.
Expand Down
5 changes: 4 additions & 1 deletion app/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
87 changes: 66 additions & 21 deletions app/marketing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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),
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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, '')

Expand Down
28 changes: 28 additions & 0 deletions app/retail/templates/settings/slack.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'settings/settings.html' %}
{% load i18n static %}
{% block settings_content %}
<form id="settings" method="POST">
<div class="form-group">
<h5>{% trans "Slack Integration" %}</h5>
<p>
{% blocktrans %} Gitcoin can post updates of bounties statuses of your repositories to your team's slack.
To do this create bot
<a href="https://my.slack.com/apps/A0F7YS25R-bots">here</a>
and provide slack API token in the field below. {% endblocktrans %}
</p>
<label class="form__label" for="token">{% trans "Slack API token" %}</label>
<input type="text" name="token" class="form__input" placeholder='API token' value="{{ profile.slack_token }}">
</div>
<div class="form-group">
<label class="form__label" for="repos">{% trans "Github repos to monitor (use full name, like 'gitcoin/web')" %}</label>
<input type="text" name="repos" class="form__input" placeholder='comma, separated, list (gitcoin/web)' value="{{ repos }}">
</div>
<div class="form-group">
<label class="form__label" for="channel">{% trans "Slack channel to post to" %}</label>
<input type="text" name="channel" class="form__input" placeholder='#notif-gitcoin' value="{{ profile.slack_channel }}">
</div>
{% csrf_token %}
<input class='button button--primary' type='submit' name='submit' value="{% trans "Go" %}">
</form>
</div>
{% endblock %}

0 comments on commit c959cf2

Please sign in to comment.