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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..18397fb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +sudo: false +os: + - linux + +python: + - "3.6" + +install: + - pip install -r requirements.txt + +script: + - py.test diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..8536506 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: python run.py \ No newline at end of file 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 71e9a40..824395c 100644 Binary files a/requirements.txt and b/requirements.txt differ 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/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 c71525c..e9d5339 100755 --- a/src/app.py +++ b/src/app.py @@ -1,93 +1,223 @@ +import json import logging import time from slackclient import SlackClient from utils.log_manager import setup_logging -from src.creds import TOKEN, PROXY +from decouple import config import traceback +from pprint import pprint +import requests + +from src.help_menu import HELP_MENU_RESPONSES +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 -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 ") - -PROXY = PROXY if PROXY else None +PROXY = config('PROXY', default=None) + +TOKEN = config('PERSONAL_APP_TOKEN') +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_COMMUNITY_ID') +# 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' + slack_client = SlackClient(TOKEN, proxies=PROXY) -def build_message(message_template, **kwargs): +# 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): +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) - 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'])) - - # 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(): - - # 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 help_menu_interaction(data: dict) -> None: + """ + Receives help menu selection from the user and dynamically updates + displayed message + :param data: + """ + + 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) + + # 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': 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. + + Displays the user that claimed the greeting along with the option + to un-claim + """ + 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) + 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) + + +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":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 new_member(event_dict): +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'] - # user_id = event_dict['user'] logging.info('team_join message') - custom_message = build_message(MESSAGE, - real_name=user_name_from_id(user_id)) + real_name = user_name_from_id(user_id) + custom_message = MESSAGE.format(real_name=real_name) - new_event_logger.info('Built message: {}'.format(event_dict)) + new_event_logger.info('Built message: {}'.format(custom_message)) response = slack_client.api_call('chat.postMessage', channel=user_id, - text=custom_message, - as_user=True) - - - if response['ok'] == 'true': - new_event_logger.info('New Member Slack response: {}'.format(response)) + # 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, + **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'] 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: {}'.format(response)) + new_event_logger.error('FAILED -- Message to new member returned error: {}\n{}'.format(response, r2)) -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']: @@ -97,14 +227,21 @@ def user_name_from_id(user_id): else: return 'New Member' + 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=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: + """ 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 new file mode 100644 index 0000000..2f0b3f6 --- /dev/null +++ b/src/flask_endpoint.py @@ -0,0 +1,80 @@ +from pprint import pprint + +from flask import Flask, request, make_response +from decouple import config +import json + +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("/user_interaction", methods=['POST']) +def interaction(): + """ + Receives requests from Slack's interactive messages + """ + + 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'] + 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) + elif callback == 'mentor_request': + # pprint(data) + bot.mentor_submission(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(): + """ + Endpoint for all subscribed events + """ + payload = {} + data = request.get_json() + + pprint(data) + + if data['token'] != VERIFICATION_TOKEN: + print("Bad request") + 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']) + return make_response('', 200) + + +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/messages.py b/src/messages.py new file mode 100644 index 0000000..44a8087 --- /dev/null +++ b/src/messages.py @@ -0,0 +1,262 @@ +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 \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!\n\n" + "------------------------------------------------------------------------------------------") + +HELP_MENU = { + "text": "Click a button below and info will show up here!", + "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", + }, + + ] + }, + { + "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": "Submit", + "elements": [ + { + "type": "text", + "label": "Suggestion", + "name": "suggestion", + "placeholder": "Underwater Basket Weaving" + }, + ] +} + +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 [ + { + "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/tests/test_basic_functionality.py b/tests/test_basic_functionality.py index 433ce58..b1f621a 100644 --- a/tests/test_basic_functionality.py +++ b/tests/test_basic_functionality.py @@ -4,7 +4,8 @@ import logging from src import app -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): @@ -18,14 +19,23 @@ def test_event_handler_receives_team_join_calls_new_member(self, mock_new_member app.event_handler(NEW_MEMBER) mock_new_member.assert_called_with(NEW_MEMBER) - def test_event_handler_message_event_logs_event(self): - """ - Asserts event handler correctly logs message events. - Will be removed eventually... - """ - with LogCapture() as capture: - app.event_handler(MESSAGE_EVENT) - capture.check(('src.app', 'INFO', 'Message event')) + # All events logging currently disabled + # + # def test_event_handler_message_event_logs_event(self): + # """ + # Asserts event handler correctly logs message events. + # Will be removed eventually... + # """ + # with LogCapture() as capture: + # app.event_handler(MESSAGE_EVENT) + # capture.check( + # ('src.app.all_events', + # 'INFO', + # "{'type': 'message', 'channel': 'C8DA69KM4', 'user': 'U8DG4B3EK', 'text': " + # "'.', 'ts': '1513003671.000412', 'source_team': 'T8CJ90MQV', 'team': " + # "'T8CJ90MQV'}"), + # ('src.app', 'INFO', 'Message event') + # ) @mock.patch('src.app.slack_client') @@ -63,33 +73,37 @@ 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'}) + @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(NEW_MEMBER)), + ('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. """ - app.new_member(NEW_MEMBER) - mock_client.api_call.assert_called_with('chat.postMessage', - channel=NEW_MEMBER['user']['id'], - text=MESSAGE, as_user=True) + with LogCapture() as capture: + app.new_member(NEW_MEMBER) + mock_client.api_call.assert_any_call('chat.postMessage', + channel=NEW_MEMBER['user']['id'], + as_user=True, + **HELP_MENU) # - @mock.patch('src.app.slack_client.api_call', return_value={'ok': 'false', 'info': 'stuff goes here'}) + @mock.patch('src.app.slack_client.api_call', return_value={'ok': False, 'info': 'stuff goes here'}) def test_slack_client_returns_error(self, mock_builder, mock_unfi, mock_client): """ Asserts an ERROR is logged when messaging a new member fails @@ -98,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/general_utils.py b/utils/general_utils.py new file mode 100644 index 0000000..4fa470e --- /dev/null +++ b/utils/general_utils.py @@ -0,0 +1,47 @@ +from slackclient import SlackClient +from decouple import config + +TOKEN = config('OPCODE_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): + info = slack_client.api_call("channels.info", channel=channel_id) + if info: + return 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: + # 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.") 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: