1
0
mirror of https://github.com/nikoksr/notify.git synced 2025-04-11 11:41:58 +02:00

Merge pull request #300 from immanuelhume/feat/lark

This commit is contained in:
Niko Köser 2022-08-05 10:19:08 +02:00 committed by GitHub
commit 2a4c07843e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 589 additions and 1 deletions

View File

@ -85,7 +85,8 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [DingTalk](https://www.dingtalk.com) | [service/dinding](service/dingding) | [blinkbean/dingtalk](https://github.com/blinkbean/dingtalk) |
| [Discord](https://discord.com) | [service/discord](service/discord) | [bwmarrin/discordgo](https://github.com/bwmarrin/discordgo) |
| [Email](https://wikipedia.org/wiki/Email) | [service/mail](service/mail) | [jordan-wright/email](https://github.com/jordan-wright/email) |
| [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) | [service/fcm](service/fcm) | [appleboy/go-fcm](https://github.com/appleboy/go-fcm) |
| [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) | [service/fcm](service/fcm) | [appleboy/go-fcm](https://github.com/appleboy/go-fcm) |
| [Lark](https://www.larksuite.com/) | [service/lark](service/lark) | [go-lark/lark](https://github.com/go-lark/lark) |
| [Line](https://line.me) | [service/line](service/line) | [line/line-bot-sdk-go](https://github.com/line/line-bot-sdk-go) |
| [Line Notify](https://notify-bot.line.me) | [service/line](service/line) | [utahta/go-linenotify](https://github.com/utahta/go-linenotify) |
| [Mailgun](https://www.mailgun.com) | [service/mailgun](service/mailgun) | [mailgun/mailgun-go](https://github.com/mailgun/mailgun-go) |

1
go.mod
View File

@ -35,6 +35,7 @@ require (
require (
github.com/appleboy/go-fcm v0.1.5
github.com/go-lark/lark v1.7.2
github.com/google/go-cmp v0.5.8
github.com/kevinburke/twilio-go v0.0.0-20220615032439-b0fe9b151b0e
)

5
go.sum
View File

@ -83,6 +83,8 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-lark/lark v1.7.2 h1:F2LlwbRrZVuHPs8lz1D7owDevUndPy88Hbw6ZXaho/A=
github.com/go-lark/lark v1.7.2/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc h1:jZY+lpZB92nvBo2f31oPC/ivGll6NcsnEOORm8Fkr4M=
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc/go.mod h1:25mL1NKxbJhB63ihiK8MnNeTRd+xAizd6bOdydrTLUQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@ -136,6 +138,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -334,5 +338,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

99
service/lark/README.md Normal file
View File

@ -0,0 +1,99 @@
# Lark
## Prerequisites
Depending on your requirements, you'll need either a custom app or a Lark group
chat webhook. The latter is easier to set up, but can only send messages to the
group it is in. You may refer to the doc
[here](https://open.larksuite.com/document/uAjLw4CM/ukTMukTMuhttps://open.larksuite.com/document/home/develop-a-bot-in-5-minutes/create-an-appkTM/bot-v3/use-custom-bots-in-a-group)
to set up a webhook bot, and the doc
[here](https://open.larksuite.com/document/home/develop-a-bot-in-5-minutes/create-an-app)
to set up a custom app.
## Usage
### Webhook
For webhook bots, we only need the webhook URL, which might look something like
`https://open.feishu.cn/open-apis/bot/v2/hook/xxx`. Note that there is no
method to configure receivers, because the webhook bot can only send messages
to the group in which it was created.
```go
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/lark"
)
// Replace this with your own webhook URL.
const webHookURL = "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
func main() {
larkWebhookSvc := lark.NewWebhookService(webHookURL)
notifier := notify.New()
notifier.UseServices(larkWebhookSvc)
if err := notifier.Send(context.Background(), "subject", "message"); err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}
log.Println("notification sent")
}
```
### Custom App
For custom apps, we need to pass in the App ID and App Secret when creating a
new notification service. When adding receivers, the type of the receiver ID
must be specified, as shown in the example below. You may refer to the section
entitled "Query parameters" in the doc
[here](https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create)
for more information about the different ID types.
```go
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/lark"
)
// Replace these with the credentials from your custom app.
const (
appId = "xxx"
appSecret = "xxx"
)
func main() {
larkCustomAppService := lark.NewCustomAppService(appId, appSecret)
// Lark implements five types of receiver IDs. You'll need to specify the
// type using the respective helper functions when adding them as receivers.
larkCustomAppService.AddReceivers(
lark.OpenID("xxx"),
lark.UserID("xxx"),
lark.UnionID("xxx"),
lark.Email("xyz@example.com"),
lark.ChatID("xxx"),
)
notifier := notify.New()
notifier.UseServices(larkCustomAppService)
if err := notifier.Send(context.Background(), "subject", "message"); err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}
log.Println("notification sent")
}
```

58
service/lark/common.go Normal file
View File

@ -0,0 +1,58 @@
package lark
// sender is an interface for sending a message to an already defined receiver.
type sender interface {
Send(subject, message string) error
}
// sender is an interface for sending a message to a specific receiver ID.
type sendToer interface {
SendTo(subject, message, id, idType string) error
}
// receiverID encapsulates a receiver ID and its type in Lark.
type receiverID struct {
id string
typ receiverIDType
}
// OpenID specifies an ID as a Lark Open ID.
func OpenID(s string) *receiverID {
return &receiverID{s, openID}
}
// UserID specifies an ID as a Lark User ID.
func UserID(s string) *receiverID {
return &receiverID{s, userID}
}
// UnionID specifies an ID as a Lark Union ID.
func UnionID(s string) *receiverID {
return &receiverID{s, unionID}
}
// Email specifies a receiver ID as an email.
func Email(s string) *receiverID {
return &receiverID{s, email}
}
// ChatID specifies an ID as a Lark Chat ID.
func ChatID(s string) *receiverID {
return &receiverID{s, chatID}
}
// receiverIDType represents the different ID types implemented by Lark. This
// information is required when sending a message. More information about the
// different ID types can be found in the "Query parameters" section of
// the https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create,
// or on
// https://open.larksuite.com/document/home/user-identity-introduction/introduction.
type receiverIDType string
const (
openID receiverIDType = "open_id"
userID receiverIDType = "user_id"
unionID receiverIDType = "union_id"
email receiverIDType = "email"
chatID receiverIDType = "chat_id"
)

111
service/lark/custom_app.go Normal file
View File

@ -0,0 +1,111 @@
package lark
import (
"context"
"fmt"
"net/http"
"time"
"github.com/go-lark/lark"
"github.com/nikoksr/notify"
)
type customAppService struct {
receiveIDs []*receiverID
cli sendToer
}
// Compile time check that larkCustomAppService implements notify.Notifer.
var _ notify.Notifier = &customAppService{}
// NewCustomAppService returns a new instance of a Lark notify service using a
// Lark custom app.
func NewCustomAppService(appID, appSecret string) *customAppService {
bot := lark.NewChatBot(appID, appSecret)
// We need to set the bot to use Lark's open.larksuite.com domain instead of
// the default open.feishu.cn domain.
bot.SetDomain(lark.DomainLark)
// Let the bot use a HTTP client with a longer timeout than the default 5
// seconds.
bot.SetClient(&http.Client{
Timeout: 8 * time.Second,
})
_ = bot.StartHeartbeat()
return &customAppService{
receiveIDs: make([]*receiverID, 0),
cli: &larkClientGoLarkChatBot{
bot: bot,
},
}
}
// AddReceivers adds recipients to future notifications. There are five different
// types of receiver IDs available in Lark and they must be specified here. For
// example:
//
// larkService.AddReceivers(
// lark.OpenID("ou_c99c5f35d542efc7ee492afe11af19ef"),
// lark.UserID("8335aga2"),
// lark.UnionID("on_cad4860e7af114fb4ff6c5d496d1dd76"),
// lark.Email("xyz@example.com"),
// lark.ChatID("oc_a0553eda9014c201e6969b478895c230"),
// )
func (c *customAppService) AddReceivers(ids ...*receiverID) {
c.receiveIDs = append(c.receiveIDs, ids...)
}
// Send takes a message subject and a message body and sends them to all
// previously registered recipient IDs.
func (c *customAppService) Send(ctx context.Context, subject, message string) error {
for _, id := range c.receiveIDs {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := c.cli.SendTo(subject, message, id.id, string(id.typ)); err != nil {
return err
}
}
}
return nil
}
// larkClientGoLarkChatBot is a wrapper around go-lark/lark's Bot, to be used
// for sending messages with custom apps.
type larkClientGoLarkChatBot struct {
bot *lark.Bot
}
// SendTo implements the sendToer interface using a go-lark/lark chat bot.
func (l *larkClientGoLarkChatBot) SendTo(subject, message, receiverID, idType string) error {
content := lark.NewPostBuilder().
Title(subject).
TextTag(message, 1, false).
Render()
msg := lark.NewMsgBuffer(lark.MsgPost).Post(content)
switch receiverIDType(idType) {
case openID:
msg.BindOpenID(receiverID)
case userID:
msg.BindUserID(receiverID)
case unionID:
msg.BindUnionID(receiverID)
case email:
msg.BindEmail(receiverID)
case chatID:
msg.BindChatID(receiverID)
}
res, err := l.bot.PostMessage(msg.Build())
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
if res.Code != 0 {
return fmt.Errorf("send failed with error code %d, please see https://open.larksuite.com/document/ukTMukTMukTM/ugjM14COyUjL4ITN for details", res.Code)
}
return nil
}

View File

@ -0,0 +1,80 @@
package lark
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddReceivers(t *testing.T) {
t.Parallel()
xs := []*receiverID{
OpenID("ou_c99c5f35d542efc7ee492afe11af19ef"),
UserID("8335aga2"),
}
svc := NewCustomAppService("", "")
svc.AddReceivers(xs...)
assert.ElementsMatch(t, svc.receiveIDs, xs)
// Test if adding more receivers afterwards works.
ys := []*receiverID{
UnionID("on_cad4860e7af114fb4ff6c5d496d1dd76"),
Email("xyz@example.com"),
ChatID("oc_a0553eda9014c201e6969b478895c230"),
}
svc.AddReceivers(ys...)
assert.ElementsMatch(t, svc.receiveIDs, append(xs, ys...))
}
func TestSendCustomApp(t *testing.T) {
t.Parallel()
ctx := context.Background()
assert := assert.New(t)
tests := []*receiverID{
OpenID("ou_c99c5f35d542efc7ee492afe11af19ef"),
UserID("8335aga2"),
UnionID("on_cad4860e7af114fb4ff6c5d496d1dd76"),
Email("xyz@example.com"),
ChatID("oc_a0553eda9014c201e6969b478895c230"),
}
// First, test for when the sender returns an error.
for _, tt := range tests {
mockSendToer := NewSendToer(t)
mockSendToer.
On("SendTo", "subject", "message", tt.id, string(tt.typ)).
Return(errors.New(""))
svc := NewCustomAppService("", "")
svc.cli = mockSendToer
svc.AddReceivers(tt)
err := svc.Send(ctx, "subject", "message")
assert.NotNil(err)
mockSendToer.AssertExpectations(t)
}
// Then test for when the sender does not return an error.
for _, tt := range tests {
mockSendToer := NewSendToer(t)
mockSendToer.
On("SendTo", "subject", "message", tt.id, string(tt.typ)).
Return(nil)
svc := NewCustomAppService("", "")
svc.cli = mockSendToer
svc.AddReceivers(tt)
err := svc.Send(ctx, "subject", "message")
assert.Nil(err)
mockSendToer.AssertExpectations(t)
}
}

53
service/lark/doc.go Normal file
View File

@ -0,0 +1,53 @@
/*
Package lark provides message notification integration for Lark. Two kinds of
bots on Lark are supported -- webhooks and custom apps. For information on
webhook bots, see
https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/bot-v3/use-custom-bots-in-a-group,
and for info on custom apps, see
https://open.larksuite.com/document/home/develop-a-bot-in-5-minutes/create-an-app.
Usage:
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/lark"
)
const (
webhookURL = "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
appId = "xxx"
appSecret = "xxx"
)
func main() {
// Two types of services are available depending on your requirements.
larkWebhookService := lark.NewWebhookService(webhookURL)
larkCustomAppService := lark.NewCustomAppService(appId, appSecret)
// Lark implements five types of receiver IDs. You'll need to specify the
// type using the respective helper functions when adding them as receivers
// for the custom app service.
larkCustomAppService.AddReceivers(
lark.OpenID("xxx"),
lark.UserID("xxx"),
lark.UnionID("xxx"),
lark.Email("xyz@example.com"),
lark.ChatID("xxx"),
)
notifier := notify.New()
notifier.UseServices(larkWebhookService, larkCustomAppService)
if err := notifier.Send(context.Background(), "subject", "message"); err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}
log.Println("notification sent")
}
*/
package lark

View File

@ -0,0 +1,39 @@
// Code generated by mockery v2.12.3. DO NOT EDIT.
package lark
import "github.com/stretchr/testify/mock"
// SendToer is an autogenerated mock type for the sendToer type
type SendToer struct {
mock.Mock
}
// SendTo provides a mock function with given fields: subject, message, id, idType
func (_m *SendToer) SendTo(subject string, message string, id string, idType string) error {
ret := _m.Called(subject, message, id, idType)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(subject, message, id, idType)
} else {
r0 = ret.Error(0)
}
return r0
}
type NewSendToerT interface {
mock.TestingT
Cleanup(func())
}
// NewSendToer creates a new instance of SendToer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewSendToer(t NewSendToerT) *SendToer {
mock := &SendToer{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,39 @@
// Code generated by mockery v2.12.3. DO NOT EDIT.
package lark
import "github.com/stretchr/testify/mock"
// Sender is an autogenerated mock type for the sender type
type Sender struct {
mock.Mock
}
// Send provides a mock function with given fields: subject, message
func (_m *Sender) Send(subject string, message string) error {
ret := _m.Called(subject, message)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(subject, message)
} else {
r0 = ret.Error(0)
}
return r0
}
type NewSenderT interface {
mock.TestingT
Cleanup(func())
}
// NewSender creates a new instance of Sender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewSender(t NewSenderT) *Sender {
mock := &Sender{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

58
service/lark/webhook.go Normal file
View File

@ -0,0 +1,58 @@
package lark
import (
"context"
"fmt"
"github.com/go-lark/lark"
"github.com/nikoksr/notify"
)
type webhookService struct {
cli sender
}
// Compile time check that larkCustomAppService implements notify.Notifer.
var _ notify.Notifier = &webhookService{}
// NewWebhookService returns a new instance of a Lark notify service using a
// Lark group chat webhook. Note that this service does not take any
// notification receivers because it can only push messages to the group chat
// it belongs to.
func NewWebhookService(webhookURL string) *webhookService {
bot := lark.NewNotificationBot(webhookURL)
return &webhookService{
cli: &larkClientGoLarkNotificationBot{
bot: bot,
},
}
}
// Send sends the message subject and body to the group chat.
func (w *webhookService) Send(ctx context.Context, subject, message string) error {
return w.cli.Send(subject, message)
}
// larkClientGoLarkNotificationBot is a wrapper around go-lark/lark's Bot, to
// be used for notifications via webhooks only.
type larkClientGoLarkNotificationBot struct {
bot *lark.Bot
}
// Send implements the sender interface using a go-lark/lark notification bot.
func (w *larkClientGoLarkNotificationBot) Send(subject, message string) error {
content := lark.NewPostBuilder().
Title(subject).
TextTag(message, 1, false).
Render()
msg := lark.NewMsgBuffer(lark.MsgPost).Post(content)
res, err := w.bot.PostNotificationV2(msg.Build())
if err != nil {
return fmt.Errorf("failed to post webhook message: %w", err)
}
if res.Code != 0 {
return fmt.Errorf("send failed with error code %d, please see https://open.larksuite.com/document/ukTMukTMukTM/ugjM14COyUjL4ITN for details", res.Code)
}
return nil
}

View File

@ -0,0 +1,44 @@
package lark
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSendWebhook(t *testing.T) {
t.Parallel()
assert := assert.New(t)
ctx := context.Background()
// First, test for when the sender returns an error.
{
mockSender := NewSender(t)
mockSender.
On("Send", "subject", "message").
Return(errors.New(""))
svc := NewWebhookService("")
svc.cli = mockSender
err := svc.Send(ctx, "subject", "message")
assert.NotNil(err)
mockSender.AssertExpectations(t)
}
// Then test for when the sender does not return an error.
{
mockSender := NewSender(t)
mockSender.
On("Send", "subject", "message").
Return(nil)
svc := NewWebhookService("")
svc.cli = mockSender
err := svc.Send(ctx, "subject", "message")
assert.Nil(err)
mockSender.AssertExpectations(t)
}
}