Webhooks(网络钩子)
Webhook 扩展允许你通过在特定事件触发 Webhook,将 Hocuspocus 连接到你现有的应用程序。
安装
使用以下命令安装 Webhook 包:
npm install @hocuspocus/extension-webhook配置
import { Server } from "@hocuspocus/server";
import { Webhook, Events } from "@hocuspocus/extension-webhook";
import { TiptapTransformer } from "@hocuspocus/transformer";
const server = new Server({
extensions: [
new Webhook({
// [必填] 你的应用程序 URL
url: "https://example.com/api/hocuspocus",
// [必填] 用于验证请求签名的随机字符串
secret: "459824aaffa928e05f5b1caec411ae5f",
// [必填] 你的文档转换器
transformer: TiptapTransformer,
// [可选] 触发 webhook 的事件数组
// 默认为 [ Events.onChange ]
events: [Events.onConnect, Events.onCreate, Events.onChange, Events.onDisconnect],
// [可选] change 事件的防抖时间(ms)
// 默认为 2000
debounce: 2000,
// [可选] 无论防抖情况,最大等待时间(ms)后发送 webhook
// 默认为 10000
debounceMaxWait: 10000,
}),
],
});
server.listen();工作原理
Webhook 扩展监听最多四个可配置事件/钩子,当事件触发时,向配置的 URL 发送 POST 请求。
onConnect
当新用户连接服务器时,onConnect webhook 会被触发,携带以下负载:
{
"event": "connect",
"payload": {
"documentName": "example-document",
"requestHeaders": {
"Example-Header": "Example"
},
"requestParameters": {
"example": "12345"
}
}
}你可以返回一个 JSON 负载,该负载将在整个应用程序中作为上下文使用。例如:
// 根据请求参数或头信息授权用户
if (payload.requestParameters?.get("token") !== "secret-api-token") {
response.writeHead(403, "unauthorized");
return response.end();
}
// 授权成功则返回上下文
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
user: {
id: 1,
name: "Jane Doe",
},
})
);onCreate
当新文档被创建时,onCreate webhook 会被触发,携带以下负载:
{
"event": "create",
"payload": {
"documentName": "example-document"
}
}你可以利用此事件将文档导入 Hocuspocus。Webhook 扩展首先会从主存储加载文档,且仅在文档不存在时导入它。
只需返回以字段名键控的所有单个文档。例如:
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
// “secondary” 字段的文档
secondary: {},
// “default” 字段的文档
default: {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "What is love?",
},
],
},
],
},
})
);onChange
当文档被更改时,onChange webhook 会被触发,携带包含你先前设置上下文的以下负载:
{
"event": "change",
"payload": {
"documentName": "example-document",
"document": {
"another-field-name": {},
"field-name": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "What is love?"
}
]
}
]
}
},
"context": {
"user_id": 1,
"name": "Jane Doe"
}
}
}由于该事件可能在每次敲击键盘时触发多次,默认启用了防抖处理。你可以通过 debounce 和 debounceMaxWait 配置项调整甚至关闭该功能。
onDisconnect
当用户断开连接时,onDisconnect webhook 会被触发,负载如下:
{
"event": "disconnect",
"payload": {
"documentName": "example-document",
"context": {
"user_id": 1,
"name": "Jane Doe"
}
}
}转换
Y-Doc 必须被序列化成你的应用可读的格式,导入文档时也必须相应地转换回 Y-Doc。
由于 Hocuspocus 无法预知你的数据结构,你需要向 Webhook 扩展传入转换器。你可以使用 @hocuspocus/transformer 包中的转换器。请确保正确配置它们。在此示例中,我们用到了需要扩展列表的 TiptapTransformer:
import { Server } from "@hocuspocus/server";
import { Webhook } from "@hocuspocus/extension-webhook";
import { TiptapTransformer } from "@hocuspocus/transformer";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
const server = new Server({
extensions: [
new Webhook({
url: "https://example.com/api/webhook",
secret: "459824aaffa928e05f5b1caec411ae5f",
transformer: TiptapTransformer.extensions([Document, Paragraph, Text]),
}),
],
});
server.listen();或者你也可以自行实现转换函数,通过传入将 Y-Doc 转换为你数据表示以及反向转换的方法:
import { Server } from "@hocuspocus/server";
import { Webhook } from "@hocuspocus/extension-webhook";
import { Doc } from "yjs";
const server = new Server({
extensions: [
new Webhook({
url: "https://example.com/api/webhook",
secret: "459824aaffa928e05f5b1caec411ae5f",
transformer: {
toYdoc(document: any, fieldName: string): Doc {
// 使用给定的文档(来自你的 API)和字段名转换为 Y-Doc
return new Doc();
},
fromYdoc(document: Doc): any {
// 将 Y-Doc 转换为你的表示形式
return document.toJSON();
},
},
}),
],
});
server.listen();验证请求签名
在你的应用服务器上,你应验证来自 webhook 扩展的签名以保证路由安全。
扩展发送 POST 请求,签名存储在 X-Hocuspocus-Signature-256 头,包含用 sha256 生成的消息认证码。
下面是不同语言中如何进行验证的示例:
PHP
use Symfony\Component\HttpFoundation\Request;
function verifySignature(Request $request) {
$secret = '459824aaffa928e05f5b1caec411ae5f';
if (($signature = $request->headers->get('X-Hocuspocus-Signature-256')) == null) {
throw new Exception('Header not set');
}
$parts = explode('=', $signature);
if (count($parts) != 2) {
throw new Exception('Invalid signature format');
}
$digest = hash_hmac('sha256', $request->getContent(), $secret);
return hash_equals($digest, $parts[1]);
}
JavaScript
import { IncomingMessage } from 'http'
const secret = '459824aaffa928e05f5b1caec411ae5f'
const verifySignature = (request: IncomingMessage): boolean => {
const signature = Buffer.from(request.headers['x-hocuspocus-signature-256'] as string)
const hmac = createHmac('sha256', secret)
const digest = Buffer.from(`sha256=${hmac.update(body).digest('hex')}`)
return signature.length !== digest.length || timingSafeEqual(digest, signature)
}