From a18a8969ff1c8d661a844cae6ea393d591a3572e Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Wed, 13 Dec 2017 11:11:32 -0600 Subject: [PATCH 1/7] working flask endpoint for migration to slack app --- src/app.py | 2 +- src/flask_endpoint.py | 29 +++++++++++++++++++++++++ utils/general_utils.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/flask_endpoint.py create mode 100644 utils/general_utils.py diff --git a/src/app.py b/src/app.py index cec1584..f595ced 100755 --- a/src/app.py +++ b/src/app.py @@ -24,7 +24,7 @@ "Our projects can be viewed on ") PROXY = config('PROXY') -TOKEN = config('TOKEN') +TOKEN = config('PERSONAL_APP_TOKEN') COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') PROXY = PROXY if PROXY else None diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py new file mode 100644 index 0000000..bfac303 --- /dev/null +++ b/src/flask_endpoint.py @@ -0,0 +1,29 @@ +from flask import Flask, request, Response +from decouple import config +import json +from src import app as bot +from pprint import pprint + +app = Flask(__name__) + +VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') + + +@app.route('/', methods=['POST']) +def challenge(): + pprint(request.get_json()) + payload = {} + data = request.get_json() + if data['token'] != VERIFICATION_TOKEN: + print("Bad request") + return '' + if data['type'] == 'url_verification': + payload['challenge'] = data['challenge'] + else: + bot.event_handler(data['event']) + print(payload) + return json.dumps(payload) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/utils/general_utils.py b/utils/general_utils.py new file mode 100644 index 0000000..bfa58da --- /dev/null +++ b/utils/general_utils.py @@ -0,0 +1,48 @@ +from slackclient import SlackClient +from decouple import config + +TOKEN = config('PERSONAL_APP_TOKEN') + +slack_client = SlackClient(TOKEN) + + +def list_channels(): + channels_call = slack_client.api_call("channels.list") + if channels_call.get('ok'): + return channels_call['channels'] + return None + + +def channel_info(channel_id): + channel_info = slack_client.api_call("channels.info", channel=channel_id) + if channel_info: + return channel_info['channel'] + return None + + +def send_message(channel_id, message): + slack_client.api_call( + "chat.postMessage", + channel=channel_id, + text=message, + username='test-bot', + icon_emoji=':robot_face:' + ) + + +if __name__ == '__main__': + channels = list_channels() + if channels: + print("Channels: ") + for channel in channels: + print(channel['name'] + " (" + channel['id'] + ")") + detailed_info = channel_info(channel['id']) + # if detailed_info: + # print('Latest text from ' + channel['name'] + ":") + # print(detailed_info['latest']['text']) + if channel['name'] == 'general': + send_message(channel['id'], "Hello " + + channel['name'] + "! It worked!") + print('-----') + else: + print("Unable to authenticate.") From 0a95e0b7509653f469a02a7d596201a24d62b65e Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Wed, 13 Dec 2017 12:58:33 -0600 Subject: [PATCH 2/7] added JSON for greet message --- src/app.py | 56 +++++++++++++++++++++++++++++++++++++++---- src/flask_endpoint.py | 3 +++ src/message.json | 8 +++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/message.json diff --git a/src/app.py b/src/app.py index f595ced..dc80215 100755 --- a/src/app.py +++ b/src/app.py @@ -23,6 +23,51 @@ "All active Operation Code Projects are located on our source control repository. " "Our projects can be viewed on ") +MESSAGE_JSON = { + "text": "Hi {real_name},\n\n Welcome to Operation Code! I'm a bot designed to help answer questions and get you " + "on your way in our community.\n\nPlease take a moment to review our " + "\n\nOur goal here at Operation Code is to get " + "veterans and their families started on the path to a career in programming. We do that through providing " + "you with scholarships, mentoring, career development opportunities, conference tickets, " + "and more!\n\nYou're currently in Slack, a chat application that serves as the hub of Operation Code. If " + "you're currently visiting us via your browser, Slack provides a stand alone program to make staying in " + "touch even more convenient. You can download it \n\nWant to make your " + "first change to a program right now? All active Operation Code Projects are located on our source " + "control repository. Our projects can be viewed on ", + "attachments": [ + { + "text": "", + "fallback": "", + "color": "#3AA3E3", + "attachment_type": "default", + "actions": [ + { + "name": "CoC", + "text": "Code of Conduct", + "type": "button", + "value": "Coc", + "url": "https://op.co.de/code-of-conduct" + }, + { + "name": "slack", + "text": "Download Slack", + "type": "button", + "value": "slack", + "url": "https://slack.com/downloads" + + }, + { + "name": "repo", + "text": "Op-Code Github", + "type": "button", + "value": "repo", + "url": "https://github.com/OperationCode/START_HERE" + } + ] + } + ] +} + PROXY = config('PROXY') TOKEN = config('PERSONAL_APP_TOKEN') COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') @@ -63,13 +108,16 @@ def new_member(event_dict): real_name = user_name_from_id(user_id) - custom_message = build_message(MESSAGE, real_name=real_name) + custom_message = MESSAGE.format(real_name=real_name) + MESSAGE_JSON['text'] = custom_message new_event_logger.info('Built message: {}'.format(custom_message)) - response = slack_client.api_call('chat.postMessage', + response = slack_client.api_call('chat.postEphemeral', channel=user_id, - text=custom_message, - as_user=True) + user=user_id, + as_user=True, + username="New Bot Name", + **MESSAGE_JSON) # Notify #community # slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py index bfac303..506907d 100644 --- a/src/flask_endpoint.py +++ b/src/flask_endpoint.py @@ -4,6 +4,8 @@ from src import app as bot from pprint import pprint +from utils.log_manager import setup_logging + app = Flask(__name__) VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') @@ -26,4 +28,5 @@ def challenge(): if __name__ == '__main__': + setup_logging() app.run(debug=True) diff --git a/src/message.json b/src/message.json new file mode 100644 index 0000000..b23e2cf --- /dev/null +++ b/src/message.json @@ -0,0 +1,8 @@ +{ + "text": "placeholder!!!", + "attachments": [ + { + "text": "Read our code of conduct!" + } + ] +} \ No newline at end of file From 0e0bd7b69d4cda543239749ca09647d18b634139 Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Wed, 13 Dec 2017 18:49:35 -0600 Subject: [PATCH 3/7] added interactive help menu and new member join notification --- {src => archived}/message.py | 0 archived/working_simple_bot.py | 4 +- requirements.txt | Bin 422 -> 754 bytes run.py | 5 +- src/app.py | 161 +++++++++++++++------------------ src/flask_endpoint.py | 52 +++++++++-- src/help_menu.py | 46 ++++++++++ src/message.json | 8 -- src/messages.py | 99 ++++++++++++++++++++ utils/general_utils.py | 15 ++- 10 files changed, 274 insertions(+), 116 deletions(-) rename {src => archived}/message.py (100%) create mode 100644 src/help_menu.py delete mode 100644 src/message.json create mode 100644 src/messages.py diff --git a/src/message.py b/archived/message.py similarity index 100% rename from src/message.py rename to archived/message.py diff --git a/archived/working_simple_bot.py b/archived/working_simple_bot.py index 81c5154..5d00a48 100644 --- a/archived/working_simple_bot.py +++ b/archived/working_simple_bot.py @@ -6,8 +6,8 @@ import websocket from urllib3 import disable_warnings, exceptions # allow to disable InsecureRequestWarning, not sure if needed -from src.creds import TOKEN # locally saved file "creds.py" this is added to .gitignore -from src.message import new_join +# from src.creds import TOKEN # locally saved file "creds.py" this is added to .gitignore +from archived.message import new_join from utils.log_manager import setup_logging # any file can get instance of logger. diff --git a/requirements.txt b/requirements.txt index 34f350fbe8ffc6d8dec90e275333d0be391a6798..824395ce50518dc383c1f206640c9fd967884ca2 100644 GIT binary patch literal 754 zcmZWn%TB{U44g9(pVFus`hWvh4oI9ha86SCfHVnth{DGMGixtJLaS9)_KZCq=liR` z$kSuMElZjXY<4e?{dS&a+}&)4Kvc#XLg zd25fisL_&H;K-{Eb60Q^5qHcNRA)@(0ySj3(sy9T#(9daJUWuGVZ6aEPS9fV2~{C? zRrlmKcp)y2omC^h;tBi~!rF^7^jN9s-!iK)uwT8ENcq2+>H3Q=sw=~zT_l)fsd1KCM*}iS!Yih7P z4Rqx6JEv{f)D3YDnz!QXz$%R^viZ^!Nvr;s6A=@gP)E73WS_GhW$IX~WHfo3`)1$C iD#!UWcrTe?4vmTI{G delta 37 vcmV+=0NVfZ1*QW6|NfIw0xFRpNRhx6lk5RzlY9bFlcE70lRN<&lfnTiBnJ)} diff --git a/run.py b/run.py index 649576a..1512f3c 100644 --- a/run.py +++ b/run.py @@ -1,6 +1,7 @@ import logging from src.app import run_bot - +from src.flask_endpoint import start_server if __name__ == '__main__': - run_bot(delay=1) + # run_bot(delay=1) + start_server() diff --git a/src/app.py b/src/app.py index dc80215..1717642 100755 --- a/src/app.py +++ b/src/app.py @@ -1,105 +1,84 @@ import logging import time +from pprint import pprint from slackclient import SlackClient from utils.log_manager import setup_logging -# from src.creds import TOKEN, PROXY from decouple import config import traceback +from src.help_menu import HELP_MENU_RESPONSES +from src.messages import HELP_MENU, MESSAGE, needs_greet_button, greeted_response_attachments + logger = logging.getLogger(__name__) new_event_logger = logging.getLogger(f'{__name__}.new_member') all_event_logger = logging.getLogger(f'{__name__}.all_events') # constants -MESSAGE = ( - "Hi {real_name},\n\n Welcome to Operation Code! I'm a bot designed to help answer questions and get you on your way in our community.\n\n" - "Please take a moment to review our \n\n" - "Our goal here at Operation Code is to get veterans and their families started on the path to a career in programming. " - "We do that through providing you with scholarships, mentoring, career development opportunities, conference tickets, and more!\n\n" - "You're currently in Slack, a chat application that serves as the hub of Operation Code. " - "If you're currently visiting us via your browser, Slack provides a stand alone program to make staying in touch even more convenient. " - "You can download it \n\n" - "Want to make your first change to a program right now? " - "All active Operation Code Projects are located on our source control repository. " - "Our projects can be viewed on ") - -MESSAGE_JSON = { - "text": "Hi {real_name},\n\n Welcome to Operation Code! I'm a bot designed to help answer questions and get you " - "on your way in our community.\n\nPlease take a moment to review our " - "\n\nOur goal here at Operation Code is to get " - "veterans and their families started on the path to a career in programming. We do that through providing " - "you with scholarships, mentoring, career development opportunities, conference tickets, " - "and more!\n\nYou're currently in Slack, a chat application that serves as the hub of Operation Code. If " - "you're currently visiting us via your browser, Slack provides a stand alone program to make staying in " - "touch even more convenient. You can download it \n\nWant to make your " - "first change to a program right now? All active Operation Code Projects are located on our source " - "control repository. Our projects can be viewed on ", - "attachments": [ - { - "text": "", - "fallback": "", - "color": "#3AA3E3", - "attachment_type": "default", - "actions": [ - { - "name": "CoC", - "text": "Code of Conduct", - "type": "button", - "value": "Coc", - "url": "https://op.co.de/code-of-conduct" - }, - { - "name": "slack", - "text": "Download Slack", - "type": "button", - "value": "slack", - "url": "https://slack.com/downloads" - - }, - { - "name": "repo", - "text": "Op-Code Github", - "type": "button", - "value": "repo", - "url": "https://github.com/OperationCode/START_HERE" - } - ] - } - ] -} - PROXY = config('PROXY') + TOKEN = config('PERSONAL_APP_TOKEN') -COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') +COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') + +# TOKEN = config('OPCODE_APP_TOKEN') +# COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') PROXY = PROXY if PROXY else None slack_client = SlackClient(TOKEN, proxies=PROXY) -def build_message(message_template, **kwargs): +def build_message(message_template: str, **kwargs: dict) -> str: return message_template.format(**kwargs) -def event_handler(event_dict): +def event_handler(event_dict: dict) -> None: # all_event_logger.info(event_dict) - if event_dict['type'] == 'team_join': - new_event_logger.info('New member event recieved') - new_member(event_dict) + # if event_dict['type'] == 'team_join': + # new_event_logger.info('New member event recieved') + # new_member(event_dict) - if event_dict['type'] == 'presence_change': - # all_event_logger.info('User {} changed state to {}'.format(user_name_from_id(event_dict['user']), event_dict['presence'])) - pass - # can be used for development to trigger the event instead of the team_join - if event_dict['type'] == 'message' and 'user' in event_dict.keys(): - pass - # Will need to be removed. Currently for testing - # logger.info('Message event') - if event_dict['type'] == 'message' and 'user' in event_dict.keys() and event_dict['text'] == 'test4611': + """ Trigger for testing team_join event """ + if event_dict['type'] == 'message' and 'user' in event_dict.keys() and event_dict['text'] == 'testgreet': event_dict['user'] = {'id': event_dict['user']} new_member(event_dict) -def new_member(event_dict): +def help_menu_interaction(data: dict) -> None: + params = {'text': ' \n\n\n' + HELP_MENU_RESPONSES[data['actions'][0]['value']], + 'channel': data['channel']['id'], + 'ts': data['message_ts'], + 'as_user': True + } + slack_client.api_call('chat.update', **params) + + +def greeted_interaction(data: dict) -> dict: + """ + Handles the interactive message sent to the #community channel + when a new member joins + """ + if data['actions'][0]['value'] == 'greeted': + clicker = data['user']['id'] + params = {'text': data['original_message']['text'], + "attachments": greeted_response_attachments(clicker), + 'channel': data['channel']['id'], + 'ts': data['message_ts'], + 'as_user': True + } + res = slack_client.api_call("chat.update", **params) + # TODO Do something with this return value + return res + elif data['actions'][0]['value'] == 'reset_greet': + params = {'text': data['original_message']['text'], + "attachments": needs_greet_button(), + 'channel': data['channel']['id'], + 'ts': data['message_ts'], + 'as_user': True + } + res = slack_client.api_call("chat.update", **params) + + +# TODO return something to flask app +def new_member(event_dict: dict) -> None: new_event_logger.info('Recieved json event: {}'.format(event_dict)) user_id = event_dict['user']['id'] @@ -109,19 +88,22 @@ def new_member(event_dict): real_name = user_name_from_id(user_id) custom_message = MESSAGE.format(real_name=real_name) - MESSAGE_JSON['text'] = custom_message new_event_logger.info('Built message: {}'.format(custom_message)) - response = slack_client.api_call('chat.postEphemeral', + response = slack_client.api_call('chat.postMessage', channel=user_id, - user=user_id, as_user=True, - username="New Bot Name", - **MESSAGE_JSON) + text=custom_message) + + r2 = slack_client.api_call('chat.postMessage', + channel=user_id, + as_user=True, + **HELP_MENU) # Notify #community - # slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, - # text=f"New Member -- <@{user_id}>. Automated greeting sent.") + text = f":tada: <@{user_id}> has joined the Slack team :tada:" + slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, + text=text, attachments=needs_greet_button()) if response['ok']: new_event_logger.info('New Member Slack response: {}'.format(response)) @@ -129,19 +111,22 @@ def new_member(event_dict): new_event_logger.error('FAILED -- Message to new member returned error: {}'.format(response)) -def parse_slack_output(slack_rtm_output): +def parse_slack_output(slack_rtm_output: list) -> None: """ - The Slack Real Time Messaging API is an events firehose. - This parsing function returns None unless a message - is directed at the Bot, based on its ID. + Method for parsing slack events when using the RTM API instead + of the Events/App APIs """ for output in slack_rtm_output: # process a single item in list at a time event_handler(output) -def user_name_from_id(user_id): - # get detailed user info +def user_name_from_id(user_id: str) -> str: + """ + Queries the Slack workspace for the users real name + to personalize messages. Prioritizes real_name -> name -> 'New Member' + :param user_id: + """ response = slack_client.api_call('users.info', user=user_id) if response['user']['real_name']: @@ -158,7 +143,7 @@ def join_channels(): # set the defalt to a 1 second delay -def run_bot(delay=1): +def run_bot(delay: int = 1): setup_logging() if slack_client.rtm_connect(): print(f"StarterBot connected and running with a {delay} second delay") diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py index 506907d..cf38574 100644 --- a/src/flask_endpoint.py +++ b/src/flask_endpoint.py @@ -1,32 +1,68 @@ -from flask import Flask, request, Response +from flask import Flask, request, make_response from decouple import config import json -from src import app as bot from pprint import pprint +from src import app as bot from utils.log_manager import setup_logging app = Flask(__name__) +# VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') -@app.route('/', methods=['POST']) +@app.route("/user_interaction", methods=['POST']) +def interaction(): + """ + Receives requests from Slack's interactive messages + """ + + data = json.loads(request.form['payload']) + if data['token'] != VERIFICATION_TOKEN: + print("Bad request") + return make_response("", 403) + callback = data['callback_id'] + + if callback == 'greeting_buttons': + bot.help_menu_interaction(data) + elif callback == 'greeted': + bot.greeted_interaction(data) + return make_response('', 200) + + +@app.route('/options_load', methods=['POST']) +def options_load(): + """ + Can provide dynamically created options for interactive messages. + Currently unused. + """ + return make_response('', 404) + + +@app.route('/event_endpoint', methods=['POST']) def challenge(): - pprint(request.get_json()) + """ + Endpoint for all subscribed events + """ + # pprint(request.get_json()) payload = {} data = request.get_json() if data['token'] != VERIFICATION_TOKEN: print("Bad request") - return '' + return make_response("", 403) if data['type'] == 'url_verification': payload['challenge'] = data['challenge'] + return make_response(json.dumps(payload), 200) else: bot.event_handler(data['event']) - print(payload) - return json.dumps(payload) + return make_response('', 200) -if __name__ == '__main__': +def start_server(): setup_logging() app.run(debug=True) + + +if __name__ == '__main__': + start_server() diff --git a/src/help_menu.py b/src/help_menu.py new file mode 100644 index 0000000..7dd8cc1 --- /dev/null +++ b/src/help_menu.py @@ -0,0 +1,46 @@ +HELP_MENU_RESPONSES = { + 'slack': """Slack is an online chatroom service that the Operation Code community uses. +It can be accessed online, via https://operation-code.slack.com/ or via +desktop or mobile apps, located at https://slack.com/downloads/. In addition to +chatting, Slack also allows us to share files, audio conference and even program +our own bots! Here are some tips to get you started: + - You can customize your notifications per channel by clicking the gear to the + left of the search box + - Join as many channels as you want via the + next to Channels in the side bar.""", + + 'python_help': """Python is a widely used high-level programming language used for general-purpose programming. +It's very friendly for beginners and is great for everything from web development to +data science. + +Here are some python resources: + Operation Code Python Room: <#C04D6M3JT|python> + Python's official site: https://www.python.org/ + Learn Python The Hard Way: https://learnpythonthehardway.org/book/ + Automate The Boring Stuff: https://automatetheboringstuff.com/""", + 'mentor': """The Operation Code mentorship program aims to pair you with an experienced developer in order to further your programming or career goals. When you sign up for our mentorship program you'll fill out a form with your interests. You'll then be paired up with an available mentor that best meets those interests. + +If you're interested in getting paired with a mentor, please fill out our sign up form here: http://op.co.de/mentor-request. + """, + + 'js_help': """Javascript is a high-level programming language used for general-purpose programming. +In recent years it has exploded in popularity and with the popular node.js runtime +environment it can run anywhere from the browser to a server. + +Here are some javascript resources: + Operation Code Javascript Room: <#C04CJ8H2S|javascript> + Javascript Koans: https://github.com/mrdavidlaing/javascript-koans + Eloquent Javascript: http://eloquentjavascript.net/ + Node School: http://nodeschool.io/ + Node University: http://node.university/courses""", + 'ruby_help': """Ruby is one of the most popular languages to learn as a beginner. +While it can be used in any situation it's most popular for it's +web framework 'Rails' which allows people to build websites quickly +and easily. + +Here are some ruby resources: + Operation Code Ruby Room: <#C04D6GTGT|ruby> + Try Ruby Online: http://tryruby.org/ + Learn Ruby The Hard Way: http://ruby.learncodethehardway.org/book + Learn To Program: http://pine.fm/LearnToProgram/ + Ruby Koans: http://rubykoans.com/""" +} diff --git a/src/message.json b/src/message.json deleted file mode 100644 index b23e2cf..0000000 --- a/src/message.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "text": "placeholder!!!", - "attachments": [ - { - "text": "Read our code of conduct!" - } - ] -} \ No newline at end of file diff --git a/src/messages.py b/src/messages.py new file mode 100644 index 0000000..bba92d5 --- /dev/null +++ b/src/messages.py @@ -0,0 +1,99 @@ +MESSAGE = ( + "Hi {real_name},\n\n Welcome to Operation Code! I'm a bot designed to help answer questions and get you on your way in our community.\n\n" + "Please take a moment to review our \n\n" + "Our goal here at Operation Code is to get veterans and their families started on the path to a career in programming. " + "We do that through providing you with scholarships, mentoring, career development opportunities, conference tickets, and more!\n\n" + "You're currently in Slack, a chat application that serves as the hub of Operation Code. " + "If you're currently visiting us via your browser, Slack provides a stand alone program to make staying in touch even more convenient. " + "You can download it \n\n" + "Want to make your first change to a program right now? " + "All active Operation Code Projects are located on our source control repository. " + "Our projects can be viewed on ") + +HELP_MENU = { + "text": "Click any of the buttons below to receive more information on the topic.", + "attachments": [ + { + "text": "", + "fallback": "", + "color": "#3AA3E3", + "callback_id": "greeting_buttons", + "attachment_type": "default", + "actions": [ + { + "name": "mentor", + "text": "I need a mentor!", + "type": "button", + "value": "mentor", + + }, + { + "name": "slack", + "text": "Slack Info", + "type": "button", + "value": "slack", + + }, + + { + "name": "javascript", + "text": "JavaScript", + "type": "button", + "value": "js_help", + }, + { + "name": "python", + "text": "Python", + "type": "button", + "value": "python_help", + }, + { + "name": "ruby", + "text": "Ruby", + "type": "button", + "value": "ruby_help", + }, + ] + } + ] +} + + +def greeted_response_attachments(clicker: str) -> list: + return [ + { + "text": f":100:<@{clicker}> has greeted the new user!:100:", + "fallback": "", + "color": "#3AA3E3", + "callback_id": "greeted", + "attachment_type": "default", + "actions": [{ + "name": "reset_greet", + "text": "I lied! I can't greet them!", + "type": "button", + "style": "danger", + "value": "reset_greet", + }] + }, + ] + + +def needs_greet_button() -> list: + return [ + { + 'text': "", + "fallback": "Someone should greet them!", + "color": "#3AA3E3", + "callback_id": "greeted", + "attachment_type": "default", + "actions": [ + { + "name": "greeted", + "text": "I will greet them!", + "type": "button", + "style": "primary", + "value": "greeted", + }, + ] + } + ] diff --git a/utils/general_utils.py b/utils/general_utils.py index bfa58da..f475872 100644 --- a/utils/general_utils.py +++ b/utils/general_utils.py @@ -7,9 +7,9 @@ def list_channels(): - channels_call = slack_client.api_call("channels.list") + channels_call = slack_client.api_call("groups.list") if channels_call.get('ok'): - return channels_call['channels'] + return channels_call['groups'] return None @@ -37,12 +37,11 @@ def send_message(channel_id, message): for channel in channels: print(channel['name'] + " (" + channel['id'] + ")") detailed_info = channel_info(channel['id']) - # if detailed_info: - # print('Latest text from ' + channel['name'] + ":") - # print(detailed_info['latest']['text']) - if channel['name'] == 'general': - send_message(channel['id'], "Hello " + - channel['name'] + "! It worked!") + if detailed_info: + if detailed_info['members']: + print([x for x in detailed_info['members']]) + # print('Latest text from ' + channel['name'] + ":") + # print(detailed_info['latest']['text']) print('-----') else: print("Unable to authenticate.") From 99c92afa38a9bc851c14c0eee31d73c462a31a3d Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Wed, 13 Dec 2017 22:14:51 -0600 Subject: [PATCH 4/7] added docstrings, cleaned up code a bit --- src/app.py | 54 ++++++++++++++++++++++++++++++------------ src/flask_endpoint.py | 9 +++---- utils/general_utils.py | 24 +++++++++---------- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/app.py b/src/app.py index 1717642..4a95427 100755 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,5 @@ import logging import time -from pprint import pprint from slackclient import SlackClient from utils.log_manager import setup_logging from decouple import config @@ -16,21 +15,30 @@ # constants PROXY = config('PROXY') -TOKEN = config('PERSONAL_APP_TOKEN') -COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') +# TOKEN = config('PERSONAL_APP_TOKEN') +# COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') -# TOKEN = config('OPCODE_APP_TOKEN') +TOKEN = config('OPCODE_APP_TOKEN') +# TOKEN = config('TOKEN') # COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') +COMMUNITY_CHANNEL = config('OPCODE_REWRITE_CHANNEL') +PROJECTS_CHANNEL = config('OPCODE_OC_PROJECTS_CHANNEL') PROXY = PROXY if PROXY else None slack_client = SlackClient(TOKEN, proxies=PROXY) +# TODO: Do something with all of the return values here + def build_message(message_template: str, **kwargs: dict) -> str: return message_template.format(**kwargs) def event_handler(event_dict: dict) -> None: + """ + Handles routing all of the received subscribed events to the correct method + :param event_dict: + """ # all_event_logger.info(event_dict) # if event_dict['type'] == 'team_join': # new_event_logger.info('New member event recieved') @@ -43,6 +51,11 @@ def event_handler(event_dict: dict) -> None: def help_menu_interaction(data: dict) -> None: + """ + Receives help menu selection from the user and dynamically updates + displayed message + :param data: + """ params = {'text': ' \n\n\n' + HELP_MENU_RESPONSES[data['actions'][0]['value']], 'channel': data['channel']['id'], 'ts': data['message_ts'], @@ -54,7 +67,10 @@ def help_menu_interaction(data: dict) -> None: def greeted_interaction(data: dict) -> dict: """ Handles the interactive message sent to the #community channel - when a new member joins + when a new member joins. + + Displays the user that claimed the greeting along with the option + to un-claim """ if data['actions'][0]['value'] == 'greeted': clicker = data['user']['id'] @@ -65,7 +81,6 @@ def greeted_interaction(data: dict) -> dict: 'as_user': True } res = slack_client.api_call("chat.update", **params) - # TODO Do something with this return value return res elif data['actions'][0]['value'] == 'reset_greet': params = {'text': data['original_message']['text'], @@ -77,12 +92,10 @@ def greeted_interaction(data: dict) -> dict: res = slack_client.api_call("chat.update", **params) -# TODO return something to flask app def new_member(event_dict: dict) -> None: new_event_logger.info('Recieved json event: {}'.format(event_dict)) user_id = event_dict['user']['id'] - # user_id = event_dict['user'] logging.info('team_join message') real_name = user_name_from_id(user_id) @@ -92,21 +105,23 @@ def new_member(event_dict: dict) -> None: new_event_logger.info('Built message: {}'.format(custom_message)) response = slack_client.api_call('chat.postMessage', channel=user_id, - as_user=True, + # channel=COMMUNITY_CHANNEL, # testing option + # as_user=True, text=custom_message) r2 = slack_client.api_call('chat.postMessage', channel=user_id, - as_user=True, + # channel=COMMUNITY_CHANNEL, # testing option + # as_user=True, **HELP_MENU) # Notify #community - text = f":tada: <@{user_id}> has joined the Slack team :tada:" - slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, - text=text, attachments=needs_greet_button()) + # text = f":tada: <@{user_id}> has joined the Slack team :tada:" + # slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, + # text=text, attachments=needs_greet_button()) if response['ok']: - new_event_logger.info('New Member Slack response: {}'.format(response)) + new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) else: new_event_logger.error('FAILED -- Message to new member returned error: {}'.format(response)) @@ -138,12 +153,21 @@ def user_name_from_id(user_id: str) -> str: def join_channels(): + """ + Utility function for joining channels. Move to utils? + """ response = slack_client.api_call('channels.join', name='general') print(response) # set the defalt to a 1 second delay -def run_bot(delay: int = 1): +def run_bot(delay: int=1): + """ + Runs the bot using the Slack Real Time Messaging API. + **Doesn't provide events or interactive functionality + :param delay: + :return: + """ setup_logging() if slack_client.rtm_connect(): print(f"StarterBot connected and running with a {delay} second delay") diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py index cf38574..319860c 100644 --- a/src/flask_endpoint.py +++ b/src/flask_endpoint.py @@ -1,15 +1,16 @@ from flask import Flask, request, make_response from decouple import config import json -from pprint import pprint from src import app as bot from utils.log_manager import setup_logging app = Flask(__name__) -# VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') -VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') +VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') + + +# VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') @app.route("/user_interaction", methods=['POST']) @@ -20,6 +21,7 @@ def interaction(): data = json.loads(request.form['payload']) if data['token'] != VERIFICATION_TOKEN: + # TODO Logger here print("Bad request") return make_response("", 403) callback = data['callback_id'] @@ -45,7 +47,6 @@ def challenge(): """ Endpoint for all subscribed events """ - # pprint(request.get_json()) payload = {} data = request.get_json() if data['token'] != VERIFICATION_TOKEN: diff --git a/utils/general_utils.py b/utils/general_utils.py index f475872..4fa470e 100644 --- a/utils/general_utils.py +++ b/utils/general_utils.py @@ -1,22 +1,22 @@ from slackclient import SlackClient from decouple import config -TOKEN = config('PERSONAL_APP_TOKEN') +TOKEN = config('OPCODE_APP_TOKEN') slack_client = SlackClient(TOKEN) def list_channels(): - channels_call = slack_client.api_call("groups.list") + channels_call = slack_client.api_call("channels.list") if channels_call.get('ok'): - return channels_call['groups'] + return channels_call['channels'] return None def channel_info(channel_id): - channel_info = slack_client.api_call("channels.info", channel=channel_id) - if channel_info: - return channel_info['channel'] + info = slack_client.api_call("channels.info", channel=channel_id) + if info: + return info['channel'] return None @@ -36,12 +36,12 @@ def send_message(channel_id, message): print("Channels: ") for channel in channels: print(channel['name'] + " (" + channel['id'] + ")") - detailed_info = channel_info(channel['id']) - if detailed_info: - if detailed_info['members']: - print([x for x in detailed_info['members']]) - # print('Latest text from ' + channel['name'] + ":") - # print(detailed_info['latest']['text']) + # detailed_info = channel_info(channel['id']) + # if detailed_info: + # if detailed_info['members']: + # print([x for x in detailed_info['members']]) + # print('Latest text from ' + channel['name'] + ":") + # print(detailed_info['latest']['text']) print('-----') else: print("Unable to authenticate.") From 66cf6b184a8babac0d6f8b1c1a2f33eec6d4d926 Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Thu, 14 Dec 2017 08:35:39 -0600 Subject: [PATCH 5/7] updated tests --- src/app.py | 20 +++++++++----------- tests/test_basic_functionality.py | 17 +++++++++-------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/app.py b/src/app.py index 4a95427..09c6d68 100755 --- a/src/app.py +++ b/src/app.py @@ -19,10 +19,9 @@ # COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') TOKEN = config('OPCODE_APP_TOKEN') -# TOKEN = config('TOKEN') -# COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') COMMUNITY_CHANNEL = config('OPCODE_REWRITE_CHANNEL') PROJECTS_CHANNEL = config('OPCODE_OC_PROJECTS_CHANNEL') +# COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') PROXY = PROXY if PROXY else None slack_client = SlackClient(TOKEN, proxies=PROXY) @@ -40,9 +39,9 @@ def event_handler(event_dict: dict) -> None: :param event_dict: """ # all_event_logger.info(event_dict) - # if event_dict['type'] == 'team_join': - # new_event_logger.info('New member event recieved') - # new_member(event_dict) + if event_dict['type'] == 'team_join': + new_event_logger.info('New member event recieved') + new_member(event_dict) """ Trigger for testing team_join event """ if event_dict['type'] == 'message' and 'user' in event_dict.keys() and event_dict['text'] == 'testgreet': @@ -106,7 +105,7 @@ def new_member(event_dict: dict) -> None: response = slack_client.api_call('chat.postMessage', channel=user_id, # channel=COMMUNITY_CHANNEL, # testing option - # as_user=True, + # as_user=True, # Currently not working. DM comes from my account text=custom_message) r2 = slack_client.api_call('chat.postMessage', @@ -116,9 +115,9 @@ def new_member(event_dict: dict) -> None: **HELP_MENU) # Notify #community - # text = f":tada: <@{user_id}> has joined the Slack team :tada:" - # slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, - # text=text, attachments=needs_greet_button()) + text = f":tada: <@{user_id}> has joined the Slack team :tada:" + slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, + text=text, attachments=needs_greet_button()) if response['ok']: new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) @@ -161,12 +160,11 @@ def join_channels(): # set the defalt to a 1 second delay -def run_bot(delay: int=1): +def run_bot(delay: int=1) -> None: """ Runs the bot using the Slack Real Time Messaging API. **Doesn't provide events or interactive functionality :param delay: - :return: """ setup_logging() if slack_client.rtm_connect(): diff --git a/tests/test_basic_functionality.py b/tests/test_basic_functionality.py index 79516c7..1807c39 100644 --- a/tests/test_basic_functionality.py +++ b/tests/test_basic_functionality.py @@ -4,6 +4,7 @@ import logging from src import app +from src.messages import HELP_MENU from .test_data import * @@ -72,33 +73,33 @@ def test_user_name_from_id_no_name_return_new_member(self, mock_client): @mock.patch('src.app.build_message', return_value=MESSAGE) class NewMemberTestCase(unittest.TestCase): - @mock.patch('src.app.slack_client.api_call', return_value={'ok': True, 'info': 'stuff goes here'}) def test_event_logged(self, mock_client, mock_builder, mock_username_from_id): """ Asserts messages are being logged properly when new_member is called """ with LogCapture() as capture: + message = MESSAGE.format(real_name="bob") app.new_member(NEW_MEMBER) capture.check( ('src.app.new_member', 'INFO', 'Recieved json event: {}'.format(NEW_MEMBER)), ('root', 'INFO', 'team_join message'), - ('src.app.new_member', 'INFO', 'Built message: {}'.format(MESSAGE)), + ('src.app.new_member', 'INFO', 'Built message: {}'.format(message)), ('src.app.new_member', 'INFO', - 'New Member Slack response: {}'.format({'ok': True, 'info': 'stuff goes here'})) + 'New Member Slack response: Response 1: {res} \nResponse2: {res}'.format( + res={'ok': True, 'info': 'stuff goes here'})) ) @mock.patch('src.app.slack_client') def test_slack_client_called_with_correct_params(self, mock_client, mock_builder, mock_unfi): """ - Asserts new_member calls the client api with correct params. + Asserts new_member calls the client api with correct params for help menu. """ with LogCapture() as capture: app.new_member(NEW_MEMBER) - - mock_client.api_call.assert_called_with('chat.postMessage', - channel=NEW_MEMBER['user']['id'], - text=MESSAGE, as_user=True) + mock_client.api_call.assert_any_call('chat.postMessage', + channel=NEW_MEMBER['user']['id'], + **HELP_MENU) # @mock.patch('src.app.slack_client.api_call', return_value={'ok': False, 'info': 'stuff goes here'}) From 05bd15fb90886720f4c4c26e32fce9c7e71b632a Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Thu, 14 Dec 2017 11:31:54 -0600 Subject: [PATCH 6/7] added topic suggestion option and notification --- src/app.py | 73 +++++++++++++++++++++++++++---------------- src/flask_endpoint.py | 8 +++-- src/messages.py | 36 +++++++++++++++++++-- 3 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/app.py b/src/app.py index 09c6d68..d9232a7 100755 --- a/src/app.py +++ b/src/app.py @@ -4,9 +4,11 @@ from utils.log_manager import setup_logging from decouple import config import traceback +from pprint import pprint + from src.help_menu import HELP_MENU_RESPONSES -from src.messages import HELP_MENU, MESSAGE, needs_greet_button, greeted_response_attachments +from src.messages import HELP_MENU, MESSAGE, needs_greet_button, greeted_response_attachments, SUGGESTION_MODAL logger = logging.getLogger(__name__) new_event_logger = logging.getLogger(f'{__name__}.new_member') @@ -19,9 +21,10 @@ # COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') TOKEN = config('OPCODE_APP_TOKEN') -COMMUNITY_CHANNEL = config('OPCODE_REWRITE_CHANNEL') -PROJECTS_CHANNEL = config('OPCODE_OC_PROJECTS_CHANNEL') +# COMMUNITY_CHANNEL = config('OPCODE_REWRITE_CHANNEL') +# PROJECTS_CHANNEL = config('OPCODE_OC_PROJECTS_CHANNEL') # COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') +COMMUNITY_CHANNEL = config('OPCODE_BOT_TESTING_CHANNEL') PROXY = PROXY if PROXY else None slack_client = SlackClient(TOKEN, proxies=PROXY) @@ -39,9 +42,9 @@ def event_handler(event_dict: dict) -> None: :param event_dict: """ # all_event_logger.info(event_dict) - if event_dict['type'] == 'team_join': - new_event_logger.info('New member event recieved') - new_member(event_dict) + # if event_dict['type'] == 'team_join': + # new_event_logger.info('New member event recieved') + # new_member(event_dict) """ Trigger for testing team_join event """ if event_dict['type'] == 'message' and 'user' in event_dict.keys() and event_dict['text'] == 'testgreet': @@ -55,12 +58,21 @@ def help_menu_interaction(data: dict) -> None: displayed message :param data: """ - params = {'text': ' \n\n\n' + HELP_MENU_RESPONSES[data['actions'][0]['value']], - 'channel': data['channel']['id'], - 'ts': data['message_ts'], - 'as_user': True - } - slack_client.api_call('chat.update', **params) + + response = data['actions'][0]['value'] + + if response == 'suggestion': + trigger_id = data['trigger_id'] + res = slack_client.api_call('dialog.open', trigger_id=trigger_id, dialog=SUGGESTION_MODAL) + pprint(res) + + else: + params = {'text': ' \n\n\n' + HELP_MENU_RESPONSES[data['actions'][0]['value']], + 'channel': data['channel']['id'], + 'ts': data['message_ts'], + 'as_user': True + } + slack_client.api_call('chat.update', **params) def greeted_interaction(data: dict) -> dict: @@ -91,6 +103,13 @@ def greeted_interaction(data: dict) -> dict: res = slack_client.api_call("chat.update", **params) +def suggestion_submission(data): + suggestion = data['submission']['suggestion'] + user_id = data['user']['id'] + message = f"<@{user_id}> just submitted a suggestion for a help topic:\n{suggestion}" + res = slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, text=message) + + def new_member(event_dict: dict) -> None: new_event_logger.info('Recieved json event: {}'.format(event_dict)) @@ -102,15 +121,15 @@ def new_member(event_dict: dict) -> None: custom_message = MESSAGE.format(real_name=real_name) new_event_logger.info('Built message: {}'.format(custom_message)) - response = slack_client.api_call('chat.postMessage', - channel=user_id, - # channel=COMMUNITY_CHANNEL, # testing option - # as_user=True, # Currently not working. DM comes from my account - text=custom_message) + # response = slack_client.api_call('chat.postMessage', + # # channel=user_id, + # channel=COMMUNITY_CHANNEL, # testing option + # # as_user=True, # Currently not working. DM comes from my account + # text=custom_message) r2 = slack_client.api_call('chat.postMessage', - channel=user_id, - # channel=COMMUNITY_CHANNEL, # testing option + # channel=user_id, + channel=COMMUNITY_CHANNEL, # testing option # as_user=True, **HELP_MENU) @@ -118,11 +137,11 @@ def new_member(event_dict: dict) -> None: text = f":tada: <@{user_id}> has joined the Slack team :tada:" slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, text=text, attachments=needs_greet_button()) - - if response['ok']: - new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) - else: - new_event_logger.error('FAILED -- Message to new member returned error: {}'.format(response)) + # + # if response['ok']: + # new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) + # else: + # new_event_logger.error('FAILED -- Message to new member returned error: {}'.format(response)) def parse_slack_output(slack_rtm_output: list) -> None: @@ -157,10 +176,10 @@ def join_channels(): """ response = slack_client.api_call('channels.join', name='general') print(response) +# set the defalt to a 1 second delay -# set the defalt to a 1 second delay -def run_bot(delay: int=1) -> None: +def run_bot(delay: int = 1) -> None: """ Runs the bot using the Slack Real Time Messaging API. **Doesn't provide events or interactive functionality @@ -183,4 +202,4 @@ def run_bot(delay: int=1) -> None: if __name__ == '__main__': - run_bot() + run_bot() \ No newline at end of file diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py index 319860c..4416416 100644 --- a/src/flask_endpoint.py +++ b/src/flask_endpoint.py @@ -1,3 +1,5 @@ +from pprint import pprint + from flask import Flask, request, make_response from decouple import config import json @@ -8,8 +10,6 @@ app = Flask(__name__) VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') - - # VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') @@ -25,11 +25,15 @@ def interaction(): print("Bad request") return make_response("", 403) callback = data['callback_id'] + pprint(data['user']) if callback == 'greeting_buttons': bot.help_menu_interaction(data) elif callback == 'greeted': bot.greeted_interaction(data) + elif callback == 'suggestion_modal': + pprint(data) + bot.suggestion_submission(data) return make_response('', 200) diff --git a/src/messages.py b/src/messages.py index bba92d5..acea6db 100644 --- a/src/messages.py +++ b/src/messages.py @@ -8,10 +8,12 @@ "You can download it \n\n" "Want to make your first change to a program right now? " "All active Operation Code Projects are located on our source control repository. " - "Our projects can be viewed on ") + "Our projects can be viewed on \n\n" + "Click any of the buttons below to receive more information on the topic.\n\n" + "If you'd like to see something that isn't here let us know!") HELP_MENU = { - "text": "Click any of the buttons below to receive more information on the topic.", + "text": "", "attachments": [ { "text": "", @@ -53,11 +55,41 @@ "type": "button", "value": "ruby_help", }, + + ] + }, + { + "text": "", + "fallback": "", + "color": "#3AA3E3", + "callback_id": "greeting_buttons", + "attachment_type": "default", + "actions": [ + { + "name": "suggestion", + "text": "Are we missing something? Click!", + "type": "button", + "value": "suggestion", + }, ] } ] } +SUGGESTION_MODAL = { + "callback_id": "suggestion_modal", + "title": "Help topic suggestion", + "submit_label": "Suggestion", + "elements": [ + { + "type": "text", + "label": "Suggestion", + "name": "suggestion", + "placeholder": "Underwater Basket Weaving" + }, + ] +} + def greeted_response_attachments(clicker: str) -> list: return [ From 8ee71f26437d2f65258d9e4185758e14939a2646 Mon Sep 17 00:00:00 2001 From: AllenAnthes Date: Fri, 15 Dec 2017 09:35:09 -0600 Subject: [PATCH 7/7] prepping for merge and deployment --- .gitignore | 1 + src/airtable_handling.py | 52 ++++++++++++ src/app.py | 119 +++++++++++++++++++------- src/flask_endpoint.py | 11 ++- src/messages.py | 137 +++++++++++++++++++++++++++++- tests/test_basic_functionality.py | 14 +-- utils/log_manager.py | 9 +- 7 files changed, 298 insertions(+), 45 deletions(-) create mode 100644 src/airtable_handling.py diff --git a/.gitignore b/.gitignore index abcec5e..5a821b5 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ credentials.json creds.py + # remove pycharm ide files .idea/ diff --git a/src/airtable_handling.py b/src/airtable_handling.py new file mode 100644 index 0000000..96de643 --- /dev/null +++ b/src/airtable_handling.py @@ -0,0 +1,52 @@ +import json +from pprint import pprint + +import requests +from airtable import Airtable +from decouple import config + +BASE_KEY = config('PERSONAL_AIRTABLE_BASE_KEY') +API_KEY = config('PERSONAL_AIRTABLE_TOKEN') +TABLE_NAME = 'Mentor Request' + + + +def get_table(table): + airtable = Airtable(BASE_KEY, table, api_key=API_KEY) + res = airtable.get_all() + pprint(res) + + + + +def test(): + params = { + 'fields': { + 'Slack User': 'Allen2', + 'Email': 'email@email.com', + 'Service': ['recBxmDasLXwmVB78'], + 'Skillsets': ["Web (Frontend Development)"], + 'Additional Details': 'Details'} + } + + headers = { + 'authorization': "Bearer " + API_KEY + } + res = requests.post("https://api.airtable.com/v0/app2p4KgQKx35WoCm/Mentor%20Request", json=params, + headers=headers) + print(res.content) + + +if __name__ == '__main__': + # test() + get_table('Services') + + +services_records = { + 'General Guidance - Slack Chat': 'recBxmDasLXwmVB78', + 'General Guidance - Voice Chat': 'recDyu4PMbPl7Ti58', + 'Pair Programming': 'recHCFAO9uNSy1WDs', + 'Code Review': 'recUK55xJXOfAaYNb', + 'Resume Review': 'recXZzUduWfaxWvSF', + 'Mock Interview': 'recdY4XLeN1CPz1l8' +} diff --git a/src/app.py b/src/app.py index d9232a7..e9d5339 100755 --- a/src/app.py +++ b/src/app.py @@ -1,3 +1,4 @@ +import json import logging import time from slackclient import SlackClient @@ -5,28 +6,34 @@ from decouple import config import traceback from pprint import pprint - +import requests from src.help_menu import HELP_MENU_RESPONSES -from src.messages import HELP_MENU, MESSAGE, needs_greet_button, greeted_response_attachments, SUGGESTION_MODAL +from src.messages import * + +# from src.airtable_handling import airtable logger = logging.getLogger(__name__) new_event_logger = logging.getLogger(f'{__name__}.new_member') all_event_logger = logging.getLogger(f'{__name__}.all_events') # constants -PROXY = config('PROXY') +PROXY = config('PROXY', default=None) -# TOKEN = config('PERSONAL_APP_TOKEN') -# COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') +TOKEN = config('PERSONAL_APP_TOKEN') +COMMUNITY_CHANNEL = config('PERSONAL_PRIVATE_CHANNEL') -TOKEN = config('OPCODE_APP_TOKEN') +# TOKEN = config('OPCODE_APP_TOKEN') # COMMUNITY_CHANNEL = config('OPCODE_REWRITE_CHANNEL') # PROJECTS_CHANNEL = config('OPCODE_OC_PROJECTS_CHANNEL') # COMMUNITY_CHANNEL = config('OPCODE_COMMUNITY_ID') -COMMUNITY_CHANNEL = config('OPCODE_BOT_TESTING_CHANNEL') +# COMMUNITY_CHANNEL = config('OPCODE_BOT_TESTING_CHANNEL') + +"""Airtable configs""" +AIRTABLE_BASE_KEY = config('PERSONAL_AIRTABLE_BASE_KEY') +AIRTABLE_API_KEY = config('PERSONAL_AIRTABLE_TOKEN') +AIRTABLE_TABLE_NAME = 'Mentor Request' -PROXY = PROXY if PROXY else None slack_client = SlackClient(TOKEN, proxies=PROXY) @@ -41,10 +48,10 @@ def event_handler(event_dict: dict) -> None: Handles routing all of the received subscribed events to the correct method :param event_dict: """ - # all_event_logger.info(event_dict) - # if event_dict['type'] == 'team_join': - # new_event_logger.info('New member event recieved') - # new_member(event_dict) + all_event_logger.info(event_dict) + if event_dict['type'] == 'team_join': + new_event_logger.info('New member event recieved') + new_member(event_dict) """ Trigger for testing team_join event """ if event_dict['type'] == 'message' and 'user' in event_dict.keys() and event_dict['text'] == 'testgreet': @@ -64,10 +71,15 @@ def help_menu_interaction(data: dict) -> None: if response == 'suggestion': trigger_id = data['trigger_id'] res = slack_client.api_call('dialog.open', trigger_id=trigger_id, dialog=SUGGESTION_MODAL) - pprint(res) + + # Disabled while airtable integration is still in development + # elif response == 'mentor': + # trigger_id = data['trigger_id'] + # res = slack_client.api_call('dialog.open', trigger_id=trigger_id, dialog=MENTOR_REQUEST_MODAL) + # pprint(res) else: - params = {'text': ' \n\n\n' + HELP_MENU_RESPONSES[data['actions'][0]['value']], + params = {'text': HELP_MENU_RESPONSES[data['actions'][0]['value']], 'channel': data['channel']['id'], 'ts': data['message_ts'], 'as_user': True @@ -103,14 +115,60 @@ def greeted_interaction(data: dict) -> dict: res = slack_client.api_call("chat.update", **params) -def suggestion_submission(data): +def suggestion_submission(data: dict) -> None: + """ + Receives the event when a user submits a suggestion for a new help topic and + posts it to the #community channel + :param data: + """ suggestion = data['submission']['suggestion'] user_id = data['user']['id'] - message = f"<@{user_id}> just submitted a suggestion for a help topic:\n{suggestion}" + message = f":exclamation:<@{user_id}> just submitted a suggestion for a help topic:exclamation:\n-- {suggestion}" res = slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, text=message) +def mentor_submission(data): + """ + Parses the mentor request dialog form and pushes the data to Airtable. + :param data: + :return: + """ + + # Temporary hack. Change this to getting the record ID's from the table itself + services_records = { + 'General Guidance - Slack Chat': 'recBxmDasLXwmVB78', + 'General Guidance - Voice Chat': 'recDyu4PMbPl7Ti58', + 'Pair Programming': 'recHCFAO9uNSy1WDs', + 'Code Review': 'recUK55xJXOfAaYNb', + 'Resume Review': 'recXZzUduWfaxWvSF', + 'Mock Interview': 'recdY4XLeN1CPz1l8' + } + + form = data['submission'] + params = { + 'fields': { + 'Slack User': form['Slack User'], + 'Email': form['Email'], + 'Service': [services_records[form['service']]], + 'Skillsets': [form['skillset']], + 'Additional Details': form['Additional Details'] + } + } + + headers = { + 'authorization': "Bearer " + AIRTABLE_API_KEY + } + res = requests.post(f"https://api.airtable.com/v0/{AIRTABLE_BASE_KEY}/{AIRTABLE_TABLE_NAME}", json=params, + headers=headers) + + def new_member(event_dict: dict) -> None: + """ + Invoked when a new user joins and a team_join event is received. + DMs the new user with the welcome message and help menu as well as pings + the #community channel with a new member notification + :param event_dict: + """ new_event_logger.info('Recieved json event: {}'.format(event_dict)) user_id = event_dict['user']['id'] @@ -121,27 +179,27 @@ def new_member(event_dict: dict) -> None: custom_message = MESSAGE.format(real_name=real_name) new_event_logger.info('Built message: {}'.format(custom_message)) - # response = slack_client.api_call('chat.postMessage', - # # channel=user_id, - # channel=COMMUNITY_CHANNEL, # testing option - # # as_user=True, # Currently not working. DM comes from my account - # text=custom_message) + response = slack_client.api_call('chat.postMessage', + channel=user_id, + # channel=COMMUNITY_CHANNEL, # testing option + as_user=True, # Currently not working. DM comes from my account + text=custom_message) r2 = slack_client.api_call('chat.postMessage', - # channel=user_id, - channel=COMMUNITY_CHANNEL, # testing option - # as_user=True, + channel=user_id, + # channel=COMMUNITY_CHANNEL, # testing option + as_user=True, **HELP_MENU) # Notify #community text = f":tada: <@{user_id}> has joined the Slack team :tada:" slack_client.api_call('chat.postMessage', channel=COMMUNITY_CHANNEL, text=text, attachments=needs_greet_button()) - # - # if response['ok']: - # new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) - # else: - # new_event_logger.error('FAILED -- Message to new member returned error: {}'.format(response)) + + if response['ok'] and r2['ok']: + new_event_logger.info('New Member Slack response: Response 1: {} \nResponse2: {}'.format(response, r2)) + else: + new_event_logger.error('FAILED -- Message to new member returned error: {}\n{}'.format(response, r2)) def parse_slack_output(slack_rtm_output: list) -> None: @@ -176,7 +234,6 @@ def join_channels(): """ response = slack_client.api_call('channels.join', name='general') print(response) -# set the defalt to a 1 second delay def run_bot(delay: int = 1) -> None: @@ -202,4 +259,4 @@ def run_bot(delay: int = 1) -> None: if __name__ == '__main__': - run_bot() \ No newline at end of file + run_bot() diff --git a/src/flask_endpoint.py b/src/flask_endpoint.py index 4416416..2f0b3f6 100644 --- a/src/flask_endpoint.py +++ b/src/flask_endpoint.py @@ -9,8 +9,8 @@ app = Flask(__name__) -VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') -# VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') +# VERIFICATION_TOKEN = config('OPCODE_VERIFICATION_TOKEN') +VERIFICATION_TOKEN = config('APP_VERIFICATION_TOKEN') @app.route("/user_interaction", methods=['POST']) @@ -24,6 +24,7 @@ def interaction(): # TODO Logger here print("Bad request") return make_response("", 403) + callback = data['callback_id'] pprint(data['user']) @@ -34,6 +35,9 @@ def interaction(): elif callback == 'suggestion_modal': pprint(data) bot.suggestion_submission(data) + elif callback == 'mentor_request': + # pprint(data) + bot.mentor_submission(data) return make_response('', 200) @@ -53,6 +57,9 @@ def challenge(): """ payload = {} data = request.get_json() + + pprint(data) + if data['token'] != VERIFICATION_TOKEN: print("Bad request") return make_response("", 403) diff --git a/src/messages.py b/src/messages.py index acea6db..44a8087 100644 --- a/src/messages.py +++ b/src/messages.py @@ -10,10 +10,11 @@ "All active Operation Code Projects are located on our source control repository. " "Our projects can be viewed on \n\n" "Click any of the buttons below to receive more information on the topic.\n\n" - "If you'd like to see something that isn't here let us know!") + "If you'd like to see something that isn't here let us know!\n\n" + "------------------------------------------------------------------------------------------") HELP_MENU = { - "text": "", + "text": "Click a button below and info will show up here!", "attachments": [ { "text": "", @@ -79,7 +80,7 @@ SUGGESTION_MODAL = { "callback_id": "suggestion_modal", "title": "Help topic suggestion", - "submit_label": "Suggestion", + "submit_label": "Submit", "elements": [ { "type": "text", @@ -90,6 +91,136 @@ ] } +MENTOR_REQUEST_MODAL = { + "callback_id": "mentor_request", + "title": "Mentor Service Request", + "submit_label": "Submit", + "elements": [ + { + "value": "test", + "type": "text", + "label": "Slack User Name", + "name": "Slack User", + "placeholder": "" + }, + { + "value": "test@test.com", + "type": "text", + "subtype": "email", + "label": "Email", + "name": "Email", + "placeholder": "" + }, + { + + "type": "select", + "label": "Service Type", + "name": "service", + "placeholder": "Choose a service type", + "options": [ + { + "label": "General Guidance - Voice Chat", + "value": "General Guidance - Voice Chat" + }, + { + "label": "General Guidance - Slack Chat", + "value": "General Guidance - Slack Chat" + }, + { + "label": "Pair Programming", + "value": "Pair Programming" + }, + { + "label": "Code Review", + "value": "Code Review" + }, + { + "label": "Mock Interview", + "value": "Mock Interview" + }, + { + "label": "Resume Review", + "value": "Resume Review" + }, + + ] + }, + { + "type": "select", + "label": "Mentor Skillset", + "name": "skillset", + "optional": "true", + "placeholder": "Choose a service type", + "options": [ + { + "label": "Web (Frontend Development)", + "value": "Web (Frontend Development)", + }, + { + "label": "Web (Backend Development)", + "value": "Web (Backend Development)", + }, + { + "label": "Mobile (Android)", + "value": "Mobile (Android)", + }, + { + "label": "Mobile (iOS)", + "value": "Mobile (iOS)", + }, + { + "label": "C / C++", + "value": "C / C++", + }, + { + "label": "C# / .NET", + "value": "C# / .NET", + }, + { + "label": "Data Science", + "value": "Data Science", + }, + { + "label": "DevOps", + "value": "DevOps", + }, + { + "label": "Design / UX", + "value": "Design / UX" + }, + { + "label": "Java", + "value": "Java", + }, + { + "label": "Javascript", + "value": "Javascript", + }, + { + "label": "Python", + "value": "Python", + }, + { + "label": "Ruby / Rails", + "value": "Ruby / Rails", + }, + { + "label": "SQL", + "value": "SQL", + }, + ] + }, + { + "type": "textarea", + "label": "Additional Details", + "name": "Additional Details", + "optional": "true", + "placeholder": "Please provide us with any more info that may help in us in assigning a mentor to this " + "request. " + }, + ] +} + def greeted_response_attachments(clicker: str) -> list: return [ diff --git a/tests/test_basic_functionality.py b/tests/test_basic_functionality.py index 1807c39..b1f621a 100644 --- a/tests/test_basic_functionality.py +++ b/tests/test_basic_functionality.py @@ -4,8 +4,8 @@ import logging from src import app -from src.messages import HELP_MENU -from .test_data import * +from src.messages import HELP_MENU, MESSAGE +from .test_data import NEW_MEMBER, USER_INFO_HAS_REAL_NAME, USER_INFO_NO_NAME, USER_INFO_HAS_NAME class EventHandlerTestCase(unittest.TestCase): @@ -99,6 +99,7 @@ def test_slack_client_called_with_correct_params(self, mock_client, mock_builder app.new_member(NEW_MEMBER) mock_client.api_call.assert_any_call('chat.postMessage', channel=NEW_MEMBER['user']['id'], + as_user=True, **HELP_MENU) # @@ -111,14 +112,17 @@ def test_slack_client_returns_error(self, mock_builder, mock_unfi, mock_client): app.new_member(USER_INFO_HAS_REAL_NAME) capture.check( ('src.app.new_member', 'ERROR', - "FAILED -- Message to new member returned error: {'ok': False, 'info': 'stuff goes here'}")) + "FAILED -- Message to new member returned error: {res}\n{res}".format( + res={'ok': False, 'info': 'stuff goes here'}))) class BuildMessageTestCase(unittest.TestCase): - def test_build_message(self): """ Asserts build_message function correctly formats message. """ - message = app.build_message(MESSAGE, real_name='Bob') + params = { + 'real_name': 'Bob' + } + message = app.build_message(MESSAGE, **params) self.assertEquals(message, MESSAGE.format(real_name='Bob')) diff --git a/utils/log_manager.py b/utils/log_manager.py index 011c44b..cea0959 100644 --- a/utils/log_manager.py +++ b/utils/log_manager.py @@ -1,16 +1,17 @@ import json import logging.config import os +import decouple logger = logging.getLogger(__name__) new_event_logger = logging.getLogger(f'{__name__}.new_member') # https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/ -def setup_logging(default_path='log_config.json', - default_level=logging.INFO - ): - file_path = os.path.join(os.path.dirname(__file__), default_path) +def setup_logging(default_level=logging.INFO): + + logging_config = decouple.config('LOGGING_CONFIG', default='log_config.json') + file_path = os.path.join(os.path.dirname(__file__), logging_config) if os.path.exists(file_path): with open(file_path, 'rt') as f: