joe 4 years ago
commit
ee53cda61d

+ 0 - 0
.gitignore


+ 0 - 0
LICENSE


+ 15 - 0
Makefile

@@ -0,0 +1,15 @@
+#
+
+.PHONY: sample test clean
+
+sample:
+	go build -o ./sample/sample ./sample
+
+test:
+	go test ./providers
+	
+run: sample
+	./sample/sample
+	
+clean:
+	-rm ./sample/sample

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+# beaconfire
+
+beaconfire is a notify library, inspired by 'github.com/gomodules/notify'
+
+# supported
+
+- [x]Discord
+- [x]Mailgun
+- [x]SMTP
+- [x]Slack
+- [x]Mattermost
+- [x]Telegram
+- [x]Twilio
+- [x]Dingtalk
+- [x]Business Wechat
+
+## same projects
+
+I also recommend you:
+
+- [shoutrrr](https://github.com/containrrr/shoutrrr)
+> It said it a [apprise](https://github.com/caronc/apprise) like tools library, it's awesome.
+
+- [notify](https://github.com/gomodules/notify)
+> First what i used.
+
+For me, both of them doesnt support Chinese social app bot, maybe i will pull requests for them someday.

+ 48 - 0
beaconfire.go

@@ -0,0 +1,48 @@
+package beaconfire
+
+import (
+	"errors"
+	"fmt"
+	"time"
+)
+
+var (
+	ErrNoReceiver = errors.New("no receiver")
+)
+
+type BeaconParam struct {
+	From, Title, Content string
+	Ts                   int64
+	To                   []string
+	Fmt                  string
+}
+
+func NewBeaconParam(from, title, content string, fmt string, to ...string) *BeaconParam {
+	bp := &BeaconParam{
+		From:    from,
+		Title:   title,
+		Content: content,
+		Ts:      time.Now().Unix(),
+		Fmt:     fmt,
+	}
+
+	bp.To = append(bp.To, to...)
+	return bp
+}
+
+func (bp BeaconParam) FormatMarkdown() string {
+	return fmt.Sprintf("### %v\n> %v\n> %v\n> %v", bp.Title, bp.From, Ts2Str(bp.Ts), bp.Content)
+}
+
+func (bp BeaconParam) FormatHTML() string {
+	return fmt.Sprintf("<b>%v</b>\n<i>%v</i>\n<i>%v</i>\n%v", bp.Title, bp.From, Ts2Str(bp.Ts), bp.Content)
+}
+
+func (bp BeaconParam) FormatPlainText() string {
+	return fmt.Sprintf("%v\n%v\n%v\n%v", bp.Title, bp.From, Ts2Str(bp.Ts), bp.Content)
+}
+
+type BeaconFire interface {
+	Name() string
+	Send(*BeaconParam) error
+}

+ 31 - 0
go.mod

@@ -0,0 +1,31 @@
+module git.wenlab.co/joe/beaconfire
+
+go 1.17
+
+require (
+	github.com/bwmarrin/discordgo v0.23.2
+	github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
+	github.com/mailgun/mailgun-go v2.0.0+incompatible
+	github.com/slack-go/slack v0.10.0
+	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
+)
+
+require (
+	github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect
+	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
+	github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
+	github.com/gobuffalo/envy v1.10.1 // indirect
+	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/joho/godotenv v1.4.0 // indirect
+	github.com/mattn/go-runewidth v0.0.9 // indirect
+	github.com/olekukonko/tablewriter v0.0.5 // indirect
+	github.com/onsi/ginkgo v1.16.5 // indirect
+	github.com/onsi/gomega v1.17.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/rogpeppe/go-internal v1.8.0 // indirect
+	github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
+	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
+	golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
+	golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
+	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+)

+ 145 - 0
go.sum

@@ -0,0 +1,145 @@
+github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
+github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0=
+github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
+github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
+github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
+github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk=
+github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
+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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+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/gobuffalo/envy v1.10.1 h1:ppDLoXv2feQ5nus4IcgtyMdHQkKng2lhJCIm33cblM0=
+github.com/gobuffalo/envy v1.10.1/go.mod h1:AWx4++KnNOW3JOeEvhSaq+mvgAvnMYOY1XSIin4Mago=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
+github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
+github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
+github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o=
+github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/slack-go/slack v0.10.0 h1:L16Eqg3QZzRKGXIVsFSZdJdygjOphb2FjRUwH6VrFu8=
+github.com/slack-go/slack v0.10.0/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
+golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 47 - 0
httpclient.go

@@ -0,0 +1,47 @@
+package beaconfire
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"time"
+)
+
+const (
+	_DEFAULT_USER_AGENT = "curl v7.19.1"
+)
+
+func Get(url string) ([]byte, error) {
+	return Request(http.MethodGet, url, nil, nil, "", "")
+}
+
+func PostJson(url string, body []byte) ([]byte, error) {
+	return Request(http.MethodPost, url, body, map[string]string{"Content-Type": "application/json"}, "", "")
+}
+
+func Request(method string, url string, body []byte, headers map[string]string, username, password string) ([]byte, error) {
+	client := http.Client{
+		Timeout: time.Second * 5,
+	}
+	req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
+	if err != nil {
+		return nil, err
+	}
+
+	if len(username) > 0 || len(password) > 0 {
+		req.SetBasicAuth(username, password)
+	}
+
+	req.Header.Set("User-Agent", _DEFAULT_USER_AGENT)
+	for k, v := range headers {
+		req.Header.Set(k, v)
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	return ioutil.ReadAll(resp.Body)
+}

+ 27 - 0
httpclient_test.go

@@ -0,0 +1,27 @@
+package beaconfire
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestGet(t *testing.T) {
+	_, err := Get("http://www.google.com")
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestPostJson(t *testing.T) {
+	body, err := json.Marshal(map[string]string{
+		"username": "test",
+		"password": "123",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+	_, err = PostJson("http://google.com", body)
+	if err != nil {
+		t.Error(err)
+	}
+}

+ 32 - 0
providers/aliyun.go

@@ -0,0 +1,32 @@
+package providers
+
+import "git.wenlab.co/joe/beaconfire"
+
+// aliyun SMS support
+
+const (
+	ALIYUN_NAME = "aliyun"
+)
+
+type OptionsAliyun struct {
+}
+
+type aliyun struct {
+	opt *OptionsAliyun
+}
+
+var _ beaconfire.BeaconFire = &aliyun{}
+
+func NewAliyun(opt *OptionsAliyun) *aliyun {
+	return &aliyun{
+		opt: opt,
+	}
+}
+
+func (a *aliyun) Name() string {
+	return ALIYUN_NAME
+}
+
+func (a *aliyun) Send(bp *beaconfire.BeaconParam) error {
+	return nil
+}

+ 76 - 0
providers/dingtalk.go

@@ -0,0 +1,76 @@
+package providers
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+// API:
+// https://open.dingtalk.com/document/group/custom-robot-access
+
+const (
+	_DINGTALK_ENDPOINT        = "https://oapi.dingtalk.com/robot/send"
+	_DINGTALK_DEFAULT_MSGTYPE = "markdown" // this type would be lcompatible with others easily
+
+	DINGTALK_NAME = "dingtalk"
+)
+
+type OptionsDingtalk struct {
+	AccessToken string `envconfig:"ACCESS_TOKEN"`
+}
+
+type dingtalk struct {
+	opt *OptionsDingtalk
+}
+
+var _ beaconfire.BeaconFire = &dingtalk{}
+
+func NewDingtalk(opt *OptionsDingtalk) *dingtalk {
+	return &dingtalk{
+		opt: opt,
+	}
+}
+
+func (c dingtalk) Name() string {
+	return DINGTALK_NAME
+}
+
+func (c *dingtalk) Send(bp *beaconfire.BeaconParam) error {
+
+	msgType := _DINGTALK_DEFAULT_MSGTYPE
+
+	url := fmt.Sprintf("%s?access_token=%s", _DINGTALK_ENDPOINT, c.opt.AccessToken)
+	content := bp.FormatMarkdown()
+	values := map[string]interface{}{
+		"msgtype": msgType,
+		msgType: map[string]interface{}{
+			"content": content,
+		},
+		"at": map[string]interface{}{
+			"atUserIds": bp.To,
+			"isAtAll":   false,
+		},
+	}
+
+	jsonBuffer, err := json.Marshal(values)
+	if err != nil {
+		return err
+	}
+
+	res, err := beaconfire.PostJson(url, jsonBuffer)
+	if err != nil {
+		return err
+	}
+
+	var r map[string]interface{}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return err
+	}
+	// DEBUG
+	fmt.Printf("dingtalk: %+v\n", r)
+
+	return nil
+}

+ 1 - 0
providers/dingtalk_test.go

@@ -0,0 +1 @@
+package providers

+ 53 - 0
providers/discord.go

@@ -0,0 +1,53 @@
+package providers
+
+import (
+	"git.wenlab.co/joe/beaconfire"
+	"github.com/bwmarrin/discordgo"
+)
+
+const (
+	DISCORD_NAME = "discord"
+)
+
+type OptionsDiscord struct {
+	AuthToken string
+}
+
+type discord struct {
+	opt *OptionsDiscord
+}
+
+var _ beaconfire.BeaconFire = &discord{}
+
+func NewDiscord(opt *OptionsDiscord) *discord {
+	return &discord{
+		opt: opt,
+	}
+}
+
+func (c discord) Name() string {
+	return DISCORD_NAME
+}
+
+func (c *discord) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	discord, err := discordgo.New("Bot " + c.opt.AuthToken)
+	if err != nil {
+		return err
+	}
+
+	if err = discord.Open(); err != nil {
+		return err
+	}
+	defer discord.Close()
+
+	for _, channel := range bp.To {
+		if _, err := discord.ChannelMessageSend(channel, bp.Content); err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 1 - 0
providers/discord_test.go

@@ -0,0 +1 @@
+package providers

+ 57 - 0
providers/mailgun.go

@@ -0,0 +1,57 @@
+package providers
+
+import (
+	"git.wenlab.co/joe/beaconfire"
+	h2t "github.com/jaytaylor/html2text"
+	mailgun "github.com/mailgun/mailgun-go"
+)
+
+const (
+	MAILGUNFIRE_NAME = "mailgunfire"
+)
+
+type OptionsMailgunfire struct {
+	Domain          string
+	ApiKey          string
+	DisableTracking bool
+}
+
+type mailgunfire struct {
+	opt *OptionsMailgunfire
+}
+
+var _ beaconfire.BeaconFire = &mailgunfire{}
+
+func NewMailgunfire(opt *OptionsMailgunfire) *mailgunfire {
+	return &mailgunfire{opt: opt}
+}
+
+func (c mailgunfire) Name() string {
+	return MAILGUNFIRE_NAME
+}
+
+func (c *mailgunfire) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) == 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	mg := mailgun.NewMailgun(c.opt.Domain, c.opt.ApiKey)
+	content := bp.Content
+	inHtml := true //format == "html"
+	if inHtml {
+		if t, err := h2t.FromString(bp.Content); err == nil {
+			content = t
+		}
+	}
+
+	msg := mg.NewMessage(bp.From, bp.Title, content, bp.To...)
+	if inHtml {
+		msg.SetHtml(content)
+	}
+
+	msg.SetTracking(!c.opt.DisableTracking)
+	msg.SetTrackingClicks(!c.opt.DisableTracking)
+	msg.SetTrackingOpens(!c.opt.DisableTracking)
+	_, _, err := mg.Send(msg)
+	return err
+}

+ 84 - 0
providers/mattermost.go

@@ -0,0 +1,84 @@
+package providers
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+const MATTERMOST_NAME = "mattermost"
+
+type OptionsMattermost struct {
+	Url     string
+	HookId  string
+	IconUrl string
+	BotName string
+	// Channel []string
+}
+
+type mattermost struct {
+	opt *OptionsMattermost
+}
+
+var _ beaconfire.BeaconFire = &mattermost{}
+
+func NewMattermost(opt *OptionsMattermost) *mattermost {
+	return &mattermost{
+		opt: opt,
+	}
+}
+
+func (c mattermost) Name() string {
+	return MATTERMOST_NAME
+}
+
+type message struct {
+	Channel  string `json:"channel,omitempty"`
+	Username string `json:"username,omitempty"`
+	IconUrl  string `json:"icon_url,omitempty"`
+	Text     string `json:"text"`
+}
+
+func (c *mattermost) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	u := fmt.Sprintf("%s/hooks/%s", c.opt.Url, c.opt.HookId)
+
+	for _, channel := range bp.To {
+
+		m := message{
+			Channel:  channel,
+			Username: c.opt.BotName,
+			IconUrl:  c.opt.IconUrl,
+			Text:     bp.Content,
+		}
+
+		msg, err := json.Marshal(m)
+		if err != nil {
+			return err
+		}
+
+		res, err := beaconfire.PostJson(u, msg)
+		if err != nil {
+			return err
+		}
+
+		var r struct {
+			Ok          bool   `json:"ok"`
+			ErrorCode   int    `json:"error_code"`
+			Description string `json:"description"`
+		}
+		err = json.Unmarshal(res, &r)
+		if nil != err {
+			return err
+		}
+		if !r.Ok {
+			return errors.New(r.Description)
+		}
+	}
+	return nil
+}

+ 1 - 0
providers/mattermost_test.go

@@ -0,0 +1 @@
+package providers

+ 47 - 0
providers/slack.go

@@ -0,0 +1,47 @@
+package providers
+
+import (
+	"context"
+
+	"git.wenlab.co/joe/beaconfire"
+	"github.com/slack-go/slack"
+)
+
+const SLACKFIRE_NAME = "slackfire"
+
+type OptionsSlack struct {
+	AuthToken string
+}
+
+type slackfire struct {
+	opt *OptionsSlack
+}
+
+var _ beaconfire.BeaconFire = &slackfire{}
+
+func NewSlackfire(opt *OptionsSlack) *slackfire {
+	return &slackfire{
+		opt: opt,
+	}
+}
+
+func (c slackfire) Name() string {
+	return SLACKFIRE_NAME
+}
+
+func (c *slackfire) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	s := slack.New(c.opt.AuthToken)
+	for _, channel := range bp.To {
+		if _, _, err := s.PostMessageContext(
+			context.TODO(),
+			channel,
+			slack.MsgOptionText(bp.Content, false)); err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 63 - 0
providers/smtp.go

@@ -0,0 +1,63 @@
+package providers
+
+import (
+	"crypto/tls"
+
+	"git.wenlab.co/joe/beaconfire"
+	gomail "gopkg.in/gomail.v2"
+)
+
+const SMTP_NAME = "smtp"
+
+type OptionsSmtp struct {
+	Host               string
+	Port               int
+	InsecureSkipVerify bool
+	Username           string
+	Password           string
+}
+
+type smtp struct {
+	opt *OptionsSmtp
+}
+
+var _ beaconfire.BeaconFire = &smtp{}
+
+func NewSmtp(opt *OptionsSmtp) *smtp {
+	return &smtp{
+		opt: opt,
+	}
+}
+
+func (c *smtp) Name() string {
+	return SMTP_NAME
+}
+
+func (c *smtp) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	mail := gomail.NewMessage()
+	mail.SetHeader("From", bp.From)
+	mail.SetHeader("To", bp.To...)
+	mail.SetHeader("Subject", bp.Title)
+	inHtml := true // format == "html"
+	content := bp.FormatHTML()
+	if inHtml {
+		mail.SetBody("text/html", content)
+	} else {
+		mail.SetBody("text/plain", bp.Content)
+	}
+
+	var d *gomail.Dialer
+	if c.opt.Username != "" && c.opt.Password != "" {
+		d = gomail.NewDialer(c.opt.Host, c.opt.Port, c.opt.Username, c.opt.Password)
+	} else {
+		d = &gomail.Dialer{Host: c.opt.Host, Port: c.opt.Port}
+	}
+	if c.opt.InsecureSkipVerify {
+		d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+	}
+	return d.DialAndSend(mail)
+}

+ 47 - 0
providers/smtp_test.go

@@ -0,0 +1,47 @@
+package providers
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+func TestSmtp(t *testing.T) {
+	host := os.Getenv("SMTP_HOST")
+	port, _ := strconv.Atoi(os.Getenv("SMTP_PORT"))
+	username := os.Getenv("SMTP_USERNAME")
+	password := os.Getenv("SMTP_PASSWORD")
+	if len(host) <= 0 || len(username) <= 0 || len(password) <= 0 {
+		fmt.Println("SMTP env is empty")
+		return
+	}
+
+	FROM := ""
+	TO := ""
+
+	if len(FROM) <= 0 || len(TO) <= 0 {
+		fmt.Println("need config FROM && TO")
+		return
+	}
+	bf := NewSmtp(&OptionsSmtp{
+		Host:               host,
+		Port:               port,
+		InsecureSkipVerify: false,
+		Username:           username,
+		Password:           password,
+	})
+	err := bf.Send(&beaconfire.BeaconParam{
+		From:    FROM,
+		To:      []string{TO},
+		Ts:      time.Now().Unix(),
+		Title:   "test",
+		Content: "cfdfgddfdfdf",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+}

+ 78 - 0
providers/telegram.go

@@ -0,0 +1,78 @@
+package providers
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+const (
+	TELEGRAM_NAME = "telegram"
+
+	_TELEGRAM_DEF_PARSE_MODE = "HTML"
+)
+
+type OptionsTelegram struct {
+	Token string
+	// Channel []string
+	// ParseMode string
+}
+
+type telegram struct {
+	opt *OptionsTelegram
+}
+
+var _ beaconfire.BeaconFire = &telegram{}
+
+func NewTelegram(opt *OptionsTelegram) *telegram {
+	return &telegram{
+		opt: opt,
+	}
+}
+
+func (c *telegram) Name() string {
+	return TELEGRAM_NAME
+}
+
+func (c *telegram) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	u := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", c.opt.Token)
+
+	v := url.Values{}
+	content := bp.FormatHTML()
+	v.Set("parse_mode", _TELEGRAM_DEF_PARSE_MODE)
+	v.Set("text", content)
+
+	for _, channel := range bp.To {
+
+		v.Set("chat_id", channel)
+
+		res, err := beaconfire.Request(http.MethodPost, u, []byte(v.Encode()), map[string]string{
+			"Content-Type": "application/x-www-form-urlencoded",
+		}, "", "")
+		if err != nil {
+			return err
+		}
+
+		var r struct {
+			Ok          bool   `json:"ok"`
+			ErrorCode   int    `json:"error_code"`
+			Description string `json:"description"`
+		}
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return err
+		}
+		if !r.Ok {
+			return errors.New(r.Description)
+		}
+	}
+	return nil
+}

+ 37 - 0
providers/telegram_test.go

@@ -0,0 +1,37 @@
+package providers
+
+import (
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+func TestTelegram(t *testing.T) {
+	token := os.Getenv("TELEGRAM_TOKEN")
+	if len(token) <= 0 {
+		fmt.Println("env TELEGRAM_TOKEN is empty")
+		return
+	}
+
+	TO := ""
+	if len(TO) <= 0 {
+		fmt.Println("TO is needed for Telegram")
+	}
+
+	bf := NewTelegram(&OptionsTelegram{
+		Token: token,
+	})
+	err := bf.Send(&beaconfire.BeaconParam{
+		From:    "sender",
+		To:      []string{TO},
+		Ts:      time.Now().Unix(),
+		Title:   "title",
+		Content: "new Message",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+}

+ 79 - 0
providers/twilio.go

@@ -0,0 +1,79 @@
+package providers
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+const (
+	TWILIO_NAME = "twilio"
+)
+
+type OptionsTwilio struct {
+	AccountSid string
+	AuthToken  string
+}
+
+type twilio struct {
+	opt *OptionsTwilio
+}
+
+var _ beaconfire.BeaconFire = &twilio{}
+
+func NewTwilio(opt *OptionsTwilio) *twilio {
+	return &twilio{
+		opt: opt,
+	}
+}
+
+func (c twilio) Name() string {
+	return TWILIO_NAME
+}
+
+func (c *twilio) Send(bp *beaconfire.BeaconParam) error {
+	if len(bp.To) <= 0 {
+		return beaconfire.ErrNoReceiver
+	}
+
+	// hc := &http.Client{Timeout: time.Second * 10}
+	// v := url.Values{}
+	// v.Set("From", bp.From)
+	// v.Set("Body", bp.Content)
+	// for _, receiver := range bp.To {
+	// 	v.Set("To", receiver)
+	// 	urlStr := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", c.opt.AccountSid)
+	// 	req, err := http.NewRequest("POST", urlStr, strings.NewReader(v.Encode()))
+	// 	if err != nil {
+	// 		return err
+	// 	}
+
+	// 	req.SetBasicAuth(c.opt.AccountSid, c.opt.AuthToken)
+	// 	req.Header.Add("Accept", "application/json")
+	// 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	// 	_, err = hc.Do(req)
+	// 	if err != nil {
+	// 		return err
+	// 	}
+	// }
+	urlStr := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", c.opt.AccountSid)
+
+	v := url.Values{}
+	v.Set("From", bp.From)
+	v.Set("Body", bp.Content)
+
+	for _, receiver := range bp.To {
+		v.Set("To", receiver)
+
+		_, err := beaconfire.Request(http.MethodPost, urlStr, []byte(v.Encode()), map[string]string{
+			"Accept":       "application/json",
+			"Content-Type": "application/x-www-form-urlencoded",
+		}, c.opt.AccountSid, c.opt.AuthToken)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 88 - 0
providers/workwx.go

@@ -0,0 +1,88 @@
+package providers
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+// API:
+// https://work.weixin.qq.com/api/doc/90000/90136/91770
+
+const (
+	_WORKWX_ENDPOINT         = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send"
+	_WORKWX_MSGTYPE_MARKDOWN = "markdown" // this type would be lcompatible with others easily
+
+	WORKWX_NAME = "workwx"
+)
+
+type OptionsWorkwx struct {
+	Key string
+}
+
+type workwx struct {
+	opt *OptionsWorkwx
+}
+
+var _ beaconfire.BeaconFire = &workwx{}
+
+func NewWorkWx(opt *OptionsWorkwx) *workwx {
+	return &workwx{
+		opt: opt,
+	}
+}
+
+func (c *workwx) Name() string {
+	return WORKWX_NAME
+}
+
+func (c *workwx) Send(bp *beaconfire.BeaconParam) error {
+	msgtype := _WORKWX_MSGTYPE_MARKDOWN
+	// modify content
+
+	content := bp.Content
+	if msgtype == _WORKWX_MSGTYPE_MARKDOWN {
+		content = bp.FormatMarkdown()
+	}
+	url := fmt.Sprintf("%s?key=%s", _WORKWX_ENDPOINT, c.opt.Key)
+	values := map[string]interface{}{
+		"msgtype": msgtype,
+		msgtype: map[string]interface{}{
+			"content":        content,
+			"mentioned_list": bp.To,
+		},
+	}
+
+	jsonBuffer, err := json.Marshal(values)
+	if err != nil {
+		return err
+	}
+	data, err := beaconfire.PostJson(url, jsonBuffer)
+	if err != nil {
+		return err
+	}
+
+	var res struct {
+		ErrorCode int    `json:"errorcode"`
+		ErrMsg    string `json:"errmsg"`
+	}
+	err = json.NewDecoder(bytes.NewBuffer(data)).Decode(&res)
+	if err != nil {
+		return err
+	}
+
+	if res.ErrorCode != 0 {
+		return errors.New(res.ErrMsg)
+	}
+	/**
+	returns:
+	{
+		"errcode":0,
+		"errmsg":"ok"
+	}
+	*/
+	return nil
+}

+ 31 - 0
providers/workwx_test.go

@@ -0,0 +1,31 @@
+package providers
+
+import (
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"git.wenlab.co/joe/beaconfire"
+)
+
+func TestWorkwx(t *testing.T) {
+
+	key := os.Getenv("WORKWX_KEY")
+	if len(key) <= 0 {
+		fmt.Println("env WORKWX_KEY is empty")
+		return
+	}
+
+	bf := NewWorkWx(&OptionsWorkwx{
+		Key: key,
+	})
+
+	bf.Send(&beaconfire.BeaconParam{
+		From:    "sender",
+		To:      []string{""},
+		Ts:      time.Now().Unix(),
+		Title:   "title",
+		Content: "test message",
+	})
+}

+ 40 - 0
sample/main.go

@@ -0,0 +1,40 @@
+package main
+
+import (
+	"fmt"
+
+	"git.wenlab.co/joe/beaconfire"
+	"git.wenlab.co/joe/beaconfire/providers"
+)
+
+func main() {
+	// multi channels
+	var bfs []beaconfire.BeaconFire
+	// workwx
+	wx := providers.NewWorkWx(&providers.OptionsWorkwx{
+		Key: "xfdd",
+	})
+	// telegram
+	tg := providers.NewTelegram(&providers.OptionsTelegram{
+		Token: "ttt",
+	})
+	// together
+	bfs = append(bfs, wx, tg)
+
+	// send
+	var err error
+	var name string
+	for _, bf := range bfs {
+		// checker original
+		name = bf.Name()
+		if name == providers.TELEGRAM_NAME {
+			fmt.Println("telegram sending ...")
+		}
+		// send
+		err = bf.Send(beaconfire.NewBeaconParam("sender", "subject", "im a message", "", "receiver1", "receiver2"))
+		if err != nil {
+			// log
+			fmt.Println("Error:", err)
+		}
+	}
+}

+ 7 - 0
utils.go

@@ -0,0 +1,7 @@
+package beaconfire
+
+import "time"
+
+func Ts2Str(ts int64) string {
+	return time.Unix(ts, 0).Format("2006-01-02 15:04:05")
+}