Slack --APIGateway --Lambda (Python) --How to make a RedShift interactive app

Overview

Create a Slack application as the title says. If you write it properly, it will be a long sentence, so only the following points will be emphasized.

--Notes on Slack application --Application settings on Slack side (around permissions) --About Slack's HomeView (Home tab) and attachment --Implementation and library usage around Lambda (python) --APIGateWay settings

If you just want to try it simply, you can set a WebHook URL, schedule Lambda, etc. and POST it, but I'm assuming a scene where you can send a message to an interactive request using the SlackClient library.

Rather than making something that works if you follow the procedure Please be aware that it is troublesome to look up, and I will introduce the points that I was unexpectedly addicted to in fragments.

Things to keep in mind when creating a Slack application

Response is returned once within 3 seconds

When clicking the button and returning a response, an error will occur if it exceeds 3 seconds.

This is a process that returns HTTP code 200 for the time being, After that, it can be easily avoided by performing processing asynchronously and notifying the channel of the result again.

I won't go into detail in this article, Actually, the response_url of the request parameter is valid for several minutes or more even after 3 seconds have passed. It is possible to POST directly to the response_url while preparing for a timeout.

A JSON assembly called Block Kit is required to decorate the UI (home tab, form) and messages.

You can try it with Block Kit Builder, but it's very troublesome.

At a minimum, understand channel and user IDs

The user ID is in PayLoad of requestBody. Note that it is not a user name. The channel ID is also in PayLoad of requestBody, but it may not come in with a specific pattern. You may want to notify another channel, so it's a good idea to embed the target ID with a constant.

The channel ID can be easily identified from the address when the target channel is opened with a browser.

/app.slack.com/client/ //details/top

Message decoration markup does not support tables

There is no help for what you can't do. I forcibly shaped it using dividers, quote markup and space filling. ..

Application settings on Slack side (around permissions)

Only where it's okay to set this much. Surprisingly limited. I will not use WebHookURL this time, but I will set it for the time being.

After setting [Settings] -> [Installed App Settings] -> [(Re)Install to WorkSpace] Don't forget to reflect on.

Display information

[Settings] -> [Basic Infomation] -> [Display Information] App name, description, logo, etc. Appropriately.

WebHookURL [Features] -> [Incoming Webhooks]

--Turn on Activate Incoming Webhooks --Add with [Add New Webhook to Workspace] in [Webhook URLs for Your Workspace]

Authentication and authorization

[Settings] -> [OAuth & Permissions]

[Bot User OAuth Access Token] Make a note of the value here as it will be needed to send messages etc. from lambda

[Scope] -> [Bot Token Scopes] What is essential I think it's about chat: write, chat: write.customize, channels: history. If you use WebHook, you can also use incoming-webhook. I also put app_mentions: read, commands, reactions: read and so on.

Event activation and request URL setting

[Features] -> [Event Subscriptions]

After creating the API and setting up the Gateway (the method will be described later) Finally, turn on [Enable Events] here and enter various settings.

By setting the URL etc. of the API created here Make sure that the request flies when you actually perform a specific action.

Request (API) URL settings

I can't do it at this stage, so I'll set it later. Even if you enter an appropriate URL temporarily, it will be NG by authentication. At the very least, authentication will not pass unless challenge response is implemented in the processing on the API side.

Please be aware that it is the place to put the URL of the API at the end.

Setting which event to hook

Set in [Subscribe to bot events].

For the time being, app_home_opened for the UI display of the home tab It's almost OK if you add message.channels to take the button event from the message.

Regarding UI and message decoration of Slack application

Below is a simple example and image. The implementation image in the code will be described later together with the usage sample of SlackClient API.

Home tab

スクリーンショット 2021-01-06 14.58.00.png

HomeTabSample.json


{
	"type": "home",
	"blocks": [
		{
			"type": "header",
			"text": {
				"type": "plain_text",
				"text": "This is a super simple app test."
			}
		},
		{
			"type": "divider"
		},
		{
			"type": "actions",
			"elements": [
				{
					"type": "button",
					"text": {
						"type": "plain_text",
						"text": "Show current time"
					},
					"style": "primary",
					"value": "click_from_home_tab"
				}
			]
		}
	]
}

Message with button

スクリーンショット 2021-01-06 15.07.28.png

MessageSample.json


{
	"blocks": [
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "Hello, it is a sample application!"
			}
		},
		{
			"type": "actions",
			"elements": [
				{
					"type": "button",
					"text": {
						"type": "plain_text",
						"emoji": true,
						"text": "Tell me the current time"
					},
					"style": "primary",
					"value": "click_from_message_button"
				}
			]
		}
	]
}

Build the actual API with lambda (Python)

Introducing only the points. By the way, since the log related to API Gateway is enough, it is not set in particular on the lambda side.

Required configuration (for asynchronous implementation)

Slack application (mentioned above) ↓↑ AWS API Gateway (described later) ↓↑ lambda slack-request.py API (explained here)

Since there is a three-second rule, the process is delegated to the asynchronous lambda and the response is returned once. Most of the payload analysis etc. is done here ↓↑ lambda async-notice.py API (explained here) Process with all request details and attribute information received and notify Slack of the result This time, an example of schedule execution is also here

Preparation of necessary libraries (Layer setting)

The part that is sober and difficult. It takes several hours if you get hooked. I make a lambda every time I forget it, so He said he forgot to wrap it in a python folder, uploaded it, and was confused because it didn't work.

Well, upload this and add it to both lambda layers. I feel like I only need slack-client (please google for how to make it).

I don't need it this time, but if you want to do various things, it's a good idea to include the latest versions of django and pandas. In the trial version, all you need is the above slack layer. スクリーンショット 2021-01-06 15.41.02.png

Implementation point: Request processing lambda

** Challenge-response implementation ** At a minimum, the request URL should include the following implementation at the beginning. Without it, you will not be authenticated in the application settings.

slack-request.py


    if "challenge" in event:
        return event["challenge"]

** Notifications are made using the Slack Client API **

Slack Client generation


#Bot User OAuth Access Token value in application settings
import slack
client = slack.WebClient(token="xoxb-XXXXXXXXXX....")

** Home tab UI drawing ** Specify in JSON

Draw the home tab


    """
Displaying the Home Tab menu
    """
    if ('event' in event):
        slack_event = event['event']
        if (slack_event['type'] == 'app_home_opened') :
            if ('tab' in slack_event) and (slack_event['tab'] == 'home'):
                user = slack_event['user']
                channel = slack_event['channel']
                blocks = <Here is JSON>
                views = {"type": "home", "blocks": blocks}
                client.views_publish(user_id=user, view=views)
                return {'statusCode': 200}

Home tab JSON (repost)

Home tab JSON


{
	"type": "home",
	"blocks": [
		{
			"type": "header",
			"text": {
				"type": "plain_text",
				"text": "This is a super simple app test."
			}
		},
		{
			"type": "divider"
		},
		{
			"type": "actions",
			"elements": [
				{
					"type": "button",
					"text": {
						"type": "plain_text",
						"text": "Show current time"
					},
					"style": "primary",
					"value": "click_from_home_tab"
				}
			]
		}
	]
}

** Event handle (handling home button and interactive message button) ** Strictly speaking, it's better to sort them out a little more, but it still works.

The biggest point is ** Request asynchronously to another lambda and return the response that received the request first. ** **

Also, in the sample, you may not want to return a response to other Slack users at each notification. The message notification is set to "Only you can see" Ephemeral.

slack-request.py


    """
Hook Action, distribute processing, and return Response to Slack
ChannelId is fixed and invariant, so use a constant instead of from the request
    """
    if ('body-json' in event):
        
        #Extract the contents of payload
        action_message = urllib.parse.unquote(event['body-json'].split('=')[1])
        action_message = json.loads(action_message)

        #Get the user ID to be notified
        user_id = action_message['user']['id']

        # Message Button ==Determine if it is an Interactive Message
		isInteractiveMessage = False
        if('type' in action_message ) and (action_message['type'] == 'interactive_message'):
            isInteractiveMessage = True

        #Hook the event if there is action
        if ('actions' in action_message):

            """
Get value and request notification asynchronously to another lambda
There may be multiple asynchronous requests, but requests are mainly judged only in Primary Mode.
            """
            delegateLambda_primaryMode = []
            
            #Extract only the action message part from the payload
            action_message = action_message['actions'][0]
            #Since it is sent asynchronously, set the Default of the synchronous response message first.
            send_message = "The request has been accepted. please wait a moment."
            
            if (action_message['type'] == 'button') :

                if (action_message['value'] == 'click_from_home_tab'):
                    """
Home tab button clicked
                    """
                    delegateLambda_primaryMode.append('click_from_home_tab')
					
                elif (action_message['value'] == 'click_from_message_button'):
                    """
Home tab button clicked
                    """
                    delegateLambda_primaryMode.append('click_from_message_button')

                
                """
Make the set asynchronous request
In addition to Primary and Secondary, user_I made it a specification to pass id
Asynchronous processing will be described later
                """
                for p in delegateLambda_primaryMode:
                    input_event = {
                        "primaryMode": p,
                        "secondaryMode": "now no mean",
                        "user_id": user_id
                    }
                    lambdaMediator.callNortifyAsync(json.dumps(input_event))
                
                """
Return the response message once
                """
                if isSendEphemeral == True:
                    response = client.chat_postEphemeral(
                        channel=notice_channel_id,
                        text=send_message,
                        user=user_id
                    )
                else:
                    response = client.chat_postMessage(
                        channel=notice_channel_id,
                        text=send_message
                    )

    if isInteractiveMessage == True:
        pass
    else:
        return {'statusCode': 200}

lambdaMediator.py


import boto3

def callNortifyAsync(payload):
    """
Asynchronously delegated lambda(async-notice.py fixed)Call

    Parameters
    ----------
    event :Event json of delegation destination lambda
        PrimaryMode:First Key character string representing the request content
        SecondaryMode:Second Key character string representing the request content
        UserId:Requester's Slack user ID

    Returns
    ----------
    response
    """
    
    response = boto3.client('lambda').invoke(
        FunctionName='async-notice',
        InvocationType='Event', 
        Payload=payload
    )
    return response

Implementation point: Asynchronous processing lambda

You don't have to use attachment, but if you want to do something elaborate The sample does that because it's done with attachments rather than messages.

This time, AWS Lambda_FullAccess is given to the execution Role of both lambdas. Also, since it is a sample, I got PrimaryMode from slack-request.py, but I did not include a branch.

** Send a message using attachment **

async-notice.py


import json
import datetime
import time
import boto3
import slack

from libs import mylib
from libs import slack_info

def lambda_handler(event, context):
    """
This lambda is slack-Delegate processing from request (asynchronous), and kicked only from scheduler
    
    Parameters
    ----------
    event : 
        PrimaryMode:First Key character string representing the request content
        SecondaryMode:Second Key character string representing the request content (unused: for expansion)
        UserId:Requester's Slack user ID
        
    Returns
    ----------
    httpResponseBody : statusCode:200 , body ->Just return 200 to send from SlackClient
    
    ToDo
    ----------

    """
    
	#Get user ID
    user_id = ""
    if 'user_id' in event:
        user_id = event['user_id']
		
    """
No parameter is started from the scheduler, so general notification
Ephemeral because it is an asynchronous request with parameters(I can only see you)notification
    """
	isEphemeral = True
    if ('primaryMode' in event) == False:
        isEphemeral = False
        
    """
Send a message notification
attachments is message JSON
    """
    postDataMessage(attachments, False, isEphemeral, user_id, "Put the text message here.")

    return {
        'statusCode': 200,
        'body': json.dumps('OK')
    }
    
    
def postDataMessage(attachment_data, isSendMultiple=False, isEphemeral=True, userId="", textMessage=""):
    """
Send a message
    
    Parameters
    ----------
    attachment :Attachment data
    isSendMultiple :Whether there are multiple attachment data
    isEphemeral :Whether to send so that only the requesting user can see it
    userId :Request user ID
    textMessage :Text message

    Returns
    ----------

    ToDo
    ----------

    """
    
    client = slack.WebClient(token="xoxb-XXXXXXXXX.....")

    #Rewrap in a list even if it is a single transmission because it supports multiple transmissions
    if isSendMultiple == False:
        attachment_data = [attachment_data]
    
	# TARGET_CHANNEL_For the ID, set the channel ID of the notification destination separately.
    for attachment in attachment_data:
        if isEphemeral == True:
            response = client.chat_postEphemeral(
                channel=TARGET_CHANNEL_ID,
                attachments = attachment,
                user=userId
            ) 
        else:
            response = client.chat_postMessage(
                text=textMessage,
                channel=TARGET_CHANNEL_ID,
                attachments = attachment
            )
JSON (repost) of message with button

json example of attachment


{
	"type": "home",
	"blocks": [
		{
			"type": "header",
			"text": {
				"type": "plain_text",
				"text": "This is a super simple app test."
			}
		},
		{
			"type": "divider"
		},
		{
			"type": "actions",
			"elements": [
				{
					"type": "button",
					"text": {
						"type": "plain_text",
						"text": "Show current time"
					},
					"style": "primary",
					"value": "click_from_home_tab"
				}
			]
		}
	]
}

Bonus: If you want to execute the schedule

Just enter the async-notice lambda and cron settings from your browser when creating rules for Amazon Event Bridge.

Runs from Monday to Friday at 9am (because it's GMT time)-Set in 9 hours)


cron(0 0 ? * MON-FRI *)

image.png

Bonus: Connect to RedShift

Just specify ** redshift-data ** in boto3 Data access is very easy because it is performed with role authority within the Redshift account specified when executing the query.

Sample code

RedShift connection


import json
import time
import boto3
        
#Redshift connection information
CLUSTER_NAME='Enter the cluster name'
DATABASE_NAME='Enter the database name'
DB_USER='Enter your username'

def getDateTimeSample():
    """
Get time
    """

    sql = '''
		select getdate();
	'''
    return _getData(sql)

def _getData(sql):
    """
Issue a query to RedShift and return the result as it is
    
    Parameters
    ----------
    String :sql statement

    Returns
    ----------
    statement :The acquisition result of boto3 is as it is
    """
    
    #Submit a query to Redshift. It's asynchronous so it will come back soon
    data_client = boto3.client('redshift-data')
    result = data_client.execute_statement(
        ClusterIdentifier=CLUSTER_NAME,
        Database=DATABASE_NAME,
        DbUser=DB_USER,
        Sql=sql,
    )

    #Get execution ID
    id = result['Id']

    #Wait for the query to finish
    statement = ''
    status = ''
    while status != 'FINISHED' and status != 'FAILED' and status != 'ABORTED':
        statement = data_client.describe_statement(Id=id)
        #print(statement)
        status = statement['Status']
        time.sleep(1)

    #View results
    if status == 'FINISHED':
        if int(statement['ResultSize']) > 0:
            #If it is a select statement etc., the return value is displayed
            statement = data_client.get_statement_result(Id=id)
        else:
            #If there is no return value, only FINISH is output and the process ends.
            print('QUERY FINSHED')
    elif status == 'FAILED':
        #At the time of failure
        print('QUERY FAILED\n{}'.format(statement))
    elif status == 'ABORTED':
        #When stopped by the user
        print('QUERY ABORTED: The query run was stopped by the user.') 
    
    return statement 

API GateWay settings

Give the created lambda API a URL so that you can access it.

Rough procedure

** Created with REST API ** image.png

** Integrate lambda ** image.png

** Add application/x-www-form-urlencoded in mapping template ** The most important. In the open event of the home tab, it comes in this format, so it will not work unless you enter here. Also, since some other requests come in application/json, it is also a point to add them together.

image.png

** All you have to do is deploy the stage ** image.png

** Log settings ** This setting is sufficient for debugging. The log is output to CloudWatch as a log group with the stage name. image.png

Other points / notice

Three-second rule is tough

Well, given the nature of Slack, it's understandable, but sometimes it's fancy.

Perform processing including database access asynchronously Even if you simply return response 200, there are rare cases where it is not in time considering the cold start of lambda.

Lambda also has Provisioned Concurrency, which can also be used in some cases [https://aws.amazon.com/jp/lambda/pricing/].

I want a table for markup

I think there is a lot of demand. If you use a quote, the half-width space is faithfully reproduced in the quote, so if you use this and the division line, it will look like a table. However, the feeling cannot be wiped out by force.

Recommended Posts

Slack --APIGateway --Lambda (Python) --How to make a RedShift interactive app
How to make a slack bot
[Python] How to make a class iterable
Tweet in Chama Slack Bot ~ How to make a Slack Bot using AWS Lambda ~
How to make a Python package using VS Code
How to use Python lambda
[Python] How to make a list of character strings character by character
How to make a multiplayer online action game on Slack
How to launch AWS Batch from a python client app
How to make a Python package (written for an intern)
How to make a Japanese-English translation
How to write a Python class
How to make a crawler --Advanced
[Python] Make the function a lambda function
How to make a deadman's switch
[Blender] How to make a Blender plugin
How to make a crawler --Basic
How to make a string into an array or an array into a string in Python
How to make a surveillance camera (Security Camera) with Opencv and Python
[Python] How to make a matrix of repeating patterns (repmat / tile)
[Python] How to convert a 2D list to a 1D list
Send a message from Python to Slack
[Python] How to invert a character string
How to get a stacktrace in python
Make Qt for Python app a desktop app
How to make a Backtrader custom indicator
How to make a Pelican site map
Make a desktop app with Python with Electron
How to run a Maya Python script
How to generate a new loggroup in CloudWatch using python within Lambda
How to get a value from a parameter store in lambda (using python)
Send a message from Slack to a Python server
How to make a dialogue system dedicated to beginners
How to read a CSV file with Python 2/3
How to create a Python virtual environment (venv)
How to open a web browser from python
How to embed a variable in a python string
How to develop a cart app with Django
How to create a JSON file in Python
How to make a dictionary with a hierarchical structure.
I want to make a game with Python
How to generate a Python object from JSON
Try to make a "cryptanalysis" cipher with Python
How to add a Python module search path
How to make a QGIS plugin (package generation)
Sample to send slack notification with python lambda
How to notify a Discord channel in Python
I read "How to make a hacking lab"
[Blender x Python] How to make an animation
Try to make a dihedral group with Python
[AWS / Lambda] How to load Python external library
How to create a multi-platform app with kivy
How to make Substance Painter Python plugin (Introduction)
[Python] How to draw a histogram in Matplotlib
[Blender x Python] How to make vertex animation
How to make Python faster for beginners [numpy]
How to make Python Interpreter changes in Pycharm
[Python] Throw a message to the slack channel
Lark Basics (Python, Lark to make a shell-like guy)
Create a setting in terraform to send a message from AWS Lambda Python3.8 to Slack
A new form of app that works with GitHub: How to make GitHub Apps