diff --git a/app/app/templates/shared/rollbar.html b/app/app/templates/shared/rollbar.html
index 1a6f0cd72ed..37ce54cb9e3 100644
--- a/app/app/templates/shared/rollbar.html
+++ b/app/app/templates/shared/rollbar.html
@@ -26,6 +26,15 @@
environment: "{{ env }}"
}
};
+
+ {% if github_handle %}
+ _rollbarConfig.payload.person = {
+ id: {{ user.id }},
+ username: "{{ github_handle }}",
+ email: "{{ user.email }}"
+ };
+ {% endif %}
+
// Rollbar Snippet
!function (r) { function e(n) { if (o[n]) return o[n].exports; var t = o[n] = { exports: {}, id: n, loaded: !1 }; return r[n].call(t.exports, t, t.exports, e), t.loaded = !0, t.exports } var o = {}; return e.m = r, e.c = o, e.p = "", e(0) }([function (r, e, o) { "use strict"; var n = o(1), t = o(4); _rollbarConfig = _rollbarConfig || {}, _rollbarConfig.rollbarJsUrl = _rollbarConfig.rollbarJsUrl || "https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.3.9/rollbar.min.js", _rollbarConfig.async = void 0 === _rollbarConfig.async || _rollbarConfig.async; var a = n.setupShim(window, _rollbarConfig), l = t(_rollbarConfig); window.rollbar = n.Rollbar, a.loadFull(window, document, !_rollbarConfig.async, _rollbarConfig, l) }, function (r, e, o) { "use strict"; function n(r) { return function () { try { return r.apply(this, arguments) } catch (r) { try { console.error("[Rollbar]: Internal error", r) } catch (r) { } } } } function t(r, e) { this.options = r, this._rollbarOldOnError = null; var o = s++; this.shimId = function () { return o }, "undefined" != typeof window && window._rollbarShims && (window._rollbarShims[o] = { handler: e, messages: [] }) } function a(r, e) { if (r) { var o = e.globalAlias || "Rollbar"; if ("object" == typeof r[o]) return r[o]; r._rollbarShims = {}, r._rollbarWrappedError = null; var t = new p(e); return n(function () { e.captureUncaught && (t._rollbarOldOnError = r.onerror, i.captureUncaughtExceptions(r, t, !0), i.wrapGlobals(r, t, !0)), e.captureUnhandledRejections && i.captureUnhandledRejections(r, t, !0); var n = e.autoInstrument; return e.enabled !== !1 && (void 0 === n || n === !0 || "object" == typeof n && n.network) && r.addEventListener && (r.addEventListener("load", t.captureLoad.bind(t)), r.addEventListener("DOMContentLoaded", t.captureDomContentLoaded.bind(t))), r[o] = t, t })() } } function l(r) { return n(function () { var e = this, o = Array.prototype.slice.call(arguments, 0), n = { shim: e, method: r, args: o, ts: new Date }; window._rollbarShims[this.shimId()].messages.push(n) }) } var i = o(2), s = 0, d = o(3), c = function (r, e) { return new t(r, e) }, p = d.bind(null, c); t.prototype.loadFull = function (r, e, o, t, a) { var l = function () { var e; if (void 0 === r._rollbarDidLoad) { e = new Error("rollbar.js did not load"); for (var o, n, t, l, i = 0; o = r._rollbarShims[i++];)for (o = o.messages || []; n = o.shift();)for (t = n.args || [], i = 0; i < t.length; ++i)if (l = t[i], "function" == typeof l) { l(e); break } } "function" == typeof a && a(e) }, i = !1, s = e.createElement("script"), d = e.getElementsByTagName("script")[0], c = d.parentNode; s.crossOrigin = "", s.src = t.rollbarJsUrl, o || (s.async = !0), s.onload = s.onreadystatechange = n(function () { if (!(i || this.readyState && "loaded" !== this.readyState && "complete" !== this.readyState)) { s.onload = s.onreadystatechange = null; try { c.removeChild(s) } catch (r) { } i = !0, l() } }), c.insertBefore(s, d) }, t.prototype.wrap = function (r, e, o) { try { var n; if (n = "function" == typeof e ? e : function () { return e || {} }, "function" != typeof r) return r; if (r._isWrap) return r; if (!r._rollbar_wrapped && (r._rollbar_wrapped = function () { o && "function" == typeof o && o.apply(this, arguments); try { return r.apply(this, arguments) } catch (o) { var e = o; throw "string" == typeof e && (e = new String(e)), e._rollbarContext = n() || {}, e._rollbarContext._wrappedSource = r.toString(), window._rollbarWrappedError = e, e } }, r._rollbar_wrapped._isWrap = !0, r.hasOwnProperty)) for (var t in r) r.hasOwnProperty(t) && (r._rollbar_wrapped[t] = r[t]); return r._rollbar_wrapped } catch (e) { return r } }; for (var u = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","), f = 0; f < u.length; ++f)t.prototype[u[f]] = l(u[f]); r.exports = { setupShim: a, Rollbar: p } }, function (r, e) { "use strict"; function o(r, e, o) { if (r) { var t; "function" == typeof e._rollbarOldOnError ? t = e._rollbarOldOnError : r.onerror && !r.onerror.belongsToShim && (t = r.onerror, e._rollbarOldOnError = t); var a = function () { var o = Array.prototype.slice.call(arguments, 0); n(r, e, t, o) }; a.belongsToShim = o, r.onerror = a } } function n(r, e, o, n) { r._rollbarWrappedError && (n[4] || (n[4] = r._rollbarWrappedError), n[5] || (n[5] = r._rollbarWrappedError._rollbarContext), r._rollbarWrappedError = null), e.handleUncaughtException.apply(e, n), o && o.apply(r, n) } function t(r, e, o) { if (r) { "function" == typeof r._rollbarURH && r._rollbarURH.belongsToShim && r.removeEventListener("unhandledrejection", r._rollbarURH); var n = function (r) { var o, n, t; try { o = r.reason } catch (r) { o = void 0 } try { n = r.promise } catch (r) { n = "[unhandledrejection] error getting `promise` from event" } try { t = r.detail, !o && t && (o = t.reason, n = t.promise) } catch (r) { t = "[unhandledrejection] error getting `detail` from event" } o || (o = "[unhandledrejection] error getting `reason` from event"), e && e.handleUnhandledRejection && e.handleUnhandledRejection(o, n) }; n.belongsToShim = o, r._rollbarURH = n, r.addEventListener("unhandledrejection", n) } } function a(r, e, o) { if (r) { var n, t, a = "EventTarget,Window,Node,ApplicationCache,AudioTrackList,ChannelMergerNode,CryptoOperation,EventSource,FileReader,HTMLUnknownElement,IDBDatabase,IDBRequest,IDBTransaction,KeyOperation,MediaController,MessagePort,ModalWindow,Notification,SVGElementInstance,Screen,TextTrack,TextTrackCue,TextTrackList,WebSocket,WebSocketWorker,Worker,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload".split(","); for (n = 0; n < a.length; ++n)t = a[n], r[t] && r[t].prototype && l(e, r[t].prototype, o) } } function l(r, e, o) { if (e.hasOwnProperty && e.hasOwnProperty("addEventListener")) { for (var n = e.addEventListener; n._rollbarOldAdd && n.belongsToShim;)n = n._rollbarOldAdd; var t = function (e, o, t) { n.call(this, e, r.wrap(o), t) }; t._rollbarOldAdd = n, t.belongsToShim = o, e.addEventListener = t; for (var a = e.removeEventListener; a._rollbarOldRemove && a.belongsToShim;)a = a._rollbarOldRemove; var l = function (r, e, o) { a.call(this, r, e && e._rollbar_wrapped || e, o) }; l._rollbarOldRemove = a, l.belongsToShim = o, e.removeEventListener = l } } r.exports = { captureUncaughtExceptions: o, captureUnhandledRejections: t, wrapGlobals: a } }, function (r, e) { "use strict"; function o(r, e) { this.impl = r(e, this), this.options = e, n(o.prototype) } function n(r) { for (var e = function (r) { return function () { var e = Array.prototype.slice.call(arguments, 0); if (this.impl[r]) return this.impl[r].apply(this.impl, e) } }, o = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,_createItem,wrap,loadFull,shimId,captureEvent,captureDomContentLoaded,captureLoad".split(","), n = 0; n < o.length; n++)r[o[n]] = e(o[n]) } o.prototype._swapAndProcessMessages = function (r, e) { this.impl = r(this.options); for (var o, n, t; o = e.shift();)n = o.method, t = o.args, this[n] && "function" == typeof this[n] && ("captureDomContentLoaded" === n || "captureLoad" === n ? this[n].apply(this, [t[0], o.ts]) : this[n].apply(this, t)); return this }, r.exports = o }, function (r, e) { "use strict"; r.exports = function (r) { return function (e) { if (!e && !window._rollbarInitialized) { r = r || {}; for (var o, n, t = r.globalAlias || "Rollbar", a = window.rollbar, l = function (r) { return new a(r) }, i = 0; o = window._rollbarShims[i++];)n || (n = o.handler), o.handler._swapAndProcessMessages(l, o.messages); window[t] = n, window._rollbarInitialized = !0 } } } }]);
// End Rollbar Snippet
diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js
index f48ed169453..442f4a185e5 100644
--- a/app/assets/v2/js/pages/bounty_details.js
+++ b/app/assets/v2/js/pages/bounty_details.js
@@ -724,7 +724,7 @@ var render_activity = function(result) {
created_on: _interested.created,
age: timeDifference(new Date(result['now']), new Date(_interested.created)),
status: 'started',
- uninterest_possible: isBountyOwner(result)
+ uninterest_possible: isBountyOwner(result) || document.isStaff
});
});
}
diff --git a/app/dashboard/notifications.py b/app/dashboard/notifications.py
index 59890f733f2..3a3974c0780 100644
--- a/app/dashboard/notifications.py
+++ b/app/dashboard/notifications.py
@@ -736,7 +736,7 @@ def maybe_notify_bounty_user_removed_to_slack(bounty, username):
#todo: DRY with expiration_start_work
num_days_back_to_warn = 3
-num_days_back_to_delete_interest = 10
+num_days_back_to_delete_interest = 6
def maybe_notify_user_removed_github(bounty, username, last_heard_from_user_days=None):
@@ -749,9 +749,9 @@ def maybe_notify_user_removed_github(bounty, username, last_heard_from_user_days
status_header = get_status_header(bounty)
- msg = f"""{status_header}@{username} has been removed from this issue due to inactivity ({last_heard_from_user_days} days) on the github thread. @{username} if you believe this was done in error, please go to the bounty and click 'start work' again.
-* [x] warning 1 ({num_days_back_to_warn} days)
-* [x] warning 2 ({num_days_back_to_warn * 2} days)
+ msg = f"""{status_header}@{username} has been removed for inactivity and [the issue]({bounty.url}) has been returned to an ‘Open’ Status. Let us know if you believe this has been done in error!
+
+* [x] warning ({num_days_back_to_warn} days)
* [x] auto removal ({num_days_back_to_delete_interest} days)
"""
@@ -763,11 +763,8 @@ def maybe_warn_user_removed_github(bounty, username, last_heard_from_user_days):
bounty.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK):
return False
- first_warning = 'x'
- second_warning = 'x' if last_heard_from_user_days > num_days_back_to_warn else ' '
- msg = f"""@{username} are you still working on this issue?
-* [{first_warning}] warning 1 ({num_days_back_to_warn} days)
-* [{second_warning}] warning 2 ({num_days_back_to_warn * 2} days)
+ msg = f"""@{username} Hello from Gitcoin Core - are you still working on this issue? Please submit a WIP PR or comment back within the next 3 days or you will be removed from this ticket and it will be returned to an ‘Open’ status. Please let us know if you have questions!
+* [x] warning ({num_days_back_to_warn} days)
* [ ] auto removal ({num_days_back_to_delete_interest} days)
"""
diff --git a/app/dashboard/templates/bounty_details.html b/app/dashboard/templates/bounty_details.html
index 4fcf5ba6b45..00944485c0d 100644
--- a/app/dashboard/templates/bounty_details.html
+++ b/app/dashboard/templates/bounty_details.html
@@ -295,6 +295,7 @@
diff --git a/app/dashboard/views.py b/app/dashboard/views.py
index 7e3788a0dad..c26a5680777 100644
--- a/app/dashboard/views.py
+++ b/app/dashboard/views.py
@@ -289,7 +289,7 @@ def uninterested(request, bounty_id, profile_id):
return JsonResponse({'errors': ['Bounty doesn\'t exist!']},
status=401)
- if not bounty.is_funder(request.user.username.lower()):
+ if not bounty.is_funder(request.user.username.lower()) and not request.user.is_staff:
return JsonResponse(
{'error': 'Only bounty funders are allowed to remove users!'},
status=401)
@@ -675,6 +675,7 @@ def bounty_details(request, ghuser='', ghrepo='', ghissue=0, stdbounties_id=None
'is_github_token_valid': is_github_token_valid(_access_token),
'github_auth_url': get_auth_url(request.path),
"newsletter_headline": _("Be the first to know about new funded issues."),
+ 'is_staff': request.user.is_staff,
}
if issue_url:
diff --git a/app/dataviz/d3_views.py b/app/dataviz/d3_views.py
index 8967f63b47c..9d6a3b73754 100644
--- a/app/dataviz/d3_views.py
+++ b/app/dataviz/d3_views.py
@@ -1,6 +1,7 @@
import json
import math
import random
+import time
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
@@ -532,6 +533,7 @@ def viz_graph(request, _type, template='graph'):
source = bounty.org_name
if source:
for fulfillment in bounty.fulfillments.all():
+ created = fulfillment.created_on.strftime("%s")
if _type != 'fulfillments_accepted_only' or fulfillment.accepted:
target = fulfillment.fulfiller_github_username.lower()
if hide_pii:
@@ -540,7 +542,7 @@ def viz_graph(request, _type, template='graph'):
types[target] = 'target_accepted' if fulfillment.accepted else 'target'
names[source] = None
names[target] = None
- edges.append((source, target, weight))
+ edges.append((source, target, weight, created))
value = values.get(source, 0)
value += weight
@@ -550,7 +552,8 @@ def viz_graph(request, _type, template='graph'):
values[target] = value
for tip in Tip.objects.filter(network='mainnet'):
- weight = bounty.value_in_usdt
+ weight = tip.value_in_usdt
+ created = tip.created_on.strftime("%s")
if weight:
source = tip.username.lower()
if hide_pii:
@@ -565,11 +568,12 @@ def viz_graph(request, _type, template='graph'):
if source not in types.keys():
types[target] = 'target'
names[target] = None
- edges.append((source, target, weight))
+ edges.append((source, target, weight, created))
if _type in ['what_future_could_look_like', 'all']:
last_node = None
+ created = 1525147679
nodes = Profile.objects.exclude(github_access_token='').all()
for profile in nodes:
node = profile.handle.lower()
@@ -585,7 +589,7 @@ def viz_graph(request, _type, template='graph'):
target = nodes.order_by('?').first().handle.lower()
if hide_pii:
target = helper_hide_pii(target)
- edges.append((target, node, weight))
+ edges.append((target, node, weight, created))
last_node = node
@@ -600,7 +604,7 @@ def viz_graph(request, _type, template='graph'):
value = int(math.sqrt(math.sqrt(values.get(name, 1))))
output['nodes'].append({"name": name, 'value': value, 'type': types.get(name), 'avatar': avatars.get(name)})
for edge in edges:
- source, target, weight = edge
+ source, target, weight, created = edge
weight = math.sqrt(weight)
if names.get(source) and names.get(target):
source = names[source]
@@ -610,6 +614,7 @@ def viz_graph(request, _type, template='graph'):
'target': target,
'value': value,
'weight': weight,
+ 'created': created,
})
return JsonResponse(output)
@@ -620,6 +625,7 @@ def viz_graph(request, _type, template='graph'):
'viz_type': _type,
'type_options': _type_options,
'page_route': page_route,
+ 'max_time': int(time.time()),
}
return TemplateResponse(request, f'dataviz/{template}.html', params)
diff --git a/app/dataviz/templates/dataviz/graph.html b/app/dataviz/templates/dataviz/graph.html
index 04a0020841f..6578abef564 100644
--- a/app/dataviz/templates/dataviz/graph.html
+++ b/app/dataviz/templates/dataviz/graph.html
@@ -37,22 +37,29 @@
@@ -63,87 +70,156 @@
{{comments}}
+
+
+
+
+
+
Date
+
+ link = link.data(links);
+
+ link.enter().insert("line", ".node")
+ .attr("class", "link");
+ link.exit()
+ .remove();
+
+ force.start();
+}
+
+
+
{% endblock %}
diff --git a/app/marketing/management/commands/expiration_start_work.py b/app/marketing/management/commands/expiration_start_work.py
index e8eb9dd9180..56eae71d14e 100644
--- a/app/marketing/management/commands/expiration_start_work.py
+++ b/app/marketing/management/commands/expiration_start_work.py
@@ -49,7 +49,7 @@ def handle(self, *args, **options):
# TODO: DRY with dashboard/notifications.py
num_days_back_to_warn = 3
- num_days_back_to_delete_interest = 10
+ num_days_back_to_delete_interest = 6
days = [i * 3 for i in range(1, 15)]
days.reverse()
diff --git a/app/marketing/management/commands/slackbot_welcome.py b/app/marketing/management/commands/slackbot_welcome.py
deleted file mode 100644
index e8c05d657a6..00000000000
--- a/app/marketing/management/commands/slackbot_welcome.py
+++ /dev/null
@@ -1,77 +0,0 @@
-'''
- Copyright (C) 2017 Gitcoin Core
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-'''
-import os
-import time
-
-from django.conf import settings
-from django.core.management.base import BaseCommand
-
-from slackclient import SlackClient
-
-
-MESSAGE="""
-Welcome to Gitcoin! :gitcoin:
-
-Gitcoin's mission is to *Grow Open Source*.
-
-A few quick facts:
-- We are mission driven. Check out our mission: https://gitcoin.co/mission
-- We aren't an ICO. There is no Gitcoin token.
-- We are a community of blockchain developers.
-
-Here's how to get started in the community
-- Say hey :wave: in #community-intros
-- Join the community livestream every Friday at 5pm EST: https://gitcoin.co/livestream
-
-We believe that a great way to build new skills is to learn by doing, and Gitcoin's core product (Bounties) supports that.
-
-Here's how to get started with bounties:
-- Developer? Check out the open bounties at https://gitcoin.co/explorer
-- Repo Owner? Accellerate your dev progress by posting a bounty of your own at https://gitcoin.co/new
-- Send a tip to any github username with https://gitcoin.co/tip
-
-We have more details in our onboarding guide at https://gitcoin.co/onboard . We also have a FAQ and tutorials available at https://gitcoin.co/help
-
-If you have any feedback for the team, or just want to say hi, this is them:
-- @owocki, @vivek, @Pixelant, @mbeacom, @coderberry, @justin-bean
-
-See you around! :spock-hand:
-Welcome_bot (and the Gitcoin Team)
-
-"""
-
-class Command(BaseCommand):
-
- help = 'pulls mailchimp emails'
-
- def handle(self, *args, **options):
- sc = SlackClient(settings.SLACK_WELCOMEBOT_TOKEN)
- if sc.rtm_connect():
- while True:
- time.sleep(1)
- new_evts = sc.rtm_read()
- for evt in new_evts:
- if "type" in evt:
- print("-" + evt['type'])
- if evt['type'] == 'team_join':
- user_info=sc.api_call("users.info", user=evt['user'])
- channel = sc.api_call("im.open", user=evt['user']['id'])
- channel_id = channel['channel']['id']
- sc.api_call("chat.postMessage", channel=channel_id, unfurl_links=False, text=MESSAGE, as_user=True)
- else:
- print("Connection Failed, invalid token?")
diff --git a/ops/lambdas/welcomebot/Makefile b/ops/lambdas/welcomebot/Makefile
new file mode 100644
index 00000000000..6959c8a9308
--- /dev/null
+++ b/ops/lambdas/welcomebot/Makefile
@@ -0,0 +1,32 @@
+# Help
+.DEFAULT_GOAL := help
+
+.PHONY: help
+
+init: ## Initialize the Zappa project for our AWS Lambda function and API Gateway.
+ @echo "Attempting to initialize Zappa"
+ @zappa init
+ @echo "Zappa initialized. Update domain_name and certificate_arn in zappa_settings.json, then run: make deploy && make certify"
+
+deploy: ## Deploy the welcomebot project for the first time. Run this once.
+ @echo "Deploying Welcomebot"
+ @zappa deploy welcomebot
+ @echo "Welcomebot deployed!"
+
+update: ## Update the welcomebot project.
+ @echo "Updating Welcomebot"
+ @zappa update welcomebot
+ @echo "Welcomebot updated!"
+
+undeploy: ## Undeploy the welcomebot.
+ @echo "Undeploying Welcomebot"
+ @zappa undeploy welcomebot
+ @echo "Welcomebot undeployed!"
+
+certify: ## Certify the AWS ACM certificate and establish your CNAME for welcomebot.
+ @echo "Certifying Welcomebot"
+ @zappa certify welcomebot
+ @echo "Welcomebot certified!"
+
+help: ## Display command information and usage instructions. Ex: make or make help
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
diff --git a/ops/lambdas/welcomebot/README.md b/ops/lambdas/welcomebot/README.md
new file mode 100644
index 00000000000..cd05b69da4a
--- /dev/null
+++ b/ops/lambdas/welcomebot/README.md
@@ -0,0 +1,11 @@
+# Gitcoin Slack Welcomebot
+
+The Gitcoin Slack Welcomebot is a [Flask](http://flask.pocoo.org/) application meant to run on [AWS Lambda](https://aws.amazon.com/lambda/) using [Zappa](https://github.com/Miserlou/Zappa).
+
+
+This bot is intended to be ran as Python 3.6 AWS Lambda function.
+
+Get started with the welcomebot by running `make init`.
+
+You will need to follow the Slack Bot setup instructions outlined in the [Python Slack Event API client](https://github.com/slackapi/python-slack-events-api) documentation.
+
diff --git a/ops/lambdas/welcomebot/app.py b/ops/lambdas/welcomebot/app.py
new file mode 100644
index 00000000000..f093b875918
--- /dev/null
+++ b/ops/lambdas/welcomebot/app.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+"""Define the Gitcoin Slack Welcome Bot flask app.
+
+Copyright (C) 2018 Gitcoin Core
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+"""
+import os
+
+from flask import Flask, abort, jsonify, redirect, request
+from slackclient import SlackClient
+from slackeventsapi import SlackEventAdapter
+
+SLACK_VERIFICATION_TOKEN = os.environ.get('SLACK_VERIFICATION_TOKEN', '')
+SLACK_WELCOMEBOT_TOKEN = os.environ.get('SLACK_WELCOMEBOT_TOKEN', '')
+SLACK_TEAM_ID = os.environ.get('SLACK_TEAM_ID', '')
+INDEX_REDIRECT_URL = os.environ.get('INDEX_REDIRECT_URL', 'https://gitcoin.co/slack')
+
+app = Flask(__name__)
+
+
+def get_default_message():
+ """Return the Gitcoin Welcomebot message."""
+ return """
+Welcome to Gitcoin! :gitcoin:
+
+Gitcoin's mission is to *Grow Open Source*.
+
+A few quick facts:
+- We are mission driven. Check out our mission: https://gitcoin.co/mission
+- We aren't an ICO. There is no Gitcoin token.
+- We are a community of blockchain developers.
+
+Here's how to get started in the community
+- Say hey :wave: in #community-intros
+- Join the community livestream every Friday at 5pm EST: https://gitcoin.co/livestream
+
+We believe that a great way to build new skills is to learn by doing, and Gitcoin's core product (Bounties) supports that.
+
+Here's how to get started with bounties:
+- Developer? Check out the open bounties at https://gitcoin.co/explorer
+- Repo Owner? Accelerate your dev progress by posting a bounty of your own at https://gitcoin.co/new
+- Send a tip to any github username with https://gitcoin.co/tip
+
+We have more details in our onboarding guide at https://gitcoin.co/onboard . We also have a FAQ and tutorials available at https://gitcoin.co/help
+
+If you have any feedback for the team, or just want to say hi, this is them:
+- @owocki, @vivek, @Pixelant, @mbeacom, @coderberry, @justin-bean
+
+See you around! :spock-hand:
+Welcome_bot (and the Gitcoin Team)
+
+"""
+
+
+MESSAGE = get_default_message()
+
+
+def is_request_valid(request, team_id=''):
+ token_valid = request.form['token'] == SLACK_VERIFICATION_TOKEN
+ team_id_valid = True
+
+ if team_id:
+ team_id_valid = request.form['team_id'] == team_id
+
+ return token_valid and team_id_valid
+
+
+@app.route('/')
+def index():
+ """Handle the index route."""
+ return redirect(INDEX_REDIRECT_URL, code=302)
+
+
+@app.route('/welcomebot', methods=['POST'])
+def welcomebot():
+ if not is_request_valid(request):
+ abort(400)
+ return jsonify(response_type='ephemeral', text=MESSAGE)
+
+
+slack_events_adapter = SlackEventAdapter(
+ SLACK_VERIFICATION_TOKEN, '/slack/events', app)
+
+
+@slack_events_adapter.on('team_join')
+def new_user_welcome(event):
+ """Handle team_join slack events by sending the user our welcome message."""
+ sc = SlackClient(SLACK_WELCOMEBOT_TOKEN)
+ user = event.get('event', {}).get('user', {}).get('id', '')
+ if user:
+ channel = sc.api_call('im.open', user=user)
+ channel_id = channel['channel']['id']
+ sc.api_call(
+ 'chat.postMessage',
+ channel=channel_id,
+ unfurl_links=False,
+ text=MESSAGE,
+ as_user=True)
+
+
+# Serve the slack welcomebot flask app.
+if __name__ == '__main__':
+ app.run()
diff --git a/ops/lambdas/welcomebot/requirements.txt b/ops/lambdas/welcomebot/requirements.txt
new file mode 100644
index 00000000000..fce42b30a28
--- /dev/null
+++ b/ops/lambdas/welcomebot/requirements.txt
@@ -0,0 +1,4 @@
+zappa
+flask
+slackeventsapi
+slackclient
diff --git a/ops/lambdas/welcomebot/zappa_settings.json b/ops/lambdas/welcomebot/zappa_settings.json
new file mode 100644
index 00000000000..52638ef221f
--- /dev/null
+++ b/ops/lambdas/welcomebot/zappa_settings.json
@@ -0,0 +1,24 @@
+{
+ "welcomebot": {
+ "app_function": "app.app",
+ "aws_region": "us-west-2",
+ "profile_name": "default",
+ "project_name": "welcomebot",
+ "runtime": "python3.6",
+ "s3_bucket": "",
+ "domain": "",
+ "slim_handler": true,
+ "certificate_arn": "",
+ "cloudwatch_log_level": "ERROR",
+ "exclude": [
+ "Makefile",
+ "README.md",
+ "requirements.txt",
+ "zappa_settings.json"
+ ],
+ "timeout_seconds": 30,
+ "keep_warm": true,
+ "keep_warm_expression": "rate(5 minutes)",
+ "memory_size": 128
+ }
+}
diff --git a/scripts/crontab b/scripts/crontab
index be35d124148..a421f27152e 100644
--- a/scripts/crontab
+++ b/scripts/crontab
@@ -38,7 +38,6 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us
15 10 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash bounty_feedback_email >> /var/log/gitcoin/bounty_feedback_email.log 2>&1
15 11 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash new_bounties_email >> /var/log/gitcoin/new_bounties_email.log 2>&1
0 0 1 */3 * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash send_quarterly_stats >> /var/log/gitcoin/send_quarterly_stats.log 2>&1
-*/15 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash slackbot_welcome >> /var/log/gitcoin/slackbot_welcome.log 2>&1
11 10 * * * cd gitcoin/coin; bash scripts/run_management_command.bash expiration_tip >> /var/log/gitcoin/expiration_tip.log 2>&1