使用AWS Lambda函数的自定义命令为Slack工作区添彩的教程及实例

326 阅读10分钟

使用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命令

我将简要地介绍一下,但有一个很好的教程,它解释了如何一步步地做

首先,创建一个新的应用程序

  1. 在基本信息部分下,你会发现应用证书。它有一个验证令牌,可以从外部识别这个应用程序。这个令牌将在以后使用,所以请保留它!
  2. 在基本信息部分的底部是显示信息,在这里你可以为你的新应用程序添加一个标志和一个简短的描述,从而发挥创意。
  3. 斜线命令的定义出现在功能部分下。
  4. 填写细节并将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:识别呼叫者的ID
  • command:触发呼叫的命令;在这个例子中,它是 "/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频道带来的新鲜补充。