使用AWS Lambda函数的自定义命令为您的Slack工作空间添彩
了解将Slack自定义命令连接到AWS Lambda函数的步骤,从Trello板获取数据,并最终将其作为响应送回Slack。
我们决定让我们的Slack频道更具吸引力。在我们的团队中,大部分与工作相关的即时通讯都是在其他渠道(WhatsApp、Jabber)完成的,而我们的目标是将这些互动迁移到Slack。我们如何促进这一变化呢?
Slack允许创建外部应用程序,这是一个巨大的好处。我们利用自定义命令的功能,使Slack更加有趣,而一点幽默感总是给人以动力。
本文介绍了将Slack的自定义命令连接到AWS Lambda函数的步骤,从Trello boards中获取数据,并最终将其作为一个响应送回Slack。
1.创建一个AWS Lambda函数
我假设你知道如何建立和配置AWS Lambda函数。你也可以阅读我之前的一篇文章。如何开始使用AWS Lambda函数或改变API网关参数。这一次,我用Python来做Lambda函数。
在定义了一个基本的Hello World函数后,我们将通过API网关(GET和POST)将其暴露出来。虽然Slack的调用只基于POST,但通过GET请求访问该函数也很有用。
#build the response
def respond(err, res=None):
if err:
logger.error(err)
return res+". Error: "+str(err)
else:
return res
# The main Lambda function method
def lambda_handler(event, context):
try:
logger.info('start '+str(event))
return respond(None, 'I received a call' )
except Exception as ex:
return respond(ex, "Ooopss.. We're not perfect")
但在调用这个初级Lambda函数之前,我们需要改变响应格式。
改变响应格式
Slack中的文本是基于Markdown格式的(在此阅读更多内容);因此,返回的字符串应该是纯文本,而不是JSON,这是Lambda函数的默认返回格式。在API网关中,在集成响应(GET和POST)下,将映射模板设置为text/plain并定义输出。
#set($inputRoot = $input.path('$'))
$inputRoot
下面的截图是这个定义的例子:
最后一步是部署API网关并测试它(如果你不确定如何做,你可以参考我的文章或使用AWS文档)。
等待一分钟!
在继续之前,有必要配置一下我们的API网关的日志级别。这样分析错误和可视化整个流程就会容易得多。在下面的例子中,阶段名称是 "prod";我把日志级别配置为INFO,并记录完整的请求/响应数据。
现在,让我们创建一个Slack命令,调用这个Lambda函数。
2.创建一个Slack命令
我将简要地介绍一下,但有一个很好的教程,它解释了如何一步步地做。
首先,创建一个新的应用程序。

- 在基本信息部分下,你会发现应用证书。它有一个验证令牌,可以从外部识别这个应用程序。这个令牌将在以后使用,所以请保留它!
- 在基本信息部分的底部是显示信息,在这里你可以为你的新应用程序添加一个标志和一个简短的描述,从而发挥创意。
- 斜线命令的定义出现在功能部分下。
- 填写细节并将URL设置为我们之前创建的API网关URL。你可以用同一个URL创建多个命令,因为命令名称是一个参数(见下一章)。
记住--对自定义命令的任何改变只有在重新安装应用程序后才会应用。
3.将Slack连接到我们的Lambda函数
现在,让我们来处理请求部分。
Slack在POST请求的正文中发送参数;安培号字符是分隔符。下面是一个经过审查的例子。
token=xuYpPjBh&
team_id=19FV3&
team_domain=domainname&
channel_id=D03EEPFS36E&
channel_name=directmessage&
user_id=U02SWEY2NUQ&
user_name=lior.k.sh
&command=%2Fchuck&
text=&
api_app_id=A03EEMBNRD0&
is_enterprise_install=false&
response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands&
trigger_id=35119694466d24a6
让我们回顾一下这个请求中的主要参数:
token:识别你的应用程序的Slack令牌(保持它的安全性!)。user_name:识别呼叫者的IDcommand:触发呼叫的命令;在这个例子中,它是 "/chuck" ,因为我们喜欢查克-诺里斯的笑话。text:命令后的文本;如果命令后没有文本,这个参数就是空的。
面对我们的第一个错误
如果我们试图按原样运行该命令,Slack可能会回复一个错误,这是一个令人讨厌的回复,因为它没有揭示出根本问题。当我试图运行自定义命令 "/chuck" 时,响应是:
/chuck failed with the error “dispatch_failed”
嗯,这不是很有信息量。明智的是,有日志。还记得我们配置了API网关的日志级别吗?现在是深入研究这些日志的时候了!
响应是错误400,意思是坏请求。日志信息表明,将请求体转化为JSON的过程失败了:
"执行失败:无法将请求主体解析为json。无法将有效载荷解析为json"。
我们的Lambda希望有效载荷的格式(又称主体)是JSON;但是,它收到的是一个以安培号(&)为界的字符串。因此,出现了一个解析错误(Bad Request 400)。
配置API网关以解析该请求
我们的下一步应该是将Slack的请求转换为JSON。API网关允许在收到请求后,在将其传递给我们的Lambda函数之前进行干预。这是在POST方法执行→IntegrationRequest→Mapping Template中完成的。
让我们定义一个新的映射模板;设置内容类型为application/x-www-form-urlencoded。
你可以复制并粘贴以下模板。它有点长,但你可以把它读完(见注释##)。
这里有一个奖励。除了原来的POST主体,我还串联了一个名为 "action "的查询字符串参数。它将被用来通过查询字符串传递额外的参数。我保证你会进一步看到。
## convert HTML POST data or HTTP GET query string to JSON
## get the raw post data from the AWS built-in variable and give it a nicer name
## Part 1: get the body content
#if ($context.httpMethod == "POST")
#set($rawAPIData = $input.path('$')+"&action="+$input.params('action'))
#elseif ($context.httpMethod == "GET")
#set($rawAPIData = $input.params().querystring)
#set($rawAPIData = $rawAPIData.toString())
#set($rawAPIDataLength = $rawAPIData.length() - 1)
#set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
#set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
#set($rawAPIData = "")
#end
## Part 2: extract the key-value pairs by parsing the &
## Check the number of "&" in the string; it tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())
## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
#set($rawPostData = $rawAPIData + "&")
#end
## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))
## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])
## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
#set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
#if ($countEquals == 1)
#set($kvTokenised = $kvPair.split("="))
## Check if the key-value pair has only key, without value.
#set($isEmpty = $kvTokenised.size()==1)
#if ($kvTokenised[0].length() > 0 && !$isEmpty)
## we found a valid key value pair. add it to the list.
#set($devNull = $tokenisedEquals.add($kvPair))
#end
#end
#end
## Part 3: Go over all the key-value pairs and construct the JSON format
{
#foreach( $kvPair in $tokenisedEquals )
## finally we output the JSON for this pair and append a comma if this isn't the last pair
#set($kvTokenised = $kvPair.split("="))
## Check if this is a pair; if yes, add it to the final JSON output "key":"value".
#if($kvTokenised[1].length() > 0)
"$util.urlDecode($kvTokenised[0])" : "$util.urlDecode($kvTokenised[1])"#if( $foreach.hasNext ),#end
#end
#end
}
最终,映射模板应该看起来像这样:

运行Slash命令(再次)
现在,当运行Slash命令时,日志显示了转换前的请求(一个带有&的字符串)和转换后的请求(JSON格式)。
一旦通过这一关,主体是基于JSON的,我们可以改变我们的Lambda函数来提取主体内容。
首先,我们需要过滤掉不是由我们的Slack应用程序进行的调用。这是一种验证调用者的方式;任何其他调用者都应抛出一个异常。下面是如何从有效载荷(POST主体)中获取Slack令牌。
def lambda_handler(event, context):
try:
token = event['token']
if os.environ['slacktoken'] != token:
return respond(Exception('Invalid request was made'))
else:
return respond(None, 'I received a call' )
# some more code....
except Exception as ex:
return respond(ex, "Ooopss.. We're not perfect")
在上面的代码示例中,实际的令牌被保存在一个环境变量中。它可以用KMS密钥进行加密,以提高安全性,但这是另一篇文章的内容。
在这一点上,我们可以运行我们的Slash命令并收到一个响应。
让我们为我们的Slash命令添加一些牛肉。
4.连接到其他API
在这一点上,当基础有了,这个Lambda服务可以连接任何API并将其传递给Slack。
正如你已经知道的,查克-诺里斯是我们的明星。下面的函数显示了从API获取查克-诺里斯笑话的实现,它以JSON格式返回一个随机笑话。
def process_chuck():
return respond(None, "%s :joy:" % (getValueFromJson('http://api.icndb.com/jokes/random', 'value','joke')))
def getValueFromJson(url, key1, key2):
content = getURLResponseJson(url)
if len(key2)>0:
return content[key1][key2]
return content[key1]
def getURLResponse(sUrl, header=None):
if header==None:
header={'Accept': 'application/json'}
res = urllib.request.urlopen(urllib.request.Request(url=sUrl,
headers=header,
method='GET'),
timeout=5)
return res
def getURLResponseJson(sUrl, header=None):
res = getURLResponse(sUrl,header)
contentStr = res.read()
return json.loads(contentStr)
这里是另一个调用API返回爸爸笑话的例子:
def process_joke(userId, command, channel, command_text):
sUrl='https://dad-jokes.p.rapidapi.com/random/joke'
header ={'Accept': 'application/json',
'X-RapidAPI-Key' :os.environ['dad']}
content = getURLResponseJson(sUrl,header)
除了笑话,这还不够。获取一些与工作有关的内容如何?
5.将Trello加入到派对中
有了一些乐趣之后,我想分享更多与工作有关的内容。由于我们的Slack应用程序没有连接到我们的网络环境,所以我们选择在其他地方保留一些信息。 Trello是这个目的的完美解决方案;它可以在任何地方访问,并且有一个广泛的API。出发点是定义一个应用密钥,然后生成一个令牌。
在直接获取其数据之前,你需要获得卡的ID。Trello有一个层次结构。Board →List Card。首先,我获取了我们董事会的所有列表。董事会的ID出现在URL上。
有了这个起点之后,我通过使用Postman运行一些查询来钻研具体的卡片。
- 获取板上的所有列表。
https://api.trello.com/1/boards/aZCi/lists?key=<myKey>&token=<myToken>
- 找到了我的列表。现在得到其中的所有卡片。
https://api.trello.com/1/lists/<listId>/cards?key=<myKey>&token=<myToken>
-
最后,我可以得到所有卡片的数据和它的所有附件。
就这样,我在Trello卡片上保存了一些方便的信息,并通过AWS Lambda服务取走了它。下面是两个从Trello卡片中获取数据的代码样本。
- 获取卡片(基于其CardID)。
def trello_getCard(cardId):
trelloAppKey=os.environ['trelloAppKey']
sUrl='https://api.trello.com/1/cards/'+cardId+'?key='+trelloAppKey+'&token='+os.environ['trello']
return getURLResponseJson(sUrl)
- 获取附件
这个动作需要两个步骤。首先,获取附件的公共URL(一个给定的卡片可以有不止一个附件),然后获取附件本身。第二个调用需要传递头中的秘密。响应是文件的内容。
def trello_getCardAttachment(cardId, index):
trelloAppKey=os.environ['trelloAppKey']
# get the attachement details to fetch its public URL
contentStr = trello_getCardAttachmentsDetails(cardId)
# read the attachemnt file path (to be accessed using OAuth)
sUrl='https://api.trello.com/1/cards/'+cardId+'/attachments/'+contentStr[index]['id']+'?key='+trelloAppKey+'&token='+os.environ['trello']
contentStr = getURLResponseJson(sUrl)
sUrl = contentStr['url']
# create a header with Oauth key
header ={'Accept': 'application/json',
'Authorization' : 'OAuth oauth_consumer_key="'+trelloAppKey+'",oauth_token="'+os.environ['trello']+'"'}
res = getURLResponse(sUrl,header)
return res
我想让Slack命令的调用更通用,所以我把Trello卡片的ID作为查询字符串参数从Slach命令请求中传递出来。你还记得添加到映射模板中的 "行动 "参数吗?这就是它的目的。它被添加到Lambda函数的输入中。
最后:让团队参与进来
下面是运行三个命令的结果:
/joke返回一个随机的爸爸笑话;它验证了一个API并获取了一个笑话。/chuck返回一个随机的Chuck Norris笑话;它执行了一个不涉及认证的API调用。/inspire返回一个从Trello卡片的附件文件中提取的随机励志名言。
正如你所看到的,我用一些表情符号给文本添加了色彩。
总结
结果如何?嗯,这个功能是最近才推出的,但团队真的很喜欢这些为我们的Slack频道带来的新鲜补充。