Skip to content

Commit 9dddba7

Browse files
feat(lark): add Lark notification service
- Implemented Lark service with text, post, and link support - Added config validation and comprehensive tests - Included usage documentation Builds on work from containrrr#456
1 parent 5ff2b07 commit 9dddba7

File tree

7 files changed

+634
-0
lines changed

7 files changed

+634
-0
lines changed

docs/services/lark.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Lark
2+
3+
Send notifications to Lark using a custom bot webhook.
4+
5+
## URL Format
6+
7+
!!! info ""
8+
lark://__`host`__/__`token`__?secret=__`secret`__&title=__`title`__&link=__`url`__
9+
10+
--8<-- "docs/services/lark/config.md"
11+
12+
- `host`: The bot API host (`open.larksuite.com` for Lark, `open.feishu.cn` for Feishu).
13+
- `token`: The bot webhook token (required).
14+
- `secret`: Optional bot secret for signed requests.
15+
- `title`: Optional message title (switches to post format if set).
16+
- `link`: Optional URL to include as a clickable link in the message.
17+
18+
### Example URL
19+
20+
```url
21+
lark://open.larksuite.com/abc123?secret=xyz789&title=Alert&link=https://example.com
22+
```
23+
24+
## Create a Custom Bot in Lark
25+
26+
Official Documentation: [Custom Bot Guide](https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot)
27+
28+
1. __Invite the Custom Bot to a Group__:
29+
a. Open the target group, click `More` in the upper-right corner, and then select `Settings`.
30+
b. In the `Settings` panel, click `Group Bot`.
31+
c. Click `Add a Bot` under `Group Bot`.
32+
d. In the `Add Bot` dialog, locate `Custom Bot` and select it.
33+
e. Set the bot’s name and description, then click `Add`.
34+
f. Copy the webhook address and click `Finish`.
35+
36+
2. __Get Host and Token__:
37+
- For __Lark__: Use `host = open.larksuite.com`.
38+
- For __Feishu__: Use `host = open.feishu.cn`.
39+
- The `token` is the last segment of the webhook URL.
40+
For example, in `https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxx`, the token is `xxxxxxxxxxxxxxxxx`.
41+
42+
3. __Get Secret (Optional)__:
43+
a. In group settings, open the bot list, find your custom bot, and select it to access its configuration.
44+
b. Under `Security Settings`, enable `Signature Verification`.
45+
c. Click `Copy` to save the secret.
46+
d. Click `Save` to apply the changes.

docs/services/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Click on the service for a more thorough explanation. <!-- @formatter:off -->
2222
| [Teams](./teams.md) | *teams://__`group`__@__`tenant`__/__`altId`__/__`groupOwner`__?host=__`organization`__.webhook.office.com* |
2323
| [Telegram](./telegram.md) | *telegram://__`token`__@telegram?chats=__`@channel-1`__[,__`chat-id-1`__,...]* |
2424
| [Zulip Chat](./zulip.md) | *zulip://__`bot-mail`__:__`bot-key`__@__`zulip-domain`__/?stream=__`name-or-id`__&topic=__`name`__* |
25+
| [Lark](./lark.md) | *lark://__`host`__/__`token`__?secret=__`secret`__&title=__`title`__&link=__`url`__* |
2526

2627
## Specialized services
2728

pkg/router/servicemap.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/nicholas-fedor/shoutrrr/pkg/services/gotify"
99
"github.com/nicholas-fedor/shoutrrr/pkg/services/ifttt"
1010
"github.com/nicholas-fedor/shoutrrr/pkg/services/join"
11+
"github.com/nicholas-fedor/shoutrrr/pkg/services/lark"
1112
"github.com/nicholas-fedor/shoutrrr/pkg/services/logger"
1213
"github.com/nicholas-fedor/shoutrrr/pkg/services/matrix"
1314
"github.com/nicholas-fedor/shoutrrr/pkg/services/mattermost"
@@ -32,6 +33,7 @@ var serviceMap = map[string]func() types.Service{
3233
"googlechat": func() types.Service { return &googlechat.Service{} },
3334
"hangouts": func() types.Service { return &googlechat.Service{} },
3435
"ifttt": func() types.Service { return &ifttt.Service{} },
36+
"lark": func() types.Service { return &lark.Service{} },
3537
"join": func() types.Service { return &join.Service{} },
3638
"logger": func() types.Service { return &logger.Service{} },
3739
"matrix": func() types.Service { return &matrix.Service{} },

pkg/services/lark/lark_config.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package lark
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strings"
7+
8+
"github.com/nicholas-fedor/shoutrrr/pkg/format"
9+
"github.com/nicholas-fedor/shoutrrr/pkg/types"
10+
)
11+
12+
// Scheme is the identifier for the Lark service protocol.
13+
const Scheme = "lark"
14+
15+
// Config represents the configuration for the Lark service.
16+
type Config struct {
17+
Host string `default:"open.larksuite.com" desc:"Custom bot URL Host" url:"Host"`
18+
Secret string `default:"" desc:"Custom bot secret" key:"secret"`
19+
Path string ` desc:"Custom bot token" url:"Path"`
20+
Title string `default:"" desc:"Message Title" key:"title"`
21+
Link string `default:"" desc:"Optional link URL" key:"link"`
22+
}
23+
24+
// Enums returns a map of enum formatters (none for this service).
25+
func (config *Config) Enums() map[string]types.EnumFormatter {
26+
return map[string]types.EnumFormatter{}
27+
}
28+
29+
// GetURL constructs a URL from the Config fields.
30+
func (config *Config) GetURL() *url.URL {
31+
resolver := format.NewPropKeyResolver(config)
32+
33+
return config.getURL(&resolver)
34+
}
35+
36+
// getURL constructs a URL using the provided resolver.
37+
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
38+
return &url.URL{
39+
Host: config.Host,
40+
Path: "/" + config.Path,
41+
Scheme: Scheme,
42+
ForceQuery: true,
43+
RawQuery: format.BuildQuery(resolver),
44+
}
45+
}
46+
47+
// SetURL updates the Config from a URL.
48+
func (config *Config) SetURL(url *url.URL) error {
49+
resolver := format.NewPropKeyResolver(config)
50+
51+
return config.setURL(&resolver, url)
52+
}
53+
54+
// setURL updates the Config from a URL using the provided resolver.
55+
// It sets the host, path, and query parameters, validating host and path, and returns an error if parsing or validation fails.
56+
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
57+
config.Host = url.Host
58+
if config.Host != larkHost && config.Host != feishuHost {
59+
return ErrInvalidHost
60+
}
61+
62+
config.Path = strings.Trim(url.Path, "/")
63+
if config.Path == "" {
64+
return ErrNoPath
65+
}
66+
67+
for key, vals := range url.Query() {
68+
if err := resolver.Set(key, vals[0]); err != nil {
69+
return fmt.Errorf("setting query parameter %q: %w", key, err)
70+
}
71+
}
72+
73+
return nil
74+
}

pkg/services/lark/lark_message.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package lark
2+
3+
// RequestBody represents the payload sent to the Lark API.
4+
type RequestBody struct {
5+
MsgType MsgType `json:"msg_type"`
6+
Content Content `json:"content"`
7+
Timestamp string `json:"timestamp,omitempty"`
8+
Sign string `json:"sign,omitempty"`
9+
}
10+
11+
// MsgType defines the type of message to send.
12+
type MsgType string
13+
14+
// Constants for message types supported by Lark.
15+
const (
16+
MsgTypeText MsgType = "text"
17+
MsgTypePost MsgType = "post"
18+
)
19+
20+
// Content holds the message content, supporting text or post formats.
21+
type Content struct {
22+
Text string `json:"text,omitempty"`
23+
Post *Post `json:"post,omitempty"`
24+
}
25+
26+
// Post represents a rich post message with language-specific content.
27+
type Post struct {
28+
Zh *Message `json:"zh_cn,omitempty"` // Chinese content
29+
En *Message `json:"en_us,omitempty"` // English content
30+
}
31+
32+
// Message defines the structure of a post message.
33+
type Message struct {
34+
Title string `json:"title"`
35+
Content [][]Item `json:"content"`
36+
}
37+
38+
// Item represents a content element within a post message.
39+
type Item struct {
40+
Tag TagValue `json:"tag"`
41+
Text string `json:"text,omitempty"`
42+
Link string `json:"href,omitempty"`
43+
}
44+
45+
// TagValue specifies the type of content item.
46+
type TagValue string
47+
48+
// Constants for tag values supported by Lark.
49+
const (
50+
TagValueText TagValue = "text"
51+
TagValueLink TagValue = "a"
52+
)
53+
54+
// Response represents the API response from Lark.
55+
type Response struct {
56+
Code int `json:"code"`
57+
Msg string `json:"msg"`
58+
Data any `json:"data"`
59+
}

0 commit comments

Comments
 (0)