├── .gitignore ├── config ├── const.go ├── funcs.go └── yaml.go ├── control ├── corp └── corp.go ├── cron └── sender.go ├── dataobj └── message.go ├── etc ├── wechat-sender.service ├── wechat-sender.yml └── wechat.tpl ├── go.mod ├── go.sum ├── http ├── http.go ├── middleware │ ├── logger.go │ └── recovery.go ├── render │ └── render.go └── router │ ├── funcs.go │ └── router.go ├── main.go ├── readme.md └── redisc ├── poper.go └── redis.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.dylib 5 | *.test 6 | *.out 7 | *.prof 8 | *.log 9 | *.o 10 | *.a 11 | *.so 12 | *.sw[po] 13 | *.tar.gz 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | _testmain.go 23 | _obj 24 | _test 25 | 26 | /log* 27 | /bin 28 | /out 29 | /dist 30 | /etc/*.local.yml 31 | 32 | .idea 33 | .data 34 | .vscode 35 | .DS_Store 36 | .cache-loader 37 | 38 | /wechat-sender* 39 | -------------------------------------------------------------------------------- /config/const.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const Version = "1.0.0" 4 | -------------------------------------------------------------------------------- /config/funcs.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/n9e/wechat-sender/corp" 9 | "github.com/toolkits/pkg/logger" 10 | ) 11 | 12 | // InitLogger init logger toolkits 13 | func InitLogger() { 14 | c := Get().Logger 15 | 16 | lb, err := logger.NewFileBackend(c.Dir) 17 | if err != nil { 18 | fmt.Println("cannot init logger:", err) 19 | os.Exit(1) 20 | } 21 | 22 | lb.SetRotateByHour(true) 23 | lb.SetKeepHours(c.KeepHours) 24 | 25 | logger.SetLogging(c.Level, lb) 26 | } 27 | 28 | func Test(args []string) { 29 | c := Get() 30 | 31 | chatClient := corp.New(c.WeChat.CorpID, c.WeChat.AgentID, c.WeChat.Secret) 32 | 33 | if len(args) == 0 { 34 | fmt.Println("user not given") 35 | os.Exit(1) 36 | } 37 | 38 | for i := 0; i < len(args); i++ { 39 | err := chatClient.Send(corp.Message{ 40 | ToUser: args[i], 41 | MsgType: "text", 42 | Text: corp.Content{Content: fmt.Sprintf("test message from n9e at %v", time.Now())}, 43 | }) 44 | 45 | if err != nil { 46 | fmt.Printf("send to %s fail: %v\n", args[i], err) 47 | } else { 48 | fmt.Printf("send to %s succ\n", args[i]) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/toolkits/pkg/file" 7 | ) 8 | 9 | type Config struct { 10 | Logger loggerSection `yaml:"logger"` 11 | WeChat wechatSection `yaml:"wechat"` 12 | Consumer consumerSection `yaml:"consumer"` 13 | Redis redisSection `yaml:"redis"` 14 | HTTP httpSection `yaml:"http"` 15 | } 16 | 17 | type loggerSection struct { 18 | Dir string `yaml:"dir"` 19 | Level string `yaml:"level"` 20 | KeepHours uint `yaml:"keepHours"` 21 | } 22 | 23 | type httpSection struct { 24 | Listen string `yaml:"listen"` 25 | } 26 | 27 | type redisSection struct { 28 | Addr string `yaml:"addr"` 29 | Pass string `yaml:"pass"` 30 | DB int `yaml:"db"` 31 | Idle int `yaml:"idle"` 32 | Timeout timeoutSection `yaml:"timeout"` 33 | } 34 | 35 | type timeoutSection struct { 36 | Conn int `yaml:"conn"` 37 | Read int `yaml:"read"` 38 | Write int `yaml:"write"` 39 | } 40 | 41 | type consumerSection struct { 42 | Enable bool `yaml:"enable"` 43 | Queue string `yaml:"queue"` 44 | Worker int `yaml:"worker"` 45 | } 46 | 47 | type wechatSection struct { 48 | CorpID string `yaml:"corp_id"` 49 | AgentID int `yaml:"agent_id"` 50 | Secret string `yaml:"secret"` 51 | } 52 | 53 | var yaml Config 54 | 55 | func Get() Config { 56 | return yaml 57 | } 58 | 59 | func ParseConfig(yf string) error { 60 | err := file.ReadYaml(yf, &yaml) 61 | if err != nil { 62 | return fmt.Errorf("cannot read yml[%s]: %v", yf, err) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | binfile=wechat-sender 4 | 5 | cwd=$(cd $(dirname $0)/; pwd) 6 | cd $cwd 7 | 8 | usage() 9 | { 10 | echo $"Usage: $0 {start|stop|restart|status|build|pack}" 11 | exit 0 12 | } 13 | 14 | start() 15 | { 16 | if [ ! -f $binfile ]; then 17 | echo "file[$binfile] not found" 18 | exit 1 19 | fi 20 | 21 | if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then 22 | echo "${binfile} already started" 23 | return 24 | fi 25 | 26 | mkdir -p logs/$binfile 27 | nohup $cwd/$binfile &> logs/${binfile}/stdout.log & 28 | 29 | for((i=1;i<=15;i++)); do 30 | if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then 31 | echo "${binfile} started" 32 | return 33 | fi 34 | sleep 0.2 35 | done 36 | 37 | echo "cannot start ${binfile}" 38 | exit 1 39 | } 40 | 41 | stop() 42 | { 43 | if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then 44 | echo "${binfile} already stopped" 45 | return 46 | fi 47 | 48 | ps aux|grep -v grep|grep -v control|grep "$binfile"|awk '{print $2}'|xargs kill 49 | for((i=1;i<=15;i++)); do 50 | if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then 51 | echo "${binfile} stopped" 52 | return 53 | fi 54 | sleep 0.2 55 | done 56 | 57 | echo "cannot stop ${binfile}" 58 | exit 1 59 | } 60 | 61 | restart() 62 | { 63 | stop 64 | start 65 | status 66 | } 67 | 68 | status() 69 | { 70 | ps aux|grep -v grep|grep ${binfile} 71 | } 72 | 73 | build() 74 | { 75 | go build 76 | } 77 | 78 | reload() 79 | { 80 | build 81 | restart 82 | } 83 | 84 | pack() 85 | { 86 | v=$(date +%Y-%m-%d-%H-%M-%S) 87 | tar zcvf $binfile-$v.tar.gz control $binfile etc/wechat.tpl etc/wechat-sender.yml etc/wechat-sender.service 88 | } 89 | 90 | case "$1" in 91 | start) 92 | start 93 | ;; 94 | stop) 95 | stop 96 | ;; 97 | restart) 98 | restart 99 | ;; 100 | status) 101 | status 102 | ;; 103 | build) 104 | build 105 | ;; 106 | reload) 107 | reload 108 | ;; 109 | pack) 110 | pack 111 | ;; 112 | *) 113 | usage 114 | esac 115 | -------------------------------------------------------------------------------- /corp/corp.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // Err 微信返回错误 14 | type Err struct { 15 | ErrCode int `json:"errcode"` 16 | ErrMsg string `json:"errmsg"` 17 | } 18 | 19 | // AccessToken 微信企业号请求Token 20 | type AccessToken struct { 21 | AccessToken string `json:"access_token"` 22 | ExpiresIn int `json:"expires_in"` 23 | Err 24 | ExpiresInTime time.Time 25 | } 26 | 27 | // Client 微信企业号应用配置信息 28 | type Client struct { 29 | CorpID string 30 | AgentID int 31 | AgentSecret string 32 | Token AccessToken 33 | } 34 | 35 | // Result 发送消息返回结果 36 | type Result struct { 37 | Err 38 | InvalidUser string `json:"invaliduser"` 39 | InvalidParty string `json:"infvalidparty"` 40 | InvalidTag string `json:"invalidtag"` 41 | } 42 | 43 | // Content 文本消息内容 44 | type Content struct { 45 | Content string `json:"content"` 46 | } 47 | 48 | // Message 消息主体参数 49 | type Message struct { 50 | ToUser string `json:"touser"` 51 | ToParty string `json:"toparty"` 52 | ToTag string `json:"totag"` 53 | MsgType string `json:"msgtype"` 54 | AgentID int `json:"agentid"` 55 | Text Content `json:"text"` 56 | } 57 | 58 | // New 实例化微信企业号应用 59 | func New(corpID string, agentID int, AgentSecret string) *Client { 60 | c := new(Client) 61 | c.CorpID = corpID 62 | c.AgentID = agentID 63 | c.AgentSecret = AgentSecret 64 | return c 65 | } 66 | 67 | // Send 发送信息 68 | func (c *Client) Send(msg Message) error { 69 | if err := c.GetAccessToken(); err != nil { 70 | return err 71 | } 72 | 73 | msg.AgentID = c.AgentID 74 | url := "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + c.Token.AccessToken 75 | 76 | resultByte, err := jsonPost(url, msg) 77 | if err != nil { 78 | return fmt.Errorf("invoke send api fail: %v", err) 79 | } 80 | 81 | result := Result{} 82 | err = json.Unmarshal(resultByte, &result) 83 | if err != nil { 84 | return fmt.Errorf("parse send api response fail: %v", err) 85 | } 86 | 87 | if result.ErrCode != 0 { 88 | err = fmt.Errorf("invoke send api return ErrCode = %d", result.ErrCode) 89 | } 90 | 91 | if result.InvalidUser != "" || result.InvalidParty != "" || result.InvalidTag != "" { 92 | err = fmt.Errorf("invoke send api partial fail, invalid user: %s, invalid party: %s, invalid tag: %s", result.InvalidUser, result.InvalidParty, result.InvalidTag) 93 | } 94 | 95 | return err 96 | } 97 | 98 | // GetAccessToken 获取会话token 99 | func (c *Client) GetAccessToken() error { 100 | var err error 101 | 102 | if c.Token.AccessToken == "" || c.Token.ExpiresInTime.Before(time.Now()) { 103 | c.Token, err = getAccessTokenFromWeixin(c.CorpID, c.AgentSecret) 104 | if err != nil { 105 | return fmt.Errorf("invoke getAccessTokenFromWeixin fail: %v", err) 106 | } 107 | c.Token.ExpiresInTime = time.Now().Add(time.Duration(c.Token.ExpiresIn-1000) * time.Second) 108 | } 109 | 110 | return err 111 | } 112 | 113 | // transport 全局复用,提升性能 114 | var transport = &http.Transport{ 115 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 116 | DisableCompression: true, 117 | } 118 | 119 | // getAccessTokenFromWeixin 从微信服务器获取token 120 | func getAccessTokenFromWeixin(corpID, secret string) (accessToken AccessToken, err error) { 121 | url := "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpID + "&corpsecret=" + secret 122 | 123 | client := &http.Client{Transport: transport} 124 | result, err := client.Get(url) 125 | if err != nil { 126 | return accessToken, fmt.Errorf("invoke api gettoken fail: %v", err) 127 | } 128 | 129 | if result.Body == nil { 130 | return accessToken, fmt.Errorf("gettoken response body is nil") 131 | } 132 | 133 | defer result.Body.Close() 134 | 135 | res, err := ioutil.ReadAll(result.Body) 136 | if err != nil { 137 | return accessToken, fmt.Errorf("read gettoken response body fail: %v", err) 138 | } 139 | 140 | err = json.Unmarshal(res, &accessToken) 141 | if err != nil { 142 | return accessToken, fmt.Errorf("parse gettoken response body fail: %v", err) 143 | } 144 | 145 | if accessToken.ExpiresIn == 0 || accessToken.AccessToken == "" { 146 | err = fmt.Errorf("invoke api gettoken fail, ErrCode: %v, ErrMsg: %v", accessToken.ErrCode, accessToken.ErrMsg) 147 | return accessToken, err 148 | } 149 | 150 | return accessToken, err 151 | } 152 | 153 | func jsonPost(url string, data interface{}) ([]byte, error) { 154 | jsonBody, err := encodeJSON(data) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | r, err := http.Post(url, "application/json;charset=utf-8", bytes.NewReader(jsonBody)) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | if r.Body == nil { 165 | return nil, fmt.Errorf("response body of %s is nil", url) 166 | } 167 | 168 | defer r.Body.Close() 169 | 170 | return ioutil.ReadAll(r.Body) 171 | } 172 | 173 | func encodeJSON(v interface{}) ([]byte, error) { 174 | var buf bytes.Buffer 175 | encoder := json.NewEncoder(&buf) 176 | encoder.SetEscapeHTML(false) 177 | if err := encoder.Encode(v); err != nil { 178 | return nil, err 179 | } 180 | return buf.Bytes(), nil 181 | } 182 | -------------------------------------------------------------------------------- /cron/sender.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "strings" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/toolkits/pkg/logger" 12 | "github.com/toolkits/pkg/runner" 13 | 14 | "github.com/n9e/wechat-sender/config" 15 | "github.com/n9e/wechat-sender/corp" 16 | "github.com/n9e/wechat-sender/dataobj" 17 | "github.com/n9e/wechat-sender/redisc" 18 | ) 19 | 20 | var semaphore chan int 21 | var chatClient *corp.Client 22 | 23 | func SendWeChat() { 24 | c := config.Get() 25 | 26 | semaphore = make(chan int, c.Consumer.Worker) 27 | 28 | chatClient = corp.New(c.WeChat.CorpID, c.WeChat.AgentID, c.WeChat.Secret) 29 | 30 | for { 31 | messages := redisc.Pop(1, c.Consumer.Queue) 32 | if len(messages) == 0 { 33 | time.Sleep(time.Duration(300) * time.Millisecond) 34 | continue 35 | } 36 | 37 | sendWeChats(messages) 38 | } 39 | } 40 | 41 | func sendWeChats(messages []*dataobj.Message) { 42 | for _, message := range messages { 43 | semaphore <- 1 44 | go sendChat(message) 45 | } 46 | } 47 | 48 | func sendChat(message *dataobj.Message) { 49 | defer func() { 50 | <-semaphore 51 | }() 52 | 53 | content := genContent(message) 54 | 55 | logger.Info("<-- hashid: %v -->", message.Event.HashId) 56 | logger.Infof("hashid: %d: endpoint: %s, metric: %s, tags: %s", message.Event.HashId, message.ReadableEndpoint, strings.Join(message.Metrics, ","), message.ReadableTags) 57 | 58 | count := len(message.Tos) 59 | for i := 0; i < count; i++ { 60 | err := chatClient.Send(corp.Message{ 61 | ToUser: message.Tos[i], 62 | MsgType: "text", 63 | Text: corp.Content{Content: content}, 64 | }) 65 | 66 | if err != nil { 67 | logger.Infof("send to %s fail: %v", message.Tos[i], err) 68 | } else { 69 | logger.Infof("send to %s succ", message.Tos[i]) 70 | } 71 | } 72 | 73 | logger.Info("<-- /hashid: %v -->", message.Event.HashId) 74 | } 75 | 76 | var ET = map[string]string{ 77 | "alert": "告警", 78 | "recovery": "恢复", 79 | } 80 | 81 | func parseEtime(etime int64) string { 82 | t := time.Unix(etime, 0) 83 | return t.Format("2006-01-02 15:04:05") 84 | } 85 | 86 | func genContent(message *dataobj.Message) string { 87 | fp := path.Join(runner.Cwd, "etc", "wechat.tpl") 88 | t, err := template.ParseFiles(fp) 89 | if err != nil { 90 | payload := fmt.Sprintf("InternalServerError: cannot parse %s %v", fp, err) 91 | logger.Errorf(payload) 92 | return fmt.Sprintf(payload) 93 | } 94 | 95 | var body bytes.Buffer 96 | err = t.Execute(&body, map[string]interface{}{ 97 | "IsAlert": message.Event.EventType == "alert", 98 | "Status": ET[message.Event.EventType], 99 | "Sname": message.Event.Sname, 100 | "Endpoint": message.ReadableEndpoint, 101 | "Metric": strings.Join(message.Metrics, ","), 102 | "Tags": message.ReadableTags, 103 | "Value": message.Event.Value, 104 | "Info": message.Event.Info, 105 | "Etime": parseEtime(message.Event.Etime), 106 | "Elink": message.EventLink, 107 | "Slink": message.StraLink, 108 | "Clink": message.ClaimLink, 109 | "IsUpgrade": message.IsUpgrade, 110 | "Bindings": message.Bindings, 111 | "Priority": message.Event.Priority, 112 | }) 113 | 114 | if err != nil { 115 | logger.Errorf("InternalServerError: %v", err) 116 | return fmt.Sprintf("InternalServerError: %v", err) 117 | } 118 | 119 | return body.String() 120 | } 121 | -------------------------------------------------------------------------------- /dataobj/message.go: -------------------------------------------------------------------------------- 1 | package dataobj 2 | 3 | import "time" 4 | 5 | type Message struct { 6 | Tos []string `json:"tos"` 7 | Event *Event `json:"event"` 8 | ClaimLink string `json:"claim_link"` 9 | StraLink string `json:"stra_link"` 10 | EventLink string `json:"event_link"` 11 | Bindings []string `json:"bindings"` 12 | NotifyType string `json:"notify_type"` 13 | Metrics []string `json:"metrics"` 14 | ReadableEndpoint string `json:"readable_endpoint"` 15 | ReadableTags string `json:"readable_tags"` 16 | IsUpgrade bool `json:"is_upgrade"` 17 | } 18 | 19 | type Event struct { 20 | Id int64 `json:"id"` 21 | Sid int64 `json:"sid"` 22 | Sname string `json:"sname"` 23 | NodePath string `json:"node_path"` 24 | Endpoint string `json:"endpoint"` 25 | EndpointAlias string `json:"endpoint_alias"` 26 | Priority int `json:"priority"` 27 | EventType string `json:"event_type"` // alert|recovery 28 | Category int `json:"category"` 29 | Status uint16 `json:"status"` 30 | HashId uint64 `json:"hashid" xorm:"hashid"` 31 | Etime int64 `json:"etime"` 32 | Value string `json:"value"` 33 | Info string `json:"info"` 34 | Created time.Time `json:"created" xorm:"created"` 35 | Detail string `json:"detail"` 36 | Users string `json:"users"` 37 | Groups string `json:"groups"` 38 | Nid int64 `json:"nid"` 39 | NeedUpgrade int `json:"need_upgrade"` 40 | AlertUpgrade string `json:"alert_upgrade"` 41 | } 42 | 43 | type EventDetail struct { 44 | Metric string `json:"metric"` 45 | Tags map[string]string `json:"tags"` 46 | Points []*EventDetailPoint `json:"points"` 47 | PredPoints []*EventDetailPoint `json:"pred_points,omitempty"` // 预测值, 预测值不为空时, 现场值对应的是实际值 48 | } 49 | 50 | type EventDetailPoint struct { 51 | Timestamp int64 `json:"timestamp"` 52 | Value float64 `json:"value"` 53 | } 54 | -------------------------------------------------------------------------------- /etc/wechat-sender.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Nightingale wechat sender 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | User=root 8 | Group=root 9 | 10 | Type=simple 11 | ExecStart=/home/n9e/wechat-sender 12 | WorkingDirectory=/home/n9e 13 | 14 | Restart=always 15 | RestartSec=1 16 | StartLimitInterval=0 17 | 18 | [Install] 19 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /etc/wechat-sender.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logger: 3 | dir: "logs/wechat-sender" 4 | level: "DEBUG" 5 | keepHours: 24 6 | 7 | http: 8 | listen: 0.0.0.0:2501 9 | 10 | redis: 11 | addr: "127.0.0.1:6379" 12 | pass: "" 13 | db: 0 14 | idle: 5 15 | timeout: 16 | conn: 500 17 | read: 3000 18 | write: 3000 19 | 20 | # worker是调用wechat的并发数 21 | consumer: 22 | enable: false 23 | queue: "/n9e/sender/im" 24 | worker: 10 25 | 26 | wechat: 27 | corp_id: "ww6424d33203e90e20" 28 | agent_id: 1000002 29 | secret: "FoST_8RQSTjZwH_CN3aQW6UKksjCSI9mizFqD7HKhrw" 30 | -------------------------------------------------------------------------------- /etc/wechat.tpl: -------------------------------------------------------------------------------- 1 | 事件状态:P{{.Priority}} {{.Status}} 2 | 策略名称:{{.Sname}} 3 | endpoint:{{.Endpoint}} 4 | metric:{{.Metric}} 5 | tags:{{.Tags}} 6 | 当前值:{{.Value}} 7 | 报警说明:{{.Info}} 8 | 触发时间:{{.Etime}} 9 | 报警详情:{{.Elink}} 10 | {{if .IsUpgrade}} 11 | --- 12 | 报警已升级!!! 13 | {{end}} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/n9e/wechat-sender 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/codegangsta/negroni v1.0.0 8 | github.com/garyburd/redigo v1.6.0 9 | github.com/gorilla/context v1.1.1 10 | github.com/gorilla/mux v1.6.2 11 | github.com/toolkits/pkg v1.1.1 12 | github.com/unrolled/render v1.0.2 13 | go.uber.org/automaxprocs v1.3.0 14 | golang.org/x/tools v0.0.0-20200108203644-89082a384178 15 | gopkg.in/yaml.v2 v2.2.8 16 | honnef.co/go/tools v0.0.1-2019.2.3 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= 4 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 7 | github.com/garyburd/redigo v1.1.0 h1:kTY6M1SUxdOiFU4rbXWTtDBsTnfsXo4vDhXzhGMjdwk= 8 | github.com/garyburd/redigo v1.1.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 9 | github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= 10 | github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 11 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 12 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 13 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 14 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 25 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 26 | github.com/toolkits/pkg v1.1.1 h1:m57zdoBKQmTzhY83F3g56seDfLm+l/toBs8cKv8QFiE= 27 | github.com/toolkits/pkg v1.1.1/go.mod h1:ge83E8FQqUnFk+2wtVtZ8kvbmoSjE1l8FP3f+qmR0fY= 28 | github.com/unrolled/render v1.0.2/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= 29 | go.uber.org/automaxprocs v1.3.0 h1:II28aZoGdaglS5vVNnspf28lnZpXScxtIozx1lAjdb0= 30 | go.uber.org/automaxprocs v1.3.0/go.mod h1:9CWT6lKIep8U41DDaPiH6eFscnTyjfTANNQNx6LrIcA= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 33 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 34 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= 35 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 36 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 37 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 38 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 46 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms= 47 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 48 | golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 49 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 53 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 55 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 56 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 57 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 58 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/codegangsta/negroni" 11 | "github.com/gorilla/mux" 12 | 13 | "github.com/n9e/wechat-sender/config" 14 | "github.com/n9e/wechat-sender/http/middleware" 15 | "github.com/n9e/wechat-sender/http/render" 16 | "github.com/n9e/wechat-sender/http/router" 17 | ) 18 | 19 | var srv = &http.Server{ 20 | ReadTimeout: 10 * time.Second, 21 | WriteTimeout: 10 * time.Second, 22 | MaxHeaderBytes: 1 << 20, 23 | } 24 | 25 | func Start() { 26 | render.Init() 27 | 28 | r := mux.NewRouter().StrictSlash(false) 29 | router.ConfigRoutes(r) 30 | 31 | n := negroni.New() 32 | n.Use(middleware.NewRecovery()) 33 | 34 | n.UseHandler(r) 35 | 36 | srv.Addr = config.Get().HTTP.Listen 37 | srv.Handler = n 38 | 39 | go func() { 40 | fmt.Println("http.listening:", srv.Addr) 41 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 42 | fmt.Printf("listening %s occur error: %s\n", srv.Addr, err) 43 | os.Exit(3) 44 | } 45 | }() 46 | } 47 | 48 | // Shutdown http server 49 | func Shutdown() { 50 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 51 | defer cancel() 52 | if err := srv.Shutdown(ctx); err != nil { 53 | fmt.Println("cannot shutdown http server:", err) 54 | os.Exit(2) 55 | } 56 | 57 | // catching ctx.Done(). timeout of 5 seconds. 58 | select { 59 | case <-ctx.Done(): 60 | fmt.Println("shutdown http server timeout of 5 seconds.") 61 | default: 62 | fmt.Println("http server stopped") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /http/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/codegangsta/negroni" 10 | ) 11 | 12 | // Logger is a middleware handler that logs the request as it goes in and the response as it goes out. 13 | type Logger struct { 14 | // Logger inherits from log.Logger used to log messages with the Logger middleware 15 | *log.Logger 16 | } 17 | 18 | // NewLogger returns a new Logger instance 19 | func NewLogger(out io.Writer) *Logger { 20 | return &Logger{log.New(out, "", 0)} 21 | } 22 | 23 | func (l *Logger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 24 | start := time.Now() 25 | next(rw, r) 26 | res := rw.(negroni.ResponseWriter) 27 | format := "[status:%d][use:%v][method:%s][uri:%s]" 28 | l.Printf(format, res.Status(), time.Since(start), r.Method, r.URL.RequestURI()) 29 | } 30 | -------------------------------------------------------------------------------- /http/middleware/recovery.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "runtime" 6 | 7 | "github.com/n9e/wechat-sender/http/render" 8 | 9 | "github.com/toolkits/pkg/errors" 10 | "github.com/toolkits/pkg/logger" 11 | ) 12 | 13 | // Recovery is a Negroni middleware that recovers from any panics and writes a 500 if there was one. 14 | type Recovery struct { 15 | StackAll bool 16 | StackSize int 17 | } 18 | 19 | // NewRecovery returns a new instance of Recovery 20 | func NewRecovery() *Recovery { 21 | return &Recovery{ 22 | StackAll: false, 23 | StackSize: 1024 * 8, 24 | } 25 | } 26 | 27 | func (rec *Recovery) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 28 | defer func() { 29 | if err := recover(); err != nil { 30 | if e, ok := err.(errors.PageError); ok { 31 | // custom error 32 | // if e.Error() == "unauthorized" { 33 | // http.Redirect(w, r, "/?callback="+r.RequestURI, 302) 34 | // return 35 | // } 36 | render.Message(w, e) 37 | return 38 | } 39 | 40 | // Negroni part 41 | w.WriteHeader(http.StatusInternalServerError) 42 | stack := make([]byte, rec.StackSize) 43 | stack = stack[:runtime.Stack(stack, rec.StackAll)] 44 | 45 | logger.Errorf("PANIC: %s\n%s", err, stack) 46 | } 47 | }() 48 | 49 | next(w, r) 50 | } 51 | 52 | func isAjax(r *http.Request) bool { 53 | return r.Header.Get("X-Requested-With") == "XMLHttpRequest" 54 | } 55 | -------------------------------------------------------------------------------- /http/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | 7 | "github.com/gorilla/context" 8 | "github.com/unrolled/render" 9 | ) 10 | 11 | var Render *render.Render 12 | var funcMap = template.FuncMap{} 13 | 14 | func Init() { 15 | Render = render.New(render.Options{ 16 | Directory: "web", 17 | Extensions: []string{".html"}, 18 | Delims: render.Delims{"{{", "}}"}, 19 | Funcs: []template.FuncMap{funcMap}, 20 | IndentJSON: false, 21 | IsDevelopment: true, 22 | }) 23 | } 24 | 25 | func Put(r *http.Request, key string, val interface{}) { 26 | m, ok := context.GetOk(r, "_DATA_MAP_") 27 | if ok { 28 | mm := m.(map[string]interface{}) 29 | mm[key] = val 30 | context.Set(r, "_DATA_MAP_", mm) 31 | } else { 32 | context.Set(r, "_DATA_MAP_", map[string]interface{}{key: val}) 33 | } 34 | } 35 | 36 | func HTML(r *http.Request, w http.ResponseWriter, name string, htmlOpt ...render.HTMLOptions) { 37 | Render.HTML(w, http.StatusOK, name, context.Get(r, "_DATA_MAP_"), htmlOpt...) 38 | } 39 | 40 | func Text(w http.ResponseWriter, v string, codes ...int) { 41 | code := http.StatusOK 42 | if len(codes) > 0 { 43 | code = codes[0] 44 | } 45 | Render.Text(w, code, v) 46 | } 47 | 48 | func Message(w http.ResponseWriter, v interface{}) { 49 | if v == nil { 50 | Render.JSON(w, http.StatusOK, map[string]string{"err": ""}) 51 | return 52 | } 53 | 54 | switch t := v.(type) { 55 | case string: 56 | Render.JSON(w, http.StatusOK, map[string]string{"err": t}) 57 | case error: 58 | Render.JSON(w, http.StatusOK, map[string]string{"err": t.Error()}) 59 | } 60 | } 61 | 62 | func Data(w http.ResponseWriter, v interface{}, err error) { 63 | if err != nil { 64 | Render.JSON(w, http.StatusOK, map[string]interface{}{"err": err.Error(), "dat": v}) 65 | } else { 66 | Render.JSON(w, http.StatusOK, map[string]interface{}{"err": "", "dat": v}) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /http/router/funcs.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/toolkits/pkg/errors" 10 | "github.com/toolkits/pkg/param" 11 | "github.com/toolkits/pkg/str" 12 | ) 13 | 14 | func isDangerous(key, val string) { 15 | if str.Dangerous(val) { 16 | errors.Bomb("arg[%s] is dangerous", key) 17 | } 18 | } 19 | 20 | func isBlank(key, val string) { 21 | v := strings.TrimSpace(val) 22 | if v == "" { 23 | errors.Bomb("arg[%s] is blank", key) 24 | } 25 | } 26 | 27 | func bindJSON(r *http.Request, val interface{}) { 28 | errors.Dangerous(param.BindJson(r, val)) 29 | } 30 | 31 | func urlParamStr(r *http.Request, field string) string { 32 | val, found := mux.Vars(r)[field] 33 | if !found { 34 | errors.Bomb("[%s] not found in url", field) 35 | } 36 | 37 | if val == "" { 38 | errors.Bomb("[%s] is blank", field) 39 | } 40 | 41 | return val 42 | } 43 | 44 | func urlParamInt64(r *http.Request, field string) int64 { 45 | strval := urlParamStr(r, field) 46 | intval, err := strconv.ParseInt(strval, 10, 64) 47 | if err != nil { 48 | errors.Bomb("cannot convert %s to int64", strval) 49 | } 50 | 51 | return intval 52 | } 53 | 54 | func urlParamInt(r *http.Request, field string) int { 55 | return int(urlParamInt64(r, field)) 56 | } 57 | -------------------------------------------------------------------------------- /http/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/toolkits/pkg/logger" 9 | 10 | "github.com/n9e/wechat-sender/config" 11 | "github.com/n9e/wechat-sender/corp" 12 | "github.com/n9e/wechat-sender/http/render" 13 | ) 14 | 15 | var chatClient *corp.Client 16 | 17 | func ConfigRoutes(r *mux.Router) { 18 | r.HandleFunc("/send/wechat", apiSendWechat) 19 | r.HandleFunc("/ping", ping) 20 | } 21 | 22 | func ping(w http.ResponseWriter, r *http.Request) { 23 | fmt.Fprintf(w, "pong") 24 | } 25 | 26 | type Message struct { 27 | Tos []string `json:"tos"` 28 | Content string `json:"content"` 29 | } 30 | 31 | func apiSendWechat(w http.ResponseWriter, r *http.Request) { 32 | var message Message 33 | bindJSON(r, &message) 34 | 35 | cnt := len(message.Tos) 36 | if cnt == 0 { 37 | logger.Warningf("api send wechat fail, empty tos, message: %+v", message) 38 | render.Message(w, "api empty tos") 39 | return 40 | } 41 | 42 | c := config.Get() 43 | client := corp.New(c.WeChat.CorpID, c.WeChat.AgentID, c.WeChat.Secret) 44 | 45 | var err error 46 | for i := 0; i < cnt; i++ { 47 | err = client.Send(corp.Message{ 48 | ToUser: message.Tos[i], 49 | MsgType: "text", 50 | Text: corp.Content{Content: message.Content}, 51 | }) 52 | 53 | if err != nil { 54 | logger.Warningf("api send to %s fail: %v", message.Tos[i], err) 55 | } else { 56 | logger.Infof("api send to %s succ", message.Tos[i]) 57 | } 58 | } 59 | 60 | render.Message(w, err) 61 | } 62 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "path" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/toolkits/pkg/file" 13 | "github.com/toolkits/pkg/logger" 14 | "github.com/toolkits/pkg/runner" 15 | 16 | "github.com/n9e/wechat-sender/config" 17 | "github.com/n9e/wechat-sender/cron" 18 | "github.com/n9e/wechat-sender/http" 19 | "github.com/n9e/wechat-sender/redisc" 20 | ) 21 | 22 | var ( 23 | vers *bool 24 | help *bool 25 | conf *string 26 | test *string 27 | ) 28 | 29 | func init() { 30 | vers = flag.Bool("v", false, "display the version.") 31 | help = flag.Bool("h", false, "print this help.") 32 | conf = flag.String("f", "", "specify configuration file.") 33 | test = flag.String("t", "", "test configuration.") 34 | flag.Parse() 35 | 36 | if *vers { 37 | fmt.Println("version:", config.Version) 38 | os.Exit(0) 39 | } 40 | 41 | if *help { 42 | flag.Usage() 43 | os.Exit(0) 44 | } 45 | 46 | runner.Init() 47 | fmt.Println("runner.cwd:", runner.Cwd) 48 | fmt.Println("runner.hostname:", runner.Hostname) 49 | } 50 | 51 | func main() { 52 | aconf() 53 | pconf() 54 | 55 | if *test != "" { 56 | config.Test(strings.Split(*test, ",")) 57 | os.Exit(0) 58 | } 59 | 60 | config.InitLogger() 61 | redisc.InitRedis() 62 | 63 | c := config.Get() 64 | if c.Consumer.Enable { 65 | go cron.SendWeChat() 66 | } 67 | 68 | http.Start() 69 | ending() 70 | } 71 | 72 | func ending() { 73 | c := make(chan os.Signal, 1) 74 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 75 | select { 76 | case <-c: 77 | fmt.Printf("stop signal caught, stopping... pid=%d\n", os.Getpid()) 78 | } 79 | 80 | logger.Close() 81 | redisc.CloseRedis() 82 | http.Shutdown() 83 | fmt.Println("sender stopped successfully") 84 | } 85 | 86 | // auto detect configuration file 87 | func aconf() { 88 | if *conf != "" && file.IsExist(*conf) { 89 | return 90 | } 91 | 92 | *conf = path.Join(runner.Cwd, "etc", "wechat-sender.local.yml") 93 | if file.IsExist(*conf) { 94 | return 95 | } 96 | 97 | *conf = path.Join(runner.Cwd, "etc", "wechat-sender.yml") 98 | if file.IsExist(*conf) { 99 | return 100 | } 101 | 102 | fmt.Println("no configuration file for sender") 103 | os.Exit(1) 104 | } 105 | 106 | // parse configuration file 107 | func pconf() { 108 | if err := config.ParseConfig(*conf); err != nil { 109 | fmt.Println("cannot parse configuration file:", err) 110 | os.Exit(1) 111 | } else { 112 | fmt.Println("parse configuration file:", *conf) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # wechat-sender 2 | 3 | Nightingale的理念,是将告警事件扔到redis里就不管了,接下来由各种sender来读取redis里的事件并发送,毕竟发送报警的方式太多了,适配起来比较费劲,希望社区同仁能够共建。 4 | 5 | 这里提供一个微信的sender,参考了[https://github.com/yanjunhui/chat](https://github.com/yanjunhui/chat),具体如何获取企业微信信息,也可以参看yanjunhui这个repo 6 | 7 | ## compile 8 | 9 | ```bash 10 | cd $GOPATH/src 11 | mkdir -p github.com/n9e 12 | cd github.com/n9e 13 | git clone https://github.com/n9e/wechat-sender.git 14 | cd wechat-sender 15 | go build 16 | ``` 17 | 18 | 如上编译完就可以拿到二进制了。 19 | 20 | ## configuration 21 | 22 | 直接修改etc/wechat-sender.yml即可。另外n9e-monapi这个模块默认的发送通道只打开了mail,如果要同时使用im,需要在notify这里打开相关配置: 23 | 24 | ```yaml 25 | notify: 26 | p1: ["mail", "im"] 27 | p2: ["mail", "im"] 28 | p3: ["mail", "im"] 29 | ``` 30 | 31 | ## pack 32 | 33 | 编译完成之后可以打个包扔到线上去跑,将二进制和配置文件打包即可: 34 | 35 | ```bash 36 | tar zcvf wechat-sender.tar.gz wechat-sender etc/wechat-sender.yml etc/wechat.tpl 37 | ``` 38 | 39 | ## test 40 | 41 | 配置etc/wechat-sender.yml,相关配置修改好,我们先来测试一下是否好使, `./wechat-sender -t `,程序会自动读取etc目录下的配置文件,发一个测试消息给`toUser` 42 | 43 | ## run 44 | 45 | 如果测试发送没问题,扔到线上跑吧,使用systemd或者supervisor之类的托管起来,systemd的配置实例: 46 | 47 | 48 | ``` 49 | $ cat wechat-sender.service 50 | [Unit] 51 | Description=Nightingale wechat sender 52 | After=network-online.target 53 | Wants=network-online.target 54 | 55 | [Service] 56 | User=root 57 | Group=root 58 | 59 | Type=simple 60 | ExecStart=/home/n9e/wechat-sender 61 | WorkingDirectory=/home/n9e 62 | 63 | Restart=always 64 | RestartSec=1 65 | StartLimitInterval=0 66 | 67 | [Install] 68 | WantedBy=multi-user.target 69 | ``` 70 | -------------------------------------------------------------------------------- /redisc/poper.go: -------------------------------------------------------------------------------- 1 | package redisc 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | "github.com/toolkits/pkg/logger" 8 | 9 | "github.com/n9e/wechat-sender/dataobj" 10 | ) 11 | 12 | func Pop(count int, queue string) []*dataobj.Message { 13 | var lst []*dataobj.Message 14 | 15 | rc := RedisConnPool.Get() 16 | defer rc.Close() 17 | 18 | for i := 0; i < count; i++ { 19 | reply, err := redis.String(rc.Do("RPOP", queue)) 20 | if err != nil { 21 | if err != redis.ErrNil { 22 | logger.Errorf("rpop queue:%s failed, err: %v", queue, err) 23 | } 24 | break 25 | } 26 | 27 | if reply == "" || reply == "nil" { 28 | continue 29 | } 30 | 31 | var message dataobj.Message 32 | err = json.Unmarshal([]byte(reply), &message) 33 | if err != nil { 34 | logger.Errorf("unmarshal message failed, err: %v, redis reply: %v", err, reply) 35 | continue 36 | } 37 | 38 | lst = append(lst, &message) 39 | } 40 | 41 | return lst 42 | } 43 | -------------------------------------------------------------------------------- /redisc/redis.go: -------------------------------------------------------------------------------- 1 | package redisc 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | "github.com/n9e/wechat-sender/config" 8 | "github.com/toolkits/pkg/logger" 9 | ) 10 | 11 | var RedisConnPool *redis.Pool 12 | 13 | func InitRedis() { 14 | cfg := config.Get() 15 | 16 | addr := cfg.Redis.Addr 17 | pass := cfg.Redis.Pass 18 | db := cfg.Redis.DB 19 | maxIdle := cfg.Redis.Idle 20 | idleTimeout := 240 * time.Second 21 | 22 | connTimeout := time.Duration(cfg.Redis.Timeout.Conn) * time.Millisecond 23 | readTimeout := time.Duration(cfg.Redis.Timeout.Read) * time.Millisecond 24 | writeTimeout := time.Duration(cfg.Redis.Timeout.Write) * time.Millisecond 25 | 26 | RedisConnPool = &redis.Pool{ 27 | MaxIdle: maxIdle, 28 | IdleTimeout: idleTimeout, 29 | Dial: func() (redis.Conn, error) { 30 | c, err := redis.Dial("tcp", addr, redis.DialConnectTimeout(connTimeout), redis.DialReadTimeout(readTimeout), redis.DialWriteTimeout(writeTimeout)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if pass != "" { 36 | if _, err := c.Do("AUTH", pass); err != nil { 37 | c.Close() 38 | logger.Error("redis auth fail, pass: ", pass) 39 | return nil, err 40 | } 41 | } 42 | if db != 0 { 43 | if _, err := c.Do("SELECT", db); err != nil { 44 | c.Close() 45 | logger.Error("redis select db fail, db: ", db) 46 | return nil, err 47 | } 48 | } 49 | 50 | return c, err 51 | }, 52 | TestOnBorrow: PingRedis, 53 | } 54 | } 55 | 56 | func PingRedis(c redis.Conn, t time.Time) error { 57 | _, err := c.Do("ping") 58 | if err != nil { 59 | logger.Error("ping redis fail: ", err) 60 | } 61 | return err 62 | } 63 | 64 | func CloseRedis() { 65 | logger.Info("closing redis...") 66 | RedisConnPool.Close() 67 | } 68 | --------------------------------------------------------------------------------