From: Carl Worth Date: Thu, 22 Oct 2020 07:50:09 +0000 (-0700) Subject: Defer all sheet creation until after the channel is created X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=b9fc94a0e63e3eea4e06b869d7c771357714178c;hp=9b570c69c799bc2442d712507474c803f7a571bf;p=turbot Defer all sheet creation until after the channel is created Specifically, we no longer attempt to do sheet creation in the handler for the interaction of the user submitting a modal dialog. The reason that was a a bad idea is that the sheet creation can take several seconds, (including time to copy data from a template sheet, etc.), while Slack will timeout after merely 3 seconds of a user submission and report an error. This way, we can promptly validate the user's input (giving them a clean error on the form if necessary), then simply create the channel in the handler and primomptly return (so Slack doesn't timeout). Then, separately, we now have a listener setup for the channel_created event and in _that_ handler we do all the sheet creation necessary. We have all the time we want here since channel_created is just an event without a user waiting for a response, so Slack doesn't impose a 3-second timeout on us. --- diff --git a/turbot/events.py b/turbot/events.py index 32d061a..26c8945 100644 --- a/turbot/events.py +++ b/turbot/events.py @@ -1,6 +1,15 @@ from turbot.blocks import ( section_block, text_block, button_block, actions_block ) +import turbot.sheets +import turbot.slack + +TURBOT_USER_ID = 'U01B9QM4P9R' + +events = {} + +lambda_success = {'statusCode': 200} +lambda_error = {'statusCode': 400} def hunt_block(hunt): name = hunt['name'] @@ -15,10 +24,9 @@ def hunt_block(hunt): return section_block(text_block(text)) -def home(turb, user_id, body): +def home(turb, user_id): """Returns a view to be published as the turbot home tab for user_id - The body argument is a dictionary as provided by the Slack request. The return value is a dictionary suitable to be published to the Slack views_publish API.""" @@ -38,12 +46,198 @@ def home(turb, user_id, body): ] } -def app_home_opened(turb, body): - user_id = body['event']['user'] - view = home(turb, user_id, body) +def app_home_opened(turb, event): + """Handler for the app_home_opened event + + This event occurs when a user visits the home tab for the turbot app. + In response to this event we need to publish a view for the user.""" + + user_id = event['user'] + view = home(turb, user_id) turb.slack_client.views_publish(user_id=user_id, view=view) - return "OK" + return lambda_success + +events['app_home_opened'] = app_home_opened + +def hunt_channel_created(turb, channel_name, channel_id): + """Creates sheet and a DynamoDB table for a newly-created hunt channel""" + + # First see if we can find an entry for this hunt in the database. + # If not, simply return an error and let Slack retry + hunts_table = turb.db.Table("hunts") + response = hunts_table.get_item( + Key={'channel_id': channel_id}, + ConsistentRead=True + ) + if 'Item' not in response: + print("Warning: Cannot find channel_id {} in hunts table. " + .format(channel_id) + "Letting Slack retry this event") + return lambda_error + + item = response['Item'] + + if 'sheet_url' in item: + print("Info: channel_id {} already has sheet_url {}. Exiting." + .format(channel_id, item['sheet_url'])) + return lambda_success + + # Remove any None items from our item before updating + if not item['url']: + del item['url'] + + # Before launching into sheet creation, indicate that we're doing this + # in the database. This way, if we take too long to create the sheet + # and Slack retries the event, that next event will see this 'pending' + # string and cleanly return (eliminating all future retries). + item['sheet_url'] = 'pending' + hunts_table.put_item(Item=item) + + # Also, let the channel users know what we are up to + turb.slack_client.chat_postMessage( + channel=channel_id, + text="Welcome to the channel for the {} hunt! ".format(item['name']) + + "Please wait a minute or two while I create some backend resources.") + + # Create a sheet for the channel + sheet = turbot.sheets.sheets_create(turb, channel_name) + + # Update the database with the URL of the sheet + item['sheet_url'] = sheet['url'] + hunts_table.put_item(Item=item) + + # Message the channel with the URL of the sheet + turb.slack_client.chat_postMessage(channel=channel_id, + text="Sheet created for this hunt: {}" + .format(sheet['url'])) + + # Create a database table for this hunt's puzzles + table = turb.db.create_table( + TableName=channel_name, + KeySchema=[ + {'AttributeName': 'channel_id', 'KeyType': 'HASH'} + ], + AttributeDefinitions=[ + {'AttributeName': 'channel_id', 'AttributeType': 'S'} + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + ) + + # Wait until the table exists + table.meta.client.get_waiter('table_exists').wait(TableName=channel_name) + + # Mark the hunt as active in the database + item['active'] = True + hunts_table.put_item(Item=item) + + # Message the hunt channel that the database is ready + turb.slack_client.chat_postMessage( + channel=channel_id, + text="Thank you for waiting. This hunt is now ready to begin! " + + "Use `/puzzle` to create puzzles for the hunt.") + + return lambda_success + +def puzzle_channel_created(turb, puzzle_channel_name, puzzle_channel_id): + """Creates sheet and invites user for a newly-created puzzle channel""" + + hunt_id = puzzle_channel_name.split('-')[0] + + # First see if we can find an entry for this puzzle in the database. + # If not, simply return an error and let Slack retry + puzzle_table = turb.db.Table(hunt_id) + response = puzzle_table.get_item( + Key={'channel_id': puzzle_channel_id}, + ConsistentRead=True + ) + if 'Item' not in response: + print("Warning: Cannot find channel_id {} in {} table. " + .format(puzzle_channel_id, hunt_id) + + "Letting Slack retry this event") + return lambda_error + + item = response['Item'] + + if 'sheet_url' in item: + print("Info: channel_id {} already has sheet_url {}. Exiting." + .format(puzzle_channel_id, item['sheet_url'])) + return lambda_success + + # Remove any None items from our item before updating + if not item['url']: + del item['url'] + + # Before launching into sheet creation, indicate that we're doing this + # in the database. This way, if we take too long to create the sheet + # and Slack retries the event, that next event will see this 'pending' + # string and cleanly return (eliminating all future retries). + item['sheet_url'] = 'pending' + puzzle_table.put_item(Item=item) + + # Create a sheet for the puzzle + sheet = turbot.sheets.sheets_create_for_puzzle(turb, puzzle_channel_name) + + # Update the database with the URL of the sheet + item['sheet_url'] = sheet['url'] + puzzle_table.put_item(Item=item) + + # Message the channel with the URL of the puzzle's sheet + turb.slack_client.chat_postMessage(channel=puzzle_channel_id, + text="Sheet created for this puzzle: {}" + .format(sheet['url'])) + + hunts_table = turb.db.Table('hunts') + response = hunts_table.scan( + FilterExpression='hunt_id = :hunt_id', + ExpressionAttributeValues={':hunt_id': hunt_id}, + ) + + if 'Items' in response: + + hunt_channel_id=response['Items'][0]['channel_id'] + + # Find all members of the hunt channel + members = turbot.slack.slack_channel_members(turb.slack_client, + hunt_channel_id) + + # Filter out Turbot's own ID to avoid inviting itself + members = [m for m in members if m != TURBOT_USER_ID] + + turb.slack_client.chat_postMessage( + channel=puzzle_channel_id, + text="Inviting all members from the hunt channel: {}" + .format(hunt_id)) + + # Invite those members to the puzzle channel (in chunks of 500) + cursor = 0 + while cursor < len(members): + turb.slack_client.conversations_invite( + channel=puzzle_channel_id, + users=members[cursor:cursor + 500]) + cursor += 500 + + return lambda_success + +def channel_created(turb, event): + print("In channel_created with event: {}".format(str(event))) + + channel = event['channel'] + channel_id = channel['id'] + channel_name = channel['name'] + creator = channel['creator'] + + # Ignore any channels that turbot didn't create + if creator != TURBOT_USER_ID: + print("channel_created: Not a turbot-created channel. Exiting.") + return lambda_success + + # The presence of a hyphen determines whether this is a puzzle + # channel or a hunt channel. + if '-' in channel_name: + return puzzle_channel_created(turb, channel_name, channel_id) + else: + return hunt_channel_created(turb, channel_name, channel_id) -events = { - "app_home_opened": app_home_opened -} +events['channel_created'] = channel_created diff --git a/turbot/interaction.py b/turbot/interaction.py index f634cbc..8c1304a 100644 --- a/turbot/interaction.py +++ b/turbot/interaction.py @@ -8,8 +8,6 @@ import re import requests from botocore.exceptions import ClientError -TURBOT_USER_ID = 'U01B9QM4P9R' - actions = {} commands = {} submission_handlers = {} @@ -86,6 +84,7 @@ def new_hunt_submission(turb, payload, metadata): function above.""" state = payload['view']['state']['values'] + user_id = payload['user']['id'] name = state['name']['name']['value'] hunt_id = state['hunt_id']['hunt_id']['value'] url = state['url']['url']['value'] @@ -131,12 +130,6 @@ def new_hunt_submission(turb, payload, metadata): "Error creating Slack channel: {}" .format(e.response['error'])) - if not response['ok']: - return submission_error("name", - "Error occurred creating Slack channel " - + "(see CloudWatch log") - - user_id = payload['user']['id'] channel_id = response['channel']['id'] # Create a sheet for the channel @@ -145,46 +138,21 @@ def new_hunt_submission(turb, payload, metadata): channel_id = response['channel']['id'] # Insert the newly-created hunt into the database + # (leaving it as non-active for now until the channel-created handler + # finishes fixing it up with a sheet and a companion table) hunts_table.put_item( Item={ 'channel_id': channel_id, - "active": True, + "active": False, "name": name, "hunt_id": hunt_id, - "url": url, - "sheet_url": sheet['url'] + "url": url } ) # Invite the initiating user to the channel turb.slack_client.conversations_invite(channel=channel_id, users=user_id) - # Message the channel with the URL of the sheet - turb.slack_client.chat_postMessage(channel=channel_id, - text="Sheet created for this hunt: {}" - .format(sheet['url'])) - - # Create a database table for this hunt's puzzles - table = turb.db.create_table( - TableName=hunt_id, - AttributeDefinitions=[ - {'AttributeName': 'channel_id', 'AttributeType': 'S'} - ], - KeySchema=[ - {'AttributeName': 'channel_id', 'KeyType': 'HASH'} - ], - ProvisionedThroughput={ - 'ReadCapacityUnits': 5, - 'WriteCapacityUnits': 4 - } - ) - - # Message the hunt channel that the database is ready - turb.slack_client.chat_postMessage( - channel=channel_id, - text="Welcome to your new hunt! " - + "Use `/puzzle` to create puzzles for the hunt.") - return { 'statusCode': 200, } @@ -298,9 +266,6 @@ def puzzle_submission(turb, payload, metadata): This is the modal view presented to the user by the puzzle function above.""" - print("In puzzle_submission\npayload is: {}\nmetadata is {}" - .format(payload, metadata)) - meta = json.loads(metadata) hunt_id = meta['hunt_id'] hunt_channel_id = meta['hunt_channel_id'] @@ -329,12 +294,8 @@ def puzzle_submission(turb, payload, metadata): puzzle_channel_id = response['channel']['id'] - # Create a sheet for the puzzle - sheet = turbot.sheets.sheets_create_for_puzzle(turb, hunt_dash_channel) - # Insert the newly-created puzzle into the database table = turb.db.Table(hunt_id) - table.put_item( Item={ "channel_id": puzzle_channel_id, @@ -343,32 +304,9 @@ def puzzle_submission(turb, payload, metadata): "name": name, "puzzle_id": puzzle_id, "url": url, - "sheet_url": sheet['url'] } ) - # Find all members of the hunt channel - members = turbot.slack.slack_channel_members(turb.slack_client, - hunt_channel_id) - - # Filter out Turbot's own ID to avoid inviting itself - members = [m for m in members if m != TURBOT_USER_ID] - - turb.slack_client.chat_postMessage(channel=puzzle_channel_id, - text="Inviting members: {}".format(str(members))) - - # Invite those members to the puzzle channel (in chunks of 500) - cursor = 0 - while cursor < len(members): - turb.slack_client.conversations_invite( - channel=puzzle_channel_id, - users=members[cursor:cursor + 500]) - cursor += 500 - - # Message the channel with the URL of the puzzle's sheet - turb.slack_client.chat_postMessage(channel=puzzle_channel_id, - text="Sheet created for this puzzle: {}" - .format(sheet['url'])) return { 'statusCode': 200 } diff --git a/turbot_lambda/turbot_lambda.py b/turbot_lambda/turbot_lambda.py index 7469222..e19e96d 100644 --- a/turbot_lambda/turbot_lambda.py +++ b/turbot_lambda/turbot_lambda.py @@ -124,10 +124,11 @@ def url_verification_handler(turb, body): } def event_callback_handler(turb, body): - type = body['event']['type'] + event = body['event'] + type = event['type'] if type in turbot.events.events: - return turbot.events.events[type](turb, body) + return turbot.events.events[type](turb, event) return error("Unknown event type: {}".format(type)) def turbot_interactive_or_slash_command(turb, event, context):