当下聊天工具和协作平台争奇斗艳,「开放平台」成为挑选的重要标准之一。飞书的开放平台集各家之所长,既有类似于 Telegram 的 Bot 机器人,也有类似微信公众号的小程序和网页应用。
本文以 Bot 机器人为例,尝试在飞书的开放平台动手完成一个「投票机器人」,借此一窥飞书开放平台的能力和用法。

创建一个 Bot

为便于体验,我们建立一个「企业自建机器人」。在创建时,我们可以选择配置应用名称、应用副标题以及为应用选择一个图标。

进入后台配置页面,开启「机器人」权限,这样,Bot 就创建完成了。 

为了收到用户与 Bot 之间的互动消息,我们需要配置一个 URL 用于接收消息。这个网址需要填写到两个地方,一个是上图中的「消息卡片请求网址」处,另一个是在「事件订阅」菜单下的「请求网址 URL」中。

填写网址后,飞书系统会向这个网址发送一个 POST 请求。这个请求的内容如下:

POST /sspai HTTP/1.1
Host: fs.xxx.xxx
Content-Length: 121
User-Agent: Go-http-client/1.1
Content-Type: application/json;charset=utf-8
X-Request-Id: 17212xxxxxxx
Accept-Encoding: gzip

{"challenge":"b715df13-587e-48e8-acf4-84d3e8dbaa51","token":"9a86Lk3lTNsXhNmu4Jkwce6NO1cje7yR","type":"url_verification"}

按照飞书官方的说法

应用接收此请求后,需要解析出 CHALLENGE 值,并于 1 秒内回复 CHALLENGE 值。

不过由于回复的消息体仍然需要使用 json 格式,并且飞书服务器只读取 challenge 值,因此可以把发过来的消息体原样 echo 回去,并不需要从请求体的 json 数据中解析出 challenge 值。

使用 Python 框架

在进行 challenge 值验证,以及后续与用户的互动中,我们都是使用 RESTful 编程接口与飞书服务器进行交互。如今许多语言都有成熟的框架来支持 RESTful 接口编程,本文以 Python 的 Flask 框架 为例进行操作。

如果不加赘余的验证,那么整个 challenge 过程的代码非常简单:

from flask import Flask, request
app = Flask(__name__)

@app.route('/sspai', methods=['POST'])
def sspai():
    return request.data.decode('utf8')

在这份代码中,前两行可以理解成使用 Flask 创建了一个服务器应用,而第三行提示为访问到 /sspai 这个路径的 POST 请求 进行处理,最后一行则实现了「消息体原样返回」的功能。

启动服务后,在飞书后台填写网址并点击「保存」,认证就会自动完成。

第一次聊天

当正式发布版本后,用户就可以在「工作台」中看到新增的机器人。

点击机器人就可以与它单独聊天,也就是「私聊」的页面。比如,我们可以偷偷地问一下机器人,在今天的抽奖活动中有没有中奖:

当用户向机器人发送消息时,飞书会向我们的服务器发送下面的信息。由于所有通信都采用 JSON 格式,我在下文中将只列出消息体,而省略消息的头部:

{
  "uuid":"095c3xxxxxxxxxxxxxxxxxxxxxxxx",
  "event":{
    "app_id":"cli_9d64bxxxxxxxxxxxxxxx",
    "chat_type":"private",
    "is_mention":false,
    "message_id":"",
    "msg_type":"text",
    "open_chat_id":"oc_c6c35bxxxxxxxxxxxxxxxx",
    "open_id":"ou_27033dxxxxxxxxxxxxxxx",
    "open_message_id":"om_ce23eexxxxxxxxxxxxxxxxxxxx",
    "parent_id":"",
    "root_id":"",
    "tenant_key":"2exxxxxxxxxxxxx",
    "text":"我中奖了吗?",
    "text_without_at_bot":"我中奖了吗?",
    "type":"message",
    "user_agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Lark/3.11.4 Chrome/73.0.3683.119 Electron/5.0.0 Safari/537.36 LarkLocale/zh_CN",
    "user_open_id":"ou_27033dfxxxxxxxxxxxxxxxxx"
  },
  "token":"Qo0xxxxxxxxxxxxxxxxxxxxx",
  "ts":"1576051466.125391",
  "type":"event_callback"
}

在这个消息体中,我们可以依据 "type": "message" 来判定这是一条聊天消息的事件,并依据 "chat_type": "private" 来判定这是一个私聊的消息。在 text 中包含了消息的内容,一系列 ID 则可以确定用户和聊天窗口的信息。

完成了消息的「接收」,下面来看看消息的「发送」。在飞书 Bot 中,消息的发送使用单独的 POST 请求来完成。下面是一个简单的例子:

{
  "chat_id":"oc_c6c35bxxxxxxxxxxxxxxxx",
  "msg_type":"text",
  "content":{
    "text":"你没有中奖,下次努力"
  }
}

把这个消息体发送到 https://open.feishu.cn/open-apis/message/v4/send/ 后,用户就可以接收到对应的文本消息。详细的接口文档可以查看官方说明

这个过程的代码如下:

if(j['event']['chat_type'] == 'private' and j['event']['text'] == '我中奖了吗?'):
    # 经查询,该用户没有中奖
    r = requests.post('https://open.feishu.cn/open-apis/message/v4/send/', headers={'Authorization': 'Bearer '+token}, json={'chat_id': j['event']['open_chat_id'], 'msg_type': 'text', 'content': {'text': '你没有中奖,下次努力'}})
    print(json.dumps(r.json(), indent=4, ensure_ascii=False))

不过,在发送消息之前,我们需要向飞书服务器证明我们的身份,以确认我们确实代表了「中奖啦」这一机器人。这个过程通过向飞书服务器发送我们应用的 app_idapp_secret 来完成,飞书向我们回复的 json 消息中包含 tenant_access_token 字段,也就是我填入前面请求头部的 Authorization 信息。代码如下:

def get_token():
    global token
    r = requests.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/',json={"app_id": "cli_9dxxxxxxx", "app_secret": "T0OifQVaxxxxxxx"})
    token = r.json()['tenant_access_token']

关于认证的过程,在官方文档中有详细的描述。

创建投票

通过来回的消息互动,可以收集抽奖所需的各个选项:

特别地,当我们的机器人收到一个图片时,隐去一些 ID 之后,消息大致是这样的:

{
  "event":{
    "chat_type":"private",
    "image_height":"756",
    "image_key":"img_2098xxxxxxxxxxxxxxxxxxx",
    "image_url":"https://oapi.zjurl.cn/open-apis/api/v2/file/f/img_2098axxxxxxxxxxxxxx",
    "image_width":"1359",
    "is_mention":false,
    "msg_type":"image",
    "type":"message"
  },
  "type":"event_callback"
}

消息中包含了图片的尺寸,以及一个唯一的 image_key。我们可以直接用它来进行后续消息的发送,也可以通过指定接口把图片下载下来保存

拉进群里一起嗨

除了私聊,机器人当然还可以在群里和大家一起玩耍。

把机器人加入到群聊后,我们的服务器会收到相应的事件提示。如果需要,可以在每次加入群的时候自动跟大家问个好。消息内容大致如下:

{
  "event":{
    "chat_name":"抽个奖",
    "type":"add_bot"
  },
  "type":"event_callback"
}

同样的,当用户 @ 了我们的 Bot,服务器就会收到对应的请求,以便我们进行处理:

{
  "event":{
    "chat_type":"group",
    "is_mention":true,
    "msg_type":"text",
    "text":"1021 <at open_id=\"ou_437a0e8372339eefab712c09388f61eb\">@中奖啦</at>",
    "text_without_at_bot":"1021 ",
    "type":"message"
  },
  "type":"event_callback"
}

更自由的消息卡片

在生成投票后,我们可以使用「消息卡片」在群里发起投票。飞书机器人的消息卡片不但可以包含图片、分割线、纯文本、Markdown 格式的内容,还可以包含多选菜单、日期选择和按钮等互动组件,用来与用户之间形成更友好的互动。

在上图这个消息卡片中,「iPad Air 3」是消息卡片的标题。内容部分分成三块,首先是之前上传的图片,第二部分是以 Markdown 格式撰写的正文内容,最底下是一个互动按钮。
整个消息卡片的请求体如下:

{
  "open_chat_id":"oc_8e9a1dcc6d83b231ddf1bb951de09f58",
  "msg_type":"interactive",
  "card":{
    "header":{
      "title":{
        "tag":"plain_text",
        "content":"iPad Air 3"
      }
    },
    "elements":[
      {
        "tag":"img",
        "img_key":"img_2098a60d-8267-4d4c-91a8-d94b2baf90dg",
        "alt":{
          "tag":"plain_text",
          "content":"iPad Air 3"
        }
      },
      {
        "tag":"div",
        "text":{
          "tag":"lark_md",
          "content":"活动描述:**Apple 出品**\n开奖时间:**2019-12-11 18:00**"
        }
      },
      {
        "tag":"action",
        "actions":[
          {
            "tag":"button",
            "text":{
              "tag":"plain_text",
              "content":"参加抽奖"
            },
            "type":"default"
          }
        ]
      }
    ]
  }
}

自上而下分成四部分,消息卡片的基础排版格式十分清晰。除此之外,卡片还支持多种元素的组合排版,用以呈现更加丰富、定制化的卡片效果。

参加抽奖并揭晓结果

当群里的用户点击了「参加抽奖」按钮,我们的服务器会收到一个事件提示:

{
  "open_id":"ou_27033xxxxxxx",
  "user_id":"d1xxxxxx",
  "open_message_id":"om_06308xxxxxxxxxx",
  "tenant_key":"2exxxxxxxx",
  "token":"c-981edd0xxxxxxx",
  "action":{
    "tag":"button"
  }
}

由于消息中包含了用户信息,我们可以向该用户发送私信,通知他已成功参加抽奖。揭晓结果时,同样可以用私聊的形式通知该用户。
此外,在结果揭晓时,机器人会发送一条群信息,公布所有获奖的用户。通过用户的 open_id,我们可以直接在群里 @ 他们:

{
  "chat_id":"oc_8e9a1xxxxxxxxxx",
  "msg_type":"text",
  "content":{
    "text":"恭喜中奖 <at user_id=\"ou_270xxxxxxx\"></at>"
  }
}

效果如下:

如此,我们便完成了整个抽奖过程。