1
0
mirror of https://github.com/nikoksr/notify.git synced 2025-01-07 23:01:59 +02:00

Merge branch 'main' into main

This commit is contained in:
Niko Köser 2022-08-05 10:21:01 +02:00 committed by GitHub
commit 541fdf294b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 888 additions and 0 deletions

View File

@ -86,6 +86,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [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) |
| [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) |
@ -98,6 +99,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [Syslog](https://wikipedia.org/wiki/Syslog) | [service/syslog](service/syslog) | [log/syslog](https://pkg.go.dev/log/syslog) |
| [Telegram](https://telegram.org) | [service/telegram](service/telegram) | [go-telegram-bot-api/telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api) |
| [TextMagic](https://www.textmagic.com) | [service/textmagic](service/textmagic) | [textmagic/textmagic-rest-go-v2](https://github.com/textmagic/textmagic-rest-go-v2) |
| [Twilio](https://www.twilio.com/) | [service/twilio](service/twilio) | [kevinburke/twilio-go](https://github.com/kevinburke/twilio-go) |
| [Twitter](https://twitter.com) | [service/twitter](service/twitter) | [dghubble/go-twitter](https://github.com/dghubble/go-twitter) |
| [WeChat](https://www.wechat.com) | [service/wechat](service/wechat) | [silenceper/wechat](https://github.com/silenceper/wechat) |
| [WhatsApp](https://www.whatsapp.com) | [service/whatsapp](service/whatsapp) | [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp) |

8
go.mod
View File

@ -35,7 +35,9 @@ 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
)
require (
@ -55,6 +57,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
@ -62,6 +65,8 @@ require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -76,10 +81,13 @@ require (
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b // indirect
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c // indirect
golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

17
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=
@ -90,6 +92,8 @@ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaEL
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -134,11 +138,19 @@ 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=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 h1:K8qael4LemsmJCGt+ccI8b0fCNFDttmEu3qtpFt3G0M=
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7/go.mod h1:/Pk5i/SqYdYv1cie5wGwoZ4P6TpgMi+Yf58mtJSHdOw=
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c h1:hnbwWED5rIu+UaMkLR3JtnscMVGqp35lfzQwLuZAAUY=
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I=
github.com/kevinburke/twilio-go v0.0.0-20220615032439-b0fe9b151b0e h1:2HUamy+op/UxwJxDIg19oy/tIO/2M2tSasvihvhex4s=
github.com/kevinburke/twilio-go v0.0.0-20220615032439-b0fe9b151b0e/go.mod h1:PDdDH7RSKjjy9iFyoMzfeChOSmXpXuMEUqmAJSihxx4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
@ -223,6 +235,10 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4=
github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -322,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)
}
}

43
service/twilio/README.md Normal file
View File

@ -0,0 +1,43 @@
# Twilio (Message Service)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/nikoksr/notify/service/twilio)
## Prerequisites
Navigate to Twilio [console](https://console.twilio.com/), create a new account or login with an existing one.
You will find the `Account SID` and the `Auth Token` under the `Account Info` tab. You may also request a Twilio phone number, if required.
To test the integration with a phone number you can just use the sample code below.
## Usage
```go
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/twilio"
)
func main() {
twilioSvc, err := twilio.New("account_sid", "auth_token", "your_phone_number")
if err != nil {
log.Fatalf("twilio.New() failed: %s", err.Error())
}
twilioSvc.AddReceivers("recipient_phone_number")
notifier := notify.New()
notifier.UseServices(twilioSvc)
err = notifier.Send(context.Background(), "subject", "message")
if err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}
log.Println("notification sent")
}
```

35
service/twilio/doc.go Normal file
View File

@ -0,0 +1,35 @@
/*
Package twilio provides message notification integration for Twilio (Message Service).
Usage:
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/twilio"
)
func main() {
twilioSvc, err := twilio.New("account_sid", "auth_token", "your_phone_number")
if err != nil {
log.Fatalf("twilio.New() failed: %s", err.Error())
}
twilioSvc.AddReceivers("recipient_phone_number")
notifier := notify.New()
notifier.UseServices(twilioSvc)
err = notifier.Send(context.Background(), "subject", "message")
if err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}
log.Println("notification sent")
}
*/
package twilio

View File

@ -0,0 +1,49 @@
// Code generated by mockery v2.12.2. DO NOT EDIT.
package twilio
import (
url "net/url"
testing "testing"
twilio "github.com/kevinburke/twilio-go"
mock "github.com/stretchr/testify/mock"
)
// mockTwilioClient is an autogenerated mock type for the twilioClient type
type mockTwilioClient struct {
mock.Mock
}
// SendMessage provides a mock function with given fields: from, to, body, mediaURLs
func (_m *mockTwilioClient) SendMessage(from string, to string, body string, mediaURLs []*url.URL) (*twilio.Message, error) {
ret := _m.Called(from, to, body, mediaURLs)
var r0 *twilio.Message
if rf, ok := ret.Get(0).(func(string, string, string, []*url.URL) *twilio.Message); ok {
r0 = rf(from, to, body, mediaURLs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*twilio.Message)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, []*url.URL) error); ok {
r1 = rf(from, to, body, mediaURLs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// newMockTwilioClient creates a new instance of mockTwilioClient. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func newMockTwilioClient(t testing.TB) *mockTwilioClient {
mock := &mockTwilioClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

63
service/twilio/twilio.go Normal file
View File

@ -0,0 +1,63 @@
package twilio
import (
"context"
"net/url"
"github.com/kevinburke/twilio-go"
"github.com/pkg/errors"
)
// Compile-time check that twilio.MessageService satisfies twilioClient interface.
var _ twilioClient = &twilio.MessageService{}
// twilioClient abstracts twilio-go MessageService for writing unit tests
type twilioClient interface {
SendMessage(from, to, body string, mediaURLs []*url.URL) (*twilio.Message, error)
}
// Service encapsulates the Twilio Message Service client along with internal state for storing recipient phone numbers.
type Service struct {
client twilioClient
fromPhoneNumber string
toPhoneNumbers []string
}
// New returns a new instance of Twilio notification service.
func New(accountSID, authToken, fromPhoneNumber string) (*Service, error) {
client := twilio.NewClient(accountSID, authToken, nil)
s := &Service{
client: client.Messages,
fromPhoneNumber: fromPhoneNumber,
toPhoneNumbers: []string{},
}
return s, nil
}
// AddReceivers takes strings of recipient phone numbers and appends them to the internal phone numbers slice.
// The Send method will send a given message to all those phone numbers.
func (s *Service) AddReceivers(phoneNumbers ...string) {
s.toPhoneNumbers = append(s.toPhoneNumbers, phoneNumbers...)
}
// Send takes a message subject and a message body and sends them to all previously set phone numbers.
func (s *Service) Send(ctx context.Context, subject, message string) error {
body := subject + "\n" + message
for _, toPhoneNumber := range s.toPhoneNumbers {
select {
case <-ctx.Done():
return ctx.Err()
default:
_, err := s.client.SendMessage(s.fromPhoneNumber, toPhoneNumber, body, []*url.URL{})
if err != nil {
return errors.Wrapf(err, "failed to send message to phone number '%s' using Twilio", toPhoneNumber)
}
}
}
return nil
}

View File

@ -0,0 +1,90 @@
package twilio
import (
"context"
"errors"
"fmt"
"net/url"
testing "testing"
twilio "github.com/kevinburke/twilio-go"
"github.com/stretchr/testify/require"
)
func TestAddReceivers(t *testing.T) {
t.Parallel()
assert := require.New(t)
svc := &Service{
toPhoneNumbers: []string{},
}
toPhoneNumbers := []string{"PhoneNumber1", "PhoneNumber2", "PhoneNumber3"}
svc.AddReceivers(toPhoneNumbers...)
assert.Equal(svc.toPhoneNumbers, toPhoneNumbers)
}
func TestSend(t *testing.T) {
t.Parallel()
assert := require.New(t)
svc := &Service{
fromPhoneNumber: "my_phone_number",
toPhoneNumbers: []string{},
}
mockPhoneNumber := "recipient_phone_number"
mockBody := "subject\nmessage"
mockError := errors.New("some error")
// test twilio client send
mockClient := newMockTwilioClient(t)
mockClient.On("SendMessage",
svc.fromPhoneNumber,
mockPhoneNumber,
mockBody,
[]*url.URL{}).Return(&twilio.Message{Body: "a response message"}, nil)
svc.client = mockClient
svc.AddReceivers(mockPhoneNumber)
err := svc.Send(context.Background(), "subject", "message")
assert.Nil(err)
mockClient.AssertExpectations(t)
// test twilio client send returning error
mockClient = newMockTwilioClient(t)
mockClient.On("SendMessage",
svc.fromPhoneNumber,
mockPhoneNumber,
mockBody,
[]*url.URL{}).Return(nil, mockError)
svc.client = mockClient
svc.AddReceivers(mockPhoneNumber)
err = svc.Send(context.Background(), "subject", "message")
assert.NotNil(err)
assert.Equal(
fmt.Sprintf("failed to send message to phone number '%s' using Twilio: %s", mockPhoneNumber, mockError.Error()),
err.Error())
mockClient.AssertExpectations(t)
// test twilio client send multiple receivers
anotherMockPhoneNumber := "another_recipient_phone_number"
mockClient = newMockTwilioClient(t)
mockClient.On("SendMessage",
svc.fromPhoneNumber,
mockPhoneNumber,
mockBody,
[]*url.URL{}).Return(&twilio.Message{Body: "a response message"}, nil)
mockClient.On("SendMessage",
svc.fromPhoneNumber,
anotherMockPhoneNumber,
mockBody,
[]*url.URL{}).Return(&twilio.Message{Body: "a response message"}, nil)
svc.client = mockClient
svc.AddReceivers(mockPhoneNumber)
svc.AddReceivers(anotherMockPhoneNumber)
err = svc.Send(context.Background(), "subject", "message")
assert.Nil(err)
mockClient.AssertExpectations(t)
}