Skip to content

Commit

Permalink
Merge pull request #300 from immanuelhume/feat/lark
Browse files Browse the repository at this point in the history
  • Loading branch information
nikoksr committed Aug 5, 2022
2 parents 32b9410 + 96096f1 commit 2a4c078
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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 changes: 99 additions & 0 deletions service/lark/README.md
Original file line number Diff line number Diff line change
@@ -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("[email protected]"),
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 changes: 58 additions & 0 deletions service/lark/common.go
Original file line number Diff line number Diff line change
@@ -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 changes: 111 additions & 0 deletions service/lark/custom_app.go
Original file line number Diff line number Diff line change
@@ -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("[email protected]"),
// 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
}
80 changes: 80 additions & 0 deletions service/lark/custom_app_test.go
Original file line number Diff line number Diff line change
@@ -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("[email protected]"),
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("[email protected]"),
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)
}
}
Loading

0 comments on commit 2a4c078

Please sign in to comment.