Skip to content

Commit

Permalink
实现channel (#5)
Browse files Browse the repository at this point in the history
* 接入腾讯云邮件发送

* 接入twilio短信

* 一些修改

* 接入push & 增加http抽象

* 简单接入slog

* 修改content定义 & 统一使用slog输出日志 & 修改example

* 修改ci-go版本为1.21.3
  • Loading branch information
hookokoko authored Oct 19, 2023
1 parent f2ef4bb commit 56fbe58
Show file tree
Hide file tree
Showing 24 changed files with 717 additions and 413 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/notify-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19.0
go-version: 1.21.3

- name: Build
run: go build -v ./...
Expand Down
79 changes: 62 additions & 17 deletions channel/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,92 @@ package email

import (
"context"
"crypto/tls"
"net"
"net/smtp"
"time"

"github.com/ecodeclub/ekit/slice"
"github.com/ecodeclub/notify-go/pkg/notifier"
"github.com/ecodeclub/notify-go/tool"

"github.com/jordan-wright/email"
"github.com/pkg/errors"
)

type Config struct {
Addr string
Auth smtp.Auth
SmtpHostAddr string `json:"smtp_host_addr"`
SmtpUserName string `json:"smtp_user_name"`
SmtpPwd string `json:"smtp_pwd"`
SenderAddress string `json:"sender_address"` // e.g. Hooko <xxx.xx@xx>
}

type ChannelEmailImpl struct {
EmailClient *email.Email
cfg Config
email *email.Email
smtpAuth smtp.Auth
smtpHost string
Config
}

type Content struct {
From []string
Subject []string
Subject string
Cc []string
Html []string
Text []string
Html string
Text string
}

func NewEmailChannel(cfg Config) *ChannelEmailImpl {
host, _, _ := net.SplitHostPort(cfg.SmtpHostAddr)
return &ChannelEmailImpl{
EmailClient: email.NewEmail(),
cfg: cfg,
email: email.NewEmail(),
smtpHost: host,
smtpAuth: smtp.PlainAuth("", cfg.SmtpUserName, cfg.SmtpPwd, host),
Config: cfg,
}
}

func (c *ChannelEmailImpl) Execute(ctx context.Context, deli notifier.Delivery) error {
// Mock time cost
n := tool.RandIntN(700, 800)
time.Sleep(time.Millisecond * time.Duration(n))
return nil
var err error
msgContent := c.initEmailContent(deli.Content)

c.email.To = slice.Map[notifier.Receiver, string](deli.Receivers, func(idx int, src notifier.Receiver) string {
return src.Email
})

c.email.From = c.SenderAddress
// TODO cc不是抄送, 而是append到to内
c.email.Cc = msgContent.Cc
c.email.Subject = msgContent.Subject
c.email.HTML = []byte(msgContent.Html)
c.email.Text = []byte(msgContent.Text)

ch := make(chan struct{})
go func() {
defer func() {
close(ch)
}()
// TODO 如果SendWithTLS执行时间太长, 有goroutine泄露问题
// 需要改造SendWithTLS 为 SendWithTLSContext()
err = c.email.SendWithTLS(c.SmtpHostAddr, c.smtpAuth, &tls.Config{ServerName: c.smtpHost})
}()

select {
case <-ctx.Done():
err = ctx.Err()
case <-ch:
if err != nil {
err = errors.Wrap(err, "failed to send mail")
}
}

return err
}

func (c *ChannelEmailImpl) Name() string {
return "email"
}

func (c *ChannelEmailImpl) initEmailContent(nc notifier.Content) Content {
cc := Content{
Subject: nc.Title,
Html: string(nc.Data),
}
return cc
}
29 changes: 29 additions & 0 deletions channel/email/email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package email

import (
"testing"
)

func TestSend(t *testing.T) {
//e := NewEmailChannel(Config{
// SenderAddress: "Hooko <[email protected]>",
// SmtpHostAddr: "gz-smtp.qcloudmail.com:465",
// SmtpUserName: "[email protected]",
// SmtpPwd: "xxx",
//})
//
//deli := notifier.Delivery{
// Receivers: []notifier.Receiver{
// {Email: "[email protected]"},
// },
// Content: notifier.Content{
// Title: "发送主题-测试",
// Data: []byte("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>hello world</title>\n</head>\n<body>\n " +
// "<h1>我的第一个标题</h1>\n <p>我的第一个段落。</p>\n</body>\n</html>"),
// },
//}
//ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
//defer cancel()
//err := e.Execute(ctx, deli)
//t.Log(err)
}
150 changes: 138 additions & 12 deletions channel/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,155 @@ package push

import (
"context"
"time"

"crypto/sha256"
"fmt"
"github.com/ecodeclub/ekit/slice"
"github.com/ecodeclub/notify-go/pkg/notifier"
"github.com/ecodeclub/notify-go/tool"
"github.com/ecodeclub/notify-go/pkg/ral"
"github.com/pborman/uuid"
"github.com/pkg/errors"
"strconv"
"time"
)

type Config struct{}
type Config struct {
AppKey string `json:"app_key"`
MasterSecret string `json:"master_secret"`
AppId string `json:"app_id"`
}

type ChannelPushImpl struct {
config Config
client ral.Client
}

// Content 个推的请求参数
type Content struct {
RequestID string `json:"request_id"`
Settings Settings `json:"settings"`
Audience Audience `json:"audience"`
PushMessage PushMessage `json:"push_message"`
}

type Settings struct {
TTL int `json:"ttl"`
}

type Audience struct {
Cid []string `json:"cid"`
}

type Notification struct {
Title string `json:"title"`
Body string `json:"body"`
ClickType string `json:"click_type"`
URL string `json:"url"`
}

type ChannelPushImpl struct{}
type PushMessage struct {
Notification Notification `json:"notification"`
}

type Content struct{}
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data map[string]string `json:"data"`
}

func NewPushChannel(c Config) *ChannelPushImpl {
return &ChannelPushImpl{}
func NewPushChannel(c Config, client ral.Client) *ChannelPushImpl {
pc := &ChannelPushImpl{
client: client,
config: c,
}
return pc
}

func (pc *ChannelPushImpl) Execute(ctx context.Context, deli notifier.Delivery) error {
// Mock time cost
n := tool.RandIntN(700, 800)
time.Sleep(time.Millisecond * time.Duration(n))
return nil
token, err := pc.getToken(ctx)
if err != nil {
return err
}

content := pc.initPushContent(deli.Content)
if ctx.Value("req_id") != nil {
content.RequestID = ctx.Value("req_id").(string)
} else {
content.RequestID = uuid.NewUUID().String()
}

userIds := slice.Map[notifier.Receiver, string](deli.Receivers, func(idx int, recv notifier.Receiver) string {
return recv.UserId
})
content.Audience.Cid = append(content.Audience.Cid, userIds...)

req := ral.Request{
Header: map[string]string{
"content-type": "application/json;charset=utf-8",
"token": token,
},
PathParams: map[string]string{"app_id": pc.config.AppId},
Body: content,
}

var resp map[string]any
err = pc.client.Ral(ctx, "Send", req, &resp, map[string]any{})

return err
}

func (pc *ChannelPushImpl) Name() string {
return "push"
}

func (pc *ChannelPushImpl) getToken(ctx context.Context) (token string, err error) {
ts, sign := pc.getSign()
req := ral.Request{
Header: map[string]string{"content-type": "application/json;charset=utf-8"},
Body: map[string]interface{}{
"sign": sign,
"timestamp": ts,
"appkey": pc.config.AppKey,
},
PathParams: map[string]string{"app_id": pc.config.AppId},
}

var respSucc Result
err = pc.client.Ral(ctx, "Auth", req, &respSucc, map[string]any{})
if err != nil {
return
}
var ok bool
token, ok = respSucc.Data["token"]

if !ok {
err = errors.New("[push] 获取token失败")
}
return
}

func (pc *ChannelPushImpl) getSign() (timestamp string, sign string) {
timestamp = strconv.FormatInt(time.Now().UnixMilli(), 10)
dataToSign := pc.config.AppKey + timestamp + pc.config.MasterSecret

// 计算SHA-256哈希
sha256Hash := sha256.Sum256([]byte(dataToSign))

// 将哈希结果转换为十六进制字符串
sign = fmt.Sprintf("%x", sha256Hash)

return
}

func (pc *ChannelPushImpl) initPushContent(nc notifier.Content) Content {
c := Content{
Settings: Settings{TTL: 7200000},
Audience: Audience{Cid: make([]string, 0, 1)},
PushMessage: PushMessage{Notification: Notification{
Title: nc.Title,
Body: string(nc.Data),
ClickType: nc.ClickType,
URL: nc.URL},
},
}
return c
}
54 changes: 45 additions & 9 deletions channel/sms/sms.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,65 @@ package sms

import (
"context"
"time"
"net/url"

"github.com/ecodeclub/notify-go/pkg/notifier"
"github.com/ecodeclub/notify-go/tool"
"github.com/kevinburke/twilio-go"
"github.com/pkg/errors"
)

type Config struct{}
type Config struct {
AccountSID string `json:"account_sid"`
AuthToken string `json:"auth_token"`
FromPhoneNumber string `json:"from_phone_number"`
}

type ChannelSmsImpl struct{}
type twilioClient interface {
SendMessage(from, to, body string, mediaURLs []*url.URL) (*twilio.Message, error)
}

type Content struct{}
type ChannelSmsImpl struct {
client twilioClient
fromPhoneNumber string
}

type Content struct {
Data string
}

func NewSmsChannel(c Config) *ChannelSmsImpl {
return &ChannelSmsImpl{}
client := twilio.NewClient(c.AccountSID, c.AuthToken, nil)
return &ChannelSmsImpl{
client: client.Messages,
fromPhoneNumber: c.FromPhoneNumber,
}
}

func (sc *ChannelSmsImpl) Execute(ctx context.Context, deli notifier.Delivery) error {
// Mock time cost
n := tool.RandIntN(700, 800)
time.Sleep(time.Millisecond * time.Duration(n))
msgContent := sc.initSMSContent(deli.Content)

for _, recv := range deli.Receivers {
select {
case <-ctx.Done():
return ctx.Err()
default:
_, err := sc.client.SendMessage(sc.fromPhoneNumber, recv.Phone, msgContent.Data, []*url.URL{})
if err != nil {
return errors.Wrapf(err, "failed to send message to phone number '%s' using Twilio", recv.Phone)
}
}
}

return nil
}

func (sc *ChannelSmsImpl) Name() string {
return "sms"
}

func (sc *ChannelSmsImpl) initSMSContent(nc notifier.Content) Content {
c := Content{
Data: string(nc.Data),
}
return c
}
Loading

0 comments on commit 56fbe58

Please sign in to comment.