我们如何向 SendRec 添加通用 Webhooks
Source: Dev.to
(请提供需要翻译的正文内容,我才能为您完成简体中文翻译。)
Source: …
Webhook 概述
SendRec 已经支持 Slack 通知,但 Slack 只是其中一种渠道。
如果您需要在有人观看视频时触发 n8n 工作流、在观众点击号召性用语时向您的 CRM POST 数据,或将每个事件输送到自定义仪表盘,您就需要 通用 webhook —— 一个接收所有事件的 JSON POST 的单一 URL。
用户配置
- 每位用户都可以在 设置 中配置一个 webhook URL。
- 当他们的视频发生任何事件时,SendRec 会向该 URL POST 一个 JSON 负载。
{
"event": "video.viewed",
"timestamp": "2026-02-21T14:30:00Z",
"data": {
"videoId": "abc-123",
"title": "Product Demo",
"watchUrl": "https://app.sendrec.eu/watch/xyz",
"viewCount": 5,
"viewerHash": "sha256..."
}
}
事件类型
七种事件类型覆盖了完整的视频生命周期:
| 事件 | 描述 |
|---|---|
video.created | 创建了新的视频记录 |
video.ready | 视频已完成处理,准备好观看 |
video.deleted | 视频已被删除 |
video.viewed | 观众观看了视频 |
video.comment | 添加了评论 |
video.milestone | 达到了观看次数里程碑 |
video.cta_click | 点击了号召性用语按钮 |
负载签名
每个请求都会包含一个 X-Webhook-Signature 头部,以便接收方验证负载未被篡改。
我们使用 HMAC‑SHA256,并遵循 GitHub Webhook 相同的约定(sha256= 前缀)。
func SignPayload(secret string, payload []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
- 当首次保存 webhook URL 时,密钥会自动生成——32 个随机字节,十六进制编码。
- 接收方会重新计算请求体的 HMAC,并将其与头部值进行比较。
sha256=前缀明确了使用的算法,并为将来可能的算法提供了空间,而不会破坏现有的集成。
重试与投递逻辑
Webhook 端点可能会宕机、部署重启或负载均衡器出现短暂故障。一次投递失败 不应 视为事件丢失。
重试策略
- 最多 3 次尝试,采用指数退避(
1s,随后4s)。 - 可配置的延迟 (
c.retryDelays)。 - 上下文取消会中止等待,从而防止关机时被阻塞。
func (c *Client) Dispatch(ctx context.Context, userID, webhookURL, secret string, event Event) error {
body, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshal webhook payload: %w", err)
}
signature := SignPayload(secret, body)
maxAttempts := 1 + len(c.retryDelays)
var lastErr error
for attempt := 1; attempt = 200 && *statusCode maxResponseBodyBytes {
respBody = respBody[:maxResponseBodyBytes]
}
}
我们读取比限制多一个字节以检测截断,然后裁剪到恰好 1024 字节。
调度助手(Fire‑and‑Forget)
该助手在后台 goroutine 中触发 webhook,复制现有的 Slack 通知模式。
func (h *Handler) dispatchWebhook(userID string, event webhook.Event) {
if h.webhookClient == nil {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
webhookURL, secret, err := h.webhookClient.LookupConfigByUserID(ctx, userID)
if err != nil {
return
}
_ = h.webhookClient.Dispatch(ctx, userID, webhookURL, secret, event)
}()
}
- 对于已经在 goroutine 中运行的处理器(例如评论通知、里程碑记录、CTA 点击跟踪),我们直接调用 webhook 客户端——无需额外的 goroutine。
URL 验证与密钥生成
- 仅限 HTTPS,但有一个例外:
http://localhost和http://127.0.0.1允许用于本地开发。 - URL 长度限制为 500 个字符。
- 空 URL 会清除 webhook(存储为
NULL)——无需单独的开关,遵循与 Slack 相同的模式。
当 webhook URL 首次保存且不存在密钥时,我们会自动生成一个:
func generateWebhookSecret() (string, error) {
b := make([]byte, 32) // 32 random bytes
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate secret: %w", err)
}
return hex.EncodeToString(b), nil
}
生成的密钥随后用于每次发送的 X-Webhook-Signature 头部。
TL;DR
- 用户可配置的 webhook URL → 每个事件的 JSON 负载。
- 使用
X-Webhook-Signature的 HMAC‑SHA256 签名。 - 重试最多 3 次,采用指数退避,并进行完整日志记录。
- 投递记录 存储在
webhook_deliveries中(截断的响应体)。 - 异步发送(fire‑and‑forget),永不阻塞面向用户的 HTTP 响应。
- 强制使用 HTTPS(本地主机例外),并为每个用户 自动生成密钥。
所有这些为 SendRec 提供了一个健壮、通用的 webhook 系统,能够与任何下游服务集成。
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
更新 Webhook URL
当您更新 webhook URL 时,现有的 secret 会通过 COALESCE 保留下来:
INSERT INTO notification_preferences (user_id, webhook_url, webhook_secret)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
webhook_url = $2,
webhook_secret = COALESCE(notification_preferences.webhook_secret, $3);
如果您需要 新的 secret,请使用专门的 “重新生成” 按钮。该按钮会通过与保存 URL 的端点不同的单独接口创建全新的 secret,这样您在每次更改端点时就不必手动复制新的 secret 了。
设置 → Webhook 部分
- URL input – 带有 Save 按钮的字段。
- Signing secret – 默认被遮蔽,提供 Copy 和 Regenerate 按钮。
- Send test event – 发送
webhook.test事件,以便在没有真实视图的情况下验证投递。 - Recent deliveries – 显示最近 50 次投递尝试,每条记录包含:
- 事件类型
- 状态徽章
- 时间戳
- 可展开的行,展示完整的负载和响应体
- Events reference – 可折叠的表格,列出所有事件类型及其负载字段。
delivery log 是最有价值的部分:当你的 webhook 端点返回错误时,你可以准确看到发送的内容和返回的内容,而无需在服务器日志中翻找。
试用
SendRec 是开源的(AGPL‑3.0)且可自行托管。通用 webhook 已上线于 app.sendrec.eu:
- 前往 Settings → Webhooks。
- 添加一个 webhook URL。
- 点击 Send test event。
Webhook 客户端实现可在 webhook.go 中找到。