前言
本文介绍了基于开源自动化平台 n8n 搭建自动化流程,实现监控 RSS 更新并推送到飞书消息的功能。
文末会列举一些实现此工作流的其他方式,包括发送请求和接收提醒的手段。与此同时,n8n 还可以通过模块组合,实现更多更复杂的功能,本文只作为抛砖引玉。
阅读本文可能需要一定 Linux 基础知识。
n8n 是什么
n8n 是一个开源的自动化流程搭建工具,可以实现类似 IFTTT 的效果,比如「如果明天下雨,就推送要带伞的消息」。优点是开源、可以自己部署并将信息都储存在本地,同时可以与 Github、Telegram、Slack 等各种服务实现联动,以搭建自动化工作流。
利用 Docker 安裝 n8n
n8n 可以直接下载 Win 或是 Mac 版本,快速在本地使用,但如果想更稳定地长期运行,更适合部署在云服务器、树莓派或 NAS 等工具上。
这里以在云服务器上使用 Docker 进行部署为例,更多安装方式可参考 Installation guides for n8n。
假设已经安装好了 Docker,那么 n8n 的部署就非常简单,先新建一个文件夹储存数据。
# 创建数据储存文件夹
mkdir ~/n8n-data
复制运行下面的代码,利用 Docker 安装 n8n。如果云服务器有防火墙,需要把对应的端口打开,这里需要打开云服务器的 TCP 端口 5678
。
# 利用Docker安装运行n8n
docker run -d \
--name n8n --restart unless-stopped \
-p 5678:5678 \
-v ~/n8n-data:/home/node/.n8n \
-e GENERIC_TIMEZONE="Asia/Shanghai" \
n8nio/n8n
稍作等待,等 Docker 安装完成后,如果一切顺利,访问 服务器ip地址:5678
就能看到 n8n 的运行页面了,初次进入需要创建账号密码。
点击右上角的 New blank workflow 即可开始创建,也可以从软件提供的 Workflow 示例中,选择自己想部署的自动化流程。
这里以搭建一个 RSS 更新自动推送到飞书的机器人为例,展示 n8n 的一些使用方式。
搭建飞书 RSS 推送机器人
以下是我配置好的一个流程模板,复制以下内容粘贴到 n8n 新建 workflow 的页面。
{
"nodes": [
{
"parameters": {
"url": "https://sspai.com/feed"
},
"name": "RSS Feed Read",
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1,
"position": [
160.5,
440
]
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{new Date($node[\"Latest Read\"].data[\"latestRead\"]).getTime()}}",
"value2": "={{new Date($node[\"RSS Feed Read\"].data[\"isoDate\"]).getTime()}}"
}
],
"boolean": [],
"string": [
{
"value1": "={{$json[\"title\"]}}",
"operation": "contains"
}
]
}
},
"name": "IF",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
560,
440
]
},
{
"parameters": {
"functionCode": "const staticData = this.getWorkflowStaticData('global');\n\nif (items.length > 0) {\n staticData.latestRead = items[0].json.isoDate || staticData.latestRead;\n}\n\n\nreturn items;"
},
"name": "Write Latest Read",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
760,
340
]
},
{
"parameters": {},
"name": "NoOp",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
750,
580
]
},
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "everyX",
"value": 1
}
]
}
},
"name": "Cron",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [
-40,
440
]
},
{
"parameters": {
"requestMethod": "POST",
"options": {
"batchInterval": 3000,
"batchSize": 1
},
"bodyParametersUi": {
"parameter": [
{
"name": "msg_type",
"value": "interactive"
},
{
"name": "card",
"value": "={\n \"config\": {\n \"wide_screen_mode\": true\n },\n \"header\": {\n \"template\": \"black\",\n \"title\": {\n \"content\": \"{{$json[\"title\"]}}\",\n \"tag\": \"plain_text\"\n }\n },\n \"elements\": [\n {\n \"tag\": \"div\",\n \"text\": {\n \"content\": \"{{$json[\"contentSnippet\"]}}\",\n \"tag\": \"lark_md\"\n }\n },\n {\n \"tag\": \"hr\"\n },\n {\n \"elements\": [\n {\n \"content\": \"[阅读原文]({{$json[\"link\"]}})\",\n \"tag\": \"lark_md\"\n }\n ],\n \"tag\": \"note\"\n }\n ]\n}"
}
]
},
"headerParametersUi": {
"parameter": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
1000,
340
]
},
{
"parameters": {
"functionCode": "const staticData = this.getWorkflowStaticData('global');\n\nlatestRead = staticData.latestRead;\n\nfor (let item of items) {\n item.json.latestRead = latestRead || '2022-05-05';\n //item.json[\"content:encodedSnippet\"] = item.json[\"content:encodedSnippet\"].replace(/[\\r\\n]/g,\"\\\\n\");\n}\n\nreturn items;"
},
"name": "Latest Read",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
360,
440
]
}
],
"connections": {
"RSS Feed Read": {
"main": [
[
{
"node": "Latest Read",
"type": "main",
"index": 0
}
]
]
},
"IF": {
"main": [
[
{
"node": "Write Latest Read",
"type": "main",
"index": 0
}
],
[
{
"node": "NoOp",
"type": "main",
"index": 0
}
]
]
},
"Write Latest Read": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"Cron": {
"main": [
[
{
"node": "RSS Feed Read",
"type": "main",
"index": 0
}
]
]
},
"Latest Read": {
"main": [
[
{
"node": "IF",
"type": "main",
"index": 0
}
]
]
}
}
}
粘贴后可以看到如下的界面:
这里有几处可以配置,第一处是 Cron,设置自动化流程触发的频率,每隔 X 时间间隔运行一次,图中设置为每隔一小时运行。在获取 RSS 时,运行频率不宜过高。如果访问过于频繁,一方面会给对方服务器造成较大负担,同时可能被服务器禁止访问。
第二个是在 RSS Feed Read 处,填写想订阅的 RSS 地址,这里以少数派 RSS 为例,填写完后点击Excute node,先运行一次获取数据,方便后续设置。
第三处(可选)IF 处,设置是否需要针对标题或内容等进行过滤,默认不过滤。
这时先转到飞书,在飞书桌面端,打开一个群(建议先创建一个单人的群进行调试),打开设置,找到群机器人,并点击添加机器人,选择自定义机器人加入群聊,详细操作可以参照 飞书自定义机器人指南。
最后在 HTTP Request 处填入飞书机器人 webhook 地址。
填写完成后 Excute node 尝试运行,一切顺利的话就能在飞书中看到推送来的RSS消息了。
这里使用了卡片的形式展示消息,若是想调整消息展示样式,可以参考少数派文章 手把手教你用飞书 Webhook 打造一个消息推送 Bot。
消息机器人安全设置
由于采用 Webhook 的形式,请务必保管好 Webhook 链接,如果泄露可能会导致被推送垃圾信息。为了进一步加道保险,飞书提供了三种安全设置方式,分别是自定义关键词、IP 白名单和签名校验。
前两种方式非常好理解,也都很好设置。自定义关键词是只有当消息中至少含有一个预设的关键词时,才会进行消息推送;IP 白名单则是只推送名单中来源的 IP 所发送的请求。但是这两种方式也有一定的局限性:
- 关键词有时使消息不够简洁
- 部署在本地树莓派等设备上时,IP 地址不固定,无法指定
- 关键词和 IP 白名单各自最多只能添加十个条目
因此这里详细介绍一下在 n8n 中进行签名校验的配置方式。飞书的签名需要将「timestamp + "\n" + 密钥」组合起来当作签名密钥,采用 Hmac SHA256 算法计算签名,再进行 Base64 编码。在发送消息请求时,需要增加对应的 timestamp
和 sign
字段。
// 开启签名验证后发送文本消息的请求示例
{
"timestamp": "1599360473",
"sign": "xxxxxxxxxxxxxxxxxxxxx",
"msg_type": "text",
"content": {
"text": "The message content is here"
}
}
在 n8n 中,可以使用 Crypto 模块利用密钥生成签名,复制以下代码粘贴到配置界面,可以得到生成飞书签名用的模块组合。
{
"nodes": [
{
"parameters": {
"action": "hmac",
"type": "SHA256",
"value": "={{''}}",
"dataPropertyName": "sign",
"secret": "={{$json[\"timestamp\"]+'\\n'+$json[\"secret\"]}}",
"encoding": "base64"
},
"name": "Crypto",
"type": "n8n-nodes-base.crypto",
"typeVersion": 1,
"position": [
-80,
440
]
},
{
"parameters": {
"values": {
"string": [
{
"name": "timestamp",
"value": "={{Math.round(new Date().getTime()/1000)}}"
},
{
"name": "secret"
}
]
},
"options": {}
},
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [
-280,
440
]
}
],
"connections": {
"Set": {
"main": [
[
{
"node": "Crypto",
"type": "main",
"index": 0
}
]
]
}
}
}
将上面新增的两个模块按下图方式进行拖拽连接:
从飞书机器人设置界面中,勾选签名校验得到密钥,填写在 Set 模块中。
接下来将 Latest Read 模块中的代码替换为以下内容,储存计算出的签名,方便在请求的时候调用。
// JS code in the Latest Read Module
const staticData = this.getWorkflowStaticData('global');
latestRead = staticData.latestRead;
for (let item of items) {
item.json.latestRead = latestRead || '2022-05-05';
item.json.timestamp = $item("0").$node["Crypto"].json["timestamp"];
item.json.sign = $item("0").$node["Crypto"].json["sign"];
}
return items;
最后在 HTTP Request 模块中增加校验用的字段:Body Parameters → Add Parameter,添加两个参数,Name 分别为 timestamp
和 sign
,Value 处点击右侧 Add Expression,分别选择两个对应字段的值。
这样一番倒腾,给飞书机器人模块增加了签名校验,使得信息推送更加安全。当一切配置妥当后,别忘了点击界面右上角的激活,让工作流开始自动运行。
后记
本文介绍了如何用 n8n 打造一个飞书 RSS 推送机器人。订阅什么样的 RSS 来源,可以是网站自身提供的 RSS 地址,也可以利用 RSShub 将各种奇怪的网站转化为 RSS,甚至是利用 kill-the-newsletter 将任意 Newsletter 邮件转化为 RSS 进行追踪。
同时,实现类似工作流的手段还有很多。对于 n8n 这部分,可以使用 IFTTT、Integrately,或是 Github Action 等,实现工作流中「监控 RSS 更新并发送 Webhook 请求」这部分;对于接收提醒,文中利用了飞书作为展示消息的界面,而 n8n 也支持连接到 Telegram、Slack 等通讯软件,或是通过 Send Email 模块实现邮件通知,以及发送到 Cubox、flomo 等各种支持 Webhook 的工具中。
更多功能,更多组合,尽请探索,把闲置的云服务器或是积灰的树莓派等折腾起来吧。