Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

notifyutils-lark

飞书 / Lark 群「自定义机器人」driver for notifyutils: go.gh.ink/notifyutils/lark.

The official SDKs (larksuite/oapi-sdk-go) speak the application-robot protocol — app credentials, tenant_access_token, im/v1/messages — which is a different integration from a group webhook that already exists. This driver needs none of that, so it is standard library only, plus the core's utils/httpx and utils/sign.

Credentials

Key Required Meaning
webhook yes The hook URL, token included: https://open.feishu.cn/open-apis/bot/v2/hook/<token>
secret no The 密钥 behind the robot's 签名校验 security setting. Empty means no signature is computed or sent
cardTemplateID, cardTemplateVersion, cardVariables, cardTemplate no Describe a card once in config instead of per message: see Cards from the credentials

A missing webhook fails at NewClient with errors.ErrDriverCredentialInvalid. The robot's other two security settings — 关键词 and IP 白名单 — are server-side and need nothing here; a message that misses the keyword comes back as code 19024.

Recipients means "who to @", not "where to send"

The hook URL already picks the group, so msg.Recipients is the mention list: open_id or user_id values, which is all a custom robot may use — it holds no data permissions and is explicitly not allowed to address anybody by email or mobile number. Empty means nobody is addressed.

msg.Format Payload Mention syntax
plain msg_type: "text", content.text <at user_id="ou_x">ou_x</at>, <at user_id="all">所有人</at>
markdown msg_type: "interactive" + card (schema 2.0), one markdown element <at id=ou_x></at>, <at id=all></at>
html errors.ErrUnsupportedFormat —

markdown goes out as a card rather than a post because post's documented tags (text, a, at, img) print their content literally, so markdown would arrive as source code, while the card markdown element is documented as carrying markdown syntax itself.

Titles: a card has a headline slot, so msg.Title becomes card.header.title (and the header is omitted entirely when there is no title). A text message has none, so the title folds onto the first line.

Cards from the credentials

A card does not have to be written by the sending code. Four credentials describe it once, and the driver assembles and renders the card per message. The template dot is model.Message — the same documented context as every other channel here, with Vars already bound into Title and Text by the core.

Key Meaning
cardTemplateID A card built in the 卡片搭建工具, referenced by its template id
cardTemplateVersion Optional template_version_name for that template
cardVariables JSON object mapping the template's placeholder names to template strings, e.g. {"content":"{{.Text}}"}
cardTemplate A whole card JSON of your own, rendered the same way — the alternative to cardTemplateID
credential := map[string]string{
	lark.Webhook:         "https://open.feishu.cn/open-apis/bot/v2/hook/…",
	lark.CardTemplateID:  "AAqyBQVmUN0w",
	lark.CardVariables:   `{"content":"{{.Text}}","peer":"{{.Extras.peer}}"}`,
}

Decisions the driver makes so callers do not have to:

  • Variable values are always strings. cardVariables must decode to map[string]string, so a phone-shaped value such as 13800138000 reaches the card as text rather than being re-typed as 1.38e+10 by a JSON number.
  • The envelope is assembled from Go values, then encoded once: quotes and newlines inside a rendered variable cannot break out of their JSON string.
  • {{json …}} for whole-card templates. cardTemplate interpolates into JSON text, so {{.Title}} with a quote in it corrupts the document — the driver refuses to send that rather than shipping a half-parseable card. Write "content":{{json .Title}} (no surrounding quotes: the helper emits a quoted, escaped JSON string literal) whenever the value comes from the message.
  • A card on the message wins. lark/card in Extras overrides any credential card, and keeps its own rule of refusing a title or mention list it cannot place. A credential card does not refuse them: it is a template, so it can put {{.Title}} wherever it likes.
  • A missing key fails loudly. Field-style lookups ({{.Extras.peer}}) on an absent key error at send time rather than printing Go's <no value> into the card. {{index .Extras "peer-id"}} is outside that check and still renders <no value>, so use the field style for optional keys.
  • The rendered card still passes the documented 20 KB request-body ceiling, measured after encoding.

Extras

Key Type Effect
lark/atUserIDs []string / string open_ids / user_ids to @
lark/atAll bool @ the whole group; needs the group's @所有人 permission, otherwise the send fails
lark/card any / JSON string / []byte Send this card structure verbatim under card, overriding both Format and any card configured in the credentials — a hand-built card, or a 搭建工具 template {"type":"template","data":{"template_id":…}}. Because it is sent untouched, a message that also carries Title or mentions is refused rather than silently stripped

The keys are exported as ExtraAtUserIDs, ExtraAtAll and ExtraCard; MentionAll is the all value a card's <at id=all></at> tag carries, and MaxRequestBodyBytes the ceiling below.

Signature

With secret set, the body gains two fields — timestamp (seconds, stringified) and sign. The documented recipe is easy to get backwards: the HMAC-SHA256 key is the string <timestamp>\n<secret> and the signed message is empty, and the result is base64 encoded:

sign.HMACSHA256Base64([]byte(timestamp+"\n"+secret), nil)

Both fields travel in the request body, not in the URL query. A timestamp older than one hour (3600 s) fails as code 19021, as does clock skew on the sending host.

Body ceiling

One request body may not exceed 20 KB; the doc does not say whether that is 1000 or 1024 bytes, so MaxRequestBodyBytes = 20_000 takes the smaller reading. The check runs on the encoded body — the JSON overhead and the signature fields count — and an over-long message returns errors.ErrMessageTooLong without being sent. Nothing is truncated into a partial notification.

Failure shape

This channel answers HTTP 200 even when it rejects the message, so code in the body is the verdict: non-zero becomes errors.ErrDriverSendFailed with code as DriverCode, msg as DriverMessage and the raw body as DriverResponse. A 200 whose body is not the documented {code,msg} JSON is a failure as well, since it does not confirm delivery. DriverRequestID carries the X-Tt-Logid response header, which is what the docs ask you to quote when reporting a problem. Documented codes: 9499 Bad Request (body shape), 19021 sign mismatch, 19022 IP not allowed, 19024 keyword not found, 11232 rate limited.

Frequency: 单租户单机器人 100 次/分钟, 5 次/秒; the docs advise avoiding the top and half of the hour, where a burst is more likely to hit 11232.

Sources

Not verified: the guide states one ceiling for the whole body (20 KB) and no per-field length for content.text, post or card, so the driver checks nothing else. Whether the hook response actually carries x-tt-logid — the header is documented for open-platform API responses in general, not for this endpoint — was not confirmed against the live service; DriverRequestID simply stays empty when it is absent.

Requirements

Go 1.26.0+, standard library only.

License

Apache License 2.0

About

Lark driver for notifyutils

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages