diff --git a/go.mod b/go.mod index daa9932..f07b728 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/xen0n/go-workwx/v2 go 1.21 -toolchain go1.23.4 require ( github.com/PuerkitoBio/goquery v1.10.1 github.com/cenkalti/backoff/v4 v4.3.0 + github.com/go-playground/validator/v10 v10.23.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/smartystreets/goconvey v1.8.1 github.com/urfave/cli/v2 v2.27.5 @@ -15,9 +15,15 @@ require ( require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index b45c560..c86fdd7 100644 --- a/go.sum +++ b/go.sum @@ -6,17 +6,35 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -27,6 +45,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -60,6 +79,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -87,3 +107,5 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/message_types.go b/message_types.go new file mode 100644 index 0000000..97b0d14 --- /dev/null +++ b/message_types.go @@ -0,0 +1,220 @@ +package workwx + +import ( + "encoding/json" + + "github.com/go-playground/validator/v10" +) + +var ( + validate = validator.New() +) + +type WebHookMessage interface { + Struct2Map() (map[string]any, error) + Validate() error +} + +// See https://developer.work.weixin.qq.com/document/path/99110#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BA + +// TextMessage 文本消息 +type TextMessage struct { + Text struct { + Content string `json:"content" validate:"required"` + MentionedList []string `json:"mentioned_list"` + MentionedMobileList []string `json:"mentioned_mobile_list"` + } `json:"text"` +} + +func (t *TextMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *TextMessage) Validate() error { + return validate.Struct(t) +} + +// MarkdownMessage markdown +type MarkdownMessage struct { + Markdown struct { + Content string `json:"content" validate:"required"` + } `json:"markdown" validate:"required"` +} + +func (t *MarkdownMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *MarkdownMessage) Validate() error { + return validate.Struct(t) +} + +// ImageMessage 图片类型 +type ImageMessage struct { + Image struct { + Base64 string `json:"base64" validate:"required"` //图片内容的base64编码 + Md5 string `json:"md5" validate:"required"` + } `json:"image" validate:"required"` +} + +func (t *ImageMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *ImageMessage) Validate() error { + return validate.Struct(t) +} + +// VoiceMessage 语音类型 +type VoiceMessage struct { + Voice struct { + MediaID string `json:"media_id" validate:"required" ` // 语音文件id,通过下文的文件上传接口获取 + } `json:"voice" validate:"required"` +} + +func (t *VoiceMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *VoiceMessage) Validate() error { + return validate.Struct(t) +} + +// ImageArticles 图文类型 +type ImageArticles struct { + News struct { + Articles []struct { + Title string `json:"title" validate:"required"` + Description string `json:"description" validate:"required"` + URL string `json:"url" validate:"required"` + PicURL string `json:"picurl"` + } `json:"articles"` + } `json:"news"` +} + +func (t *ImageArticles) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *ImageArticles) Validate() error { + return validate.Struct(t) +} + +type FileMessage struct { + File struct { + MediaID string `json:"media_id" validate:"required"` //文件id,通过下文的文件上传接口获取 + } `json:"file" validate:"required"` +} + +func (t *FileMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *FileMessage) Validate() error { + return validate.Struct(t) +} + +type TemplateCardMessage struct { + TemplateCard struct { + CardType string `json:"card_type" validate:"required"` + Source struct { + IconURL string `json:"icon_url"` + Desc string `json:"desc"` + DescColor int `json:"desc_color"` + } `json:"source"` + MainTitle struct { + Title string `json:"title"` + Desc string `json:"desc"` + } `json:"main_title" validate:"required"` + EmphasisContent struct { + Title string `json:"title"` + Desc string `json:"desc"` + } `json:"emphasis_content"` + QuoteArea struct { + Type int `json:"type"` + URL string `json:"url"` + Appid string `json:"appid"` + Pagepath string `json:"pagepath"` + Title string `json:"title"` + QuoteText string `json:"quote_text"` + } `json:"quote_area"` + SubTitleText string `json:"sub_title_text"` + HorizontalContentList []struct { + Keyname string `json:"keyname" validate:"required"` + Value string `json:"value"` + Type int `json:"type,omitempty"` + URL string `json:"url,omitempty"` + MediaID string `json:"media_id,omitempty"` + } `json:"horizontal_content_list"` + JumpList []struct { + Type int `json:"type"` + URL string `json:"url,omitempty"` + Title string `json:"title" validate:"required"` + Appid string `json:"appid,omitempty"` + Pagepath string `json:"pagepath,omitempty"` + } `json:"jump_list"` + CardAction struct { + Type int `json:"type" validate:"required"` + URL string `json:"url"` + Appid string `json:"appid"` + Pagepath string `json:"pagepath"` + } `json:"card_action" validate:"required"` + } `json:"template_card"` +} + +func (t *TemplateCardMessage) Struct2Map() (map[string]any, error) { + var dataMap = make(map[string]any) + buf, err := json.Marshal(t) + if err != nil { + return nil, err + } + err = json.Unmarshal(buf, &dataMap) + + return dataMap, err +} + +func (t *TemplateCardMessage) Validate() error { + return validate.Struct(t) +} diff --git a/webhook_message.go b/webhook_message.go index f7b41d8..6240df6 100644 --- a/webhook_message.go +++ b/webhook_message.go @@ -68,3 +68,35 @@ func (c *WebhookClient) sendMessage( return nil } + +// SendMessage 机器人支持文本(text)、markdown(markdown)、图片(image)、图文(news)、文件(file)、语音(voice)、模板卡片(template_card)七种消息类型 +func (c *WebhookClient) SendMessage(msg WebHookMessage) error { + + if err := msg.Validate(); err != nil { + return err + } + req, err := msg.Struct2Map() + if err != nil { + return err + } + switch msg.(type) { + case *TextMessage: + req["msgtype"] = "text" + case *MarkdownMessage: + req["msgtype"] = "markdown" + + case *ImageMessage: + req["msgtype"] = "image" + case *ImageArticles: + req["msgtype"] = "news" + + case *FileMessage: + req["msgtype"] = "file" + case *VoiceMessage: + req["msgtype"] = "voice" + case *TemplateCardMessage: + req["msgtype"] = "template_card" + } + return c.executeQyapiJSONPost("/cgi-bin/webhook/send", req, nil) + +}