1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327 |
- package weixin
- import (
- "bytes"
- "crypto/aes"
- "crypto/cipher"
- "crypto/sha1"
- "encoding/base64"
- "encoding/binary"
- "encoding/json"
- "encoding/xml"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "mime/multipart"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strings"
- "sync/atomic"
- "time"
- )
- // nolint
- const (
- // Event type
- msgEvent = "event"
- EventSubscribe = "subscribe"
- EventUnsubscribe = "unsubscribe"
- EventScan = "SCAN"
- EventView = "VIEW"
- EventClick = "CLICK"
- EventLocation = "LOCATION"
- EventTemplateSent = "TEMPLATESENDJOBFINISH"
- // Message type
- MsgTypeDefault = ".*"
- MsgTypeText = "text"
- MsgTypeImage = "image"
- MsgTypeVoice = "voice"
- MsgTypeVideo = "video"
- MsgTypeShortVideo = "shortvideo"
- MsgTypeLocation = "location"
- MsgTypeLink = "link"
- MsgTypeEvent = msgEvent + ".*"
- MsgTypeEventSubscribe = msgEvent + "\\." + EventSubscribe
- MsgTypeEventUnsubscribe = msgEvent + "\\." + EventUnsubscribe
- MsgTypeEventScan = msgEvent + "\\." + EventScan
- MsgTypeEventView = msgEvent + "\\." + EventView
- MsgTypeEventClick = msgEvent + "\\." + EventClick
- MsgTypeEventLocation = msgEvent + "\\." + EventLocation
- MsgTypeEventTemplateSent = msgEvent + "\\." + EventTemplateSent
- // Media type
- MediaTypeImage = "image"
- MediaTypeVoice = "voice"
- MediaTypeVideo = "video"
- MediaTypeThumb = "thumb"
- // Button type
- MenuButtonTypeKey = "click"
- MenuButtonTypeUrl = "view"
- MenuButtonTypeScancodePush = "scancode_push"
- MenuButtonTypeScancodeWaitmsg = "scancode_waitmsg"
- MenuButtonTypePicSysphoto = "pic_sysphoto"
- MenuButtonTypePicPhotoOrAlbum = "pic_photo_or_album"
- MenuButtonTypePicWeixin = "pic_weixin"
- MenuButtonTypeLocationSelect = "location_select"
- MenuButtonTypeMediaId = "media_id"
- MenuButtonTypeViewLimited = "view_limited"
- MenuButtonTypeMiniProgram = "miniprogram"
- // Template Status
- TemplateSentStatusSuccess = "success"
- TemplateSentStatusUserBlock = "failed:user block"
- TemplateSentStatusSystemFailed = "failed:system failed"
- // Redirect Scope
- RedirectURLScopeBasic = "snsapi_base"
- RedirectURLScopeUserInfo = "snsapi_userinfo"
- // Weixin host URL
- weixinHost = "https://api.weixin.qq.com/cgi-bin"
- weixinQRScene = "https://api.weixin.qq.com/cgi-bin/qrcode"
- weixinShowQRScene = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
- weixinMaterialURL = "https://api.weixin.qq.com/cgi-bin/material"
- weixinShortURL = "https://api.weixin.qq.com/cgi-bin/shorturl"
- weixinUserInfo = "https://api.weixin.qq.com/cgi-bin/user/info"
- weixinFileURL = "http://file.api.weixin.qq.com/cgi-bin/media"
- weixinTemplate = "https://api.weixin.qq.com/cgi-bin/template"
- weixinRedirectURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
- weixinUserAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
- weixinJsApiTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
- // Max retry count
- retryMaxN = 3
- // Reply format
- replyText = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
- replyImage = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
- replyVoice = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
- replyVideo = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
- replyMusic = "<xml>%s<MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><MusicUrl><![CDATA[%s]]></MusicUrl><HQMusicUrl><![CDATA[%s]]></HQMusicUrl><ThumbMediaId><![CDATA[%s]]></ThumbMediaId></Music></xml>"
- replyNews = "<xml>%s<MsgType><![CDATA[news]]></MsgType><ArticleCount>%d</ArticleCount><Articles>%s</Articles></xml>"
- replyHeader = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
- replyArticle = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
- transferCustomerService = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
- // Material request
- requestMaterial = `{"type":"%s","offset":%d,"count":%d}`
- // QR scene request
- requestQRScene = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
- requestQRSceneStr = `{"expire_seconds":%d,"action_name":"QR_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
- requestQRLimitScene = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
- requestQRLimitSceneStr = `{"action_name":"QR_LIMIT_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
- )
- // MessageHeader is the header of common message.
- type MessageHeader struct {
- ToUserName string
- FromUserName string
- CreateTime int
- MsgType string
- Encrypt string
- }
- // Request is weixin event request.
- type Request struct {
- MessageHeader
- MsgId int64 // nolint
- Content string
- PicUrl string // nolint
- MediaId string // nolint
- Format string
- ThumbMediaId string // nolint
- LocationX float32 `xml:"Location_X"`
- LocationY float32 `xml:"Location_Y"`
- Scale float32
- Label string
- Title string
- Description string
- Url string // nolint
- Event string
- EventKey string
- Ticket string
- Latitude float32
- Longitude float32
- Precision float32
- Recognition string
- Status string
- }
- // Music is the response of music message.
- type Music struct {
- Title string `json:"title"`
- Description string `json:"description"`
- MusicUrl string `json:"musicurl"` // nolint
- HQMusicUrl string `json:"hqmusicurl"` // nolint
- ThumbMediaId string `json:"thumb_media_id"` // nolint
- }
- // Article is the response of news message.
- type Article struct {
- Title string `json:"title"`
- Description string `json:"description"`
- PicUrl string `json:"picurl"` // nolint
- Url string `json:"url"` // nolint
- }
- // QRScene is the QR code.
- type QRScene struct {
- Ticket string `json:"ticket"`
- ExpireSeconds int `json:"expire_seconds"`
- Url string `json:"url,omitempty"` // nolint
- }
- // Menu is custom menu.
- type Menu struct {
- Buttons []MenuButton `json:"button,omitempty"`
- }
- // MenuButton is the button of custom menu.
- type MenuButton struct {
- Name string `json:"name"`
- Type string `json:"type,omitempty"`
- Key string `json:"key,omitempty"`
- Url string `json:"url,omitempty"` // nolint
- MediaId string `json:"media_id,omitempty"` // nolint
- SubButtons []MenuButton `json:"sub_button,omitempty"`
- AppId string `json:"appid,omitempty"` // nolint
- PagePath string `json:"pagepath,omitempty"`
- }
- // UserAccessToken access token for user.
- type UserAccessToken struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- ExpireSeconds int `json:"expires_in"`
- OpenId string `json:"openid"` // nolint
- Scope string `json:"scope"`
- UnionId string `json:"unionid,omitempty"` // nolint
- }
- // UserInfo store user information.
- type UserInfo struct {
- Subscribe int `json:"subscribe,omitempty"`
- Language string `json:"language,omitempty"`
- OpenId string `json:"openid,omitempty"` // nolint
- UnionId string `json:"unionid,omitempty"` // nolint
- Nickname string `json:"nickname,omitempty"`
- Sex int `json:"sex,omitempty"`
- City string `json:"city,omitempty"`
- Country string `json:"country,omitempty"`
- Province string `json:"province,omitempty"`
- HeadImageUrl string `json:"headimgurl,omitempty"` // nolint
- SubscribeTime int64 `json:"subscribe_time,omitempty"`
- Remark string `json:"remark,omitempty"`
- GroupId int `json:"groupid,omitempty"` // nolint
- }
- // Material data.
- type Material struct {
- MediaId string `json:"media_id,omitempty"` // nolint
- Name string `json:"name,omitempty"`
- UpdateTime int64 `json:"update_time,omitempty"`
- CreateTime int64 `json:"create_time,omitempty"`
- Url string `json:"url,omitempty"` // nolint
- Content struct {
- NewsItem []struct {
- Title string `json:"title,omitempty"`
- ThumbMediaId string `json:"thumb_media_id,omitempty"` // nolint
- ShowCoverPic int `json:"show_cover_pic,omitempty"`
- Author string `json:"author,omitempty"`
- Digest string `json:"digest,omitempty"`
- Content string `json:"content,omitempty"`
- Url string `json:"url,omitempty"` // nolint
- ContentSourceUrl string `json:"content_source_url,omitempty"` // nolint
- } `json:"news_item,omitempty"`
- } `json:"content,omitempty"`
- }
- // Materials is the list of material
- type Materials struct {
- TotalCount int `json:"total_count,omitempty"`
- ItemCount int `json:"item_count,omitempty"`
- Items []Material `json:"item,omitempty"`
- }
- // TmplData for mini program
- type TmplData map[string]TmplItem
- // TmplItem for mini program
- type TmplItem struct {
- Value string `json:"value,omitempty"`
- Color string `json:"color,omitempty"`
- }
- // TmplMiniProgram for mini program
- type TmplMiniProgram struct {
- AppId string `json:"appid,omitempty"` // nolint
- PagePath string `json:"pagepath,omitempty"`
- }
- // TmplMsg for mini program
- type TmplMsg struct {
- ToUser string `json:"touser"`
- TemplateId string `json:"template_id"` // nolint
- Url string `json:"url,omitempty"` // nolint 若填写跳转小程序 则此为版本过低的替代跳转url
- MiniProgram *TmplMiniProgram `json:"miniprogram,omitempty"` // 跳转小程序 选填
- Data TmplData `json:"data,omitempty"`
- Color string `json:"color,omitempty"` // 全局颜色
- }
- // ResponseWriter is used to output reply
- // nolint
- type ResponseWriter interface {
- // Get weixin
- GetWeixin() *Weixin
- GetUserData() interface{}
- // Reply message
- replyMsg(msg string)
- ReplyOK()
- ReplyText(text string)
- ReplyImage(mediaId string)
- ReplyVoice(mediaId string)
- ReplyVideo(mediaId string, title string, description string)
- ReplyMusic(music *Music)
- ReplyNews(articles []Article)
- TransferCustomerService(serviceId string)
- // Post message
- PostText(text string) error
- PostImage(mediaId string) error
- PostVoice(mediaId string) error
- PostVideo(mediaId string, title string, description string) error
- PostMusic(music *Music) error
- PostNews(articles []Article) error
- PostTemplateMessage(templateid string, url string, data TmplData) (int32, error)
- // Media operator
- UploadMediaFromFile(mediaType string, filepath string) (string, error)
- DownloadMediaToFile(mediaId string, filepath string) error
- UploadMedia(mediaType string, filename string, reader io.Reader) (string, error)
- DownloadMedia(mediaId string, writer io.Writer) error
- }
- type responseWriter struct {
- wx *Weixin
- writer http.ResponseWriter
- toUserName string
- fromUserName string
- }
- type response struct {
- ErrorCode int `json:"errcode,omitempty"`
- ErrorMessage string `json:"errmsg,omitempty"`
- }
- // HandlerFunc is callback function handler
- type HandlerFunc func(ResponseWriter, *Request)
- type route struct {
- regex *regexp.Regexp
- handler HandlerFunc
- }
- // AccessToken define weixin access token.
- type AccessToken struct {
- Token string
- Expires time.Time
- }
- type jsAPITicket struct {
- ticket string
- expires time.Time
- }
- // Weixin instance
- type Weixin struct {
- token string
- routes []*route
- tokenChan chan AccessToken
- ticketChan chan jsAPITicket
- userData interface{}
- appID string
- appSecret string
- refreshToken int32
- encodingAESKey []byte
- }
- // ToURL convert qr scene to url.
- func (qr *QRScene) ToURL() string {
- return (weixinShowQRScene + "?ticket=" + qr.Ticket)
- }
- // New create a Weixin instance.
- func New(token string, appid string, secret string) *Weixin {
- wx := &Weixin{}
- wx.token = token
- wx.appID = appid
- wx.appSecret = secret
- wx.refreshToken = 0
- wx.encodingAESKey = []byte{}
- if len(appid) > 0 && len(secret) > 0 {
- wx.tokenChan = make(chan AccessToken)
- go wx.createAccessToken(wx.tokenChan, appid, secret)
- wx.ticketChan = make(chan jsAPITicket)
- go createJsAPITicket(wx.tokenChan, wx.ticketChan)
- }
- return wx
- }
- // NewWithUserData create data with userdata.
- func NewWithUserData(token string, appid string, secret string, userData interface{}) *Weixin {
- wx := New(token, appid, secret)
- wx.userData = userData
- return wx
- }
- // SetEncodingAESKey set AES key
- func (wx *Weixin) SetEncodingAESKey(key string) error {
- k, err := base64.StdEncoding.DecodeString(key + "=")
- if err != nil {
- return err
- }
- wx.encodingAESKey = k
- return nil
- }
- // GetAppId retrun app id.
- func (wx *Weixin) GetAppId() string { // nolint
- return wx.appID
- }
- // GetAppSecret return app secret.
- func (wx *Weixin) GetAppSecret() string {
- return wx.appSecret
- }
- // RefreshAccessToken update access token.
- func (wx *Weixin) RefreshAccessToken() {
- atomic.StoreInt32(&wx.refreshToken, 1)
- <-wx.tokenChan
- }
- // GetAccessToken read access token.
- func (wx *Weixin) GetAccessToken() AccessToken {
- for i := 0; i < retryMaxN; i++ {
- token := <-wx.tokenChan
- if time.Since(token.Expires).Seconds() < 0 {
- return token
- }
- }
- return AccessToken{}
- }
- // HandleFunc used to register request callback.
- func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
- regex, err := regexp.Compile(pattern)
- if err != nil {
- panic(err)
- }
- route := &route{regex, handler}
- wx.routes = append(wx.routes, route)
- }
- // PostText used to post text message.
- func (wx *Weixin) PostText(touser string, text string) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- Text struct {
- Content string `json:"content"`
- } `json:"text"`
- }
- msg.ToUser = touser
- msg.MsgType = "text"
- msg.Text.Content = text
- return postMessage(wx.tokenChan, &msg)
- }
- // PostImage used to post image message.
- func (wx *Weixin) PostImage(touser string, mediaID string) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- Image struct {
- MediaID string `json:"media_id"`
- } `json:"image"`
- }
- msg.ToUser = touser
- msg.MsgType = "image"
- msg.Image.MediaID = mediaID
- return postMessage(wx.tokenChan, &msg)
- }
- // PostVoice used to post voice message.
- func (wx *Weixin) PostVoice(touser string, mediaID string) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- Voice struct {
- MediaID string `json:"media_id"`
- } `json:"voice"`
- }
- msg.ToUser = touser
- msg.MsgType = "voice"
- msg.Voice.MediaID = mediaID
- return postMessage(wx.tokenChan, &msg)
- }
- // PostVideo used to post video message.
- func (wx *Weixin) PostVideo(touser string, m string, t string, d string) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- Video struct {
- MediaID string `json:"media_id"`
- Title string `json:"title"`
- Description string `json:"description"`
- } `json:"video"`
- }
- msg.ToUser = touser
- msg.MsgType = "video"
- msg.Video.MediaID = m
- msg.Video.Title = t
- msg.Video.Description = d
- return postMessage(wx.tokenChan, &msg)
- }
- // PostMusic used to post music message.
- func (wx *Weixin) PostMusic(touser string, music *Music) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- Music *Music `json:"music"`
- }
- msg.ToUser = touser
- msg.MsgType = "video"
- msg.Music = music
- return postMessage(wx.tokenChan, &msg)
- }
- // PostNews used to post news message.
- func (wx *Weixin) PostNews(touser string, articles []Article) error {
- var msg struct {
- ToUser string `json:"touser"`
- MsgType string `json:"msgtype"`
- News struct {
- Articles []Article `json:"articles"`
- } `json:"news"`
- }
- msg.ToUser = touser
- msg.MsgType = "news"
- msg.News.Articles = articles
- return postMessage(wx.tokenChan, &msg)
- }
- // UploadMediaFromFile used to upload media from local file.
- func (wx *Weixin) UploadMediaFromFile(mediaType string, fp string) (string, error) {
- file, err := os.Open(fp)
- if err != nil {
- return "", err
- }
- defer file.Close()
- return wx.UploadMedia(mediaType, filepath.Base(fp), file)
- }
- // DownloadMediaToFile used to download media and save to local file.
- func (wx *Weixin) DownloadMediaToFile(mediaID string, fp string) error {
- file, err := os.Create(fp)
- if err != nil {
- return err
- }
- defer file.Close()
- return wx.DownloadMedia(mediaID, file)
- }
- // UploadMedia used to upload media with media.
- func (wx *Weixin) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
- return uploadMedia(wx.tokenChan, mediaType, filename, reader)
- }
- // DownloadMedia used to download media with media.
- func (wx *Weixin) DownloadMedia(mediaID string, writer io.Writer) error {
- return downloadMedia(wx.tokenChan, mediaID, writer)
- }
- // BatchGetMaterial used to batch get Material.
- func (wx *Weixin) BatchGetMaterial(materialType string, offset int, count int) (*Materials, error) {
- reply, err := postRequest(weixinMaterialURL+"/batchget_material?access_token=", wx.tokenChan,
- []byte(fmt.Sprintf(requestMaterial, materialType, offset, count)))
- if err != nil {
- return nil, err
- }
- var materials Materials
- if err := json.Unmarshal(reply, &materials); err != nil {
- return nil, err
- }
- return &materials, nil
- }
- // GetIpList used to get ip list.
- func (wx *Weixin) GetIpList() ([]string, error) { // nolint
- reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
- if err != nil {
- return nil, err
- }
- var result struct {
- IPList []string `json:"ip_list"`
- }
- if err := json.Unmarshal(reply, &result); err != nil {
- return nil, err
- }
- return result.IPList, nil
- }
- // CreateQRScene used to create QR scene.
- func (wx *Weixin) CreateQRScene(sceneID int, expires int) (*QRScene, error) {
- reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRScene, expires, sceneID)))
- if err != nil {
- return nil, err
- }
- var qr QRScene
- if err := json.Unmarshal(reply, &qr); err != nil {
- return nil, err
- }
- return &qr, nil
- }
- // CreateQRSceneByString used to create QR scene by str.
- func (wx *Weixin) CreateQRSceneByString(sceneStr string, expires int) (*QRScene, error) {
- reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRSceneStr, expires, sceneStr)))
- if err != nil {
- return nil, err
- }
- var qr QRScene
- if err := json.Unmarshal(reply, &qr); err != nil {
- return nil, err
- }
- return &qr, nil
- }
- // CreateQRLimitScene used to create QR limit scene.
- func (wx *Weixin) CreateQRLimitScene(sceneID int) (*QRScene, error) {
- reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitScene, sceneID)))
- if err != nil {
- return nil, err
- }
- var qr QRScene
- if err := json.Unmarshal(reply, &qr); err != nil {
- return nil, err
- }
- return &qr, nil
- }
- // CreateQRLimitSceneByString used to create QR limit scene by str.
- func (wx *Weixin) CreateQRLimitSceneByString(sceneStr string) (*QRScene, error) {
- reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitSceneStr, sceneStr)))
- if err != nil {
- return nil, err
- }
- var qr QRScene
- if err := json.Unmarshal(reply, &qr); err != nil {
- return nil, err
- }
- return &qr, nil
- }
- // ShortURL used to convert long url to short url
- func (wx *Weixin) ShortURL(url string) (string, error) {
- var request struct {
- Action string `json:"action"`
- LongURL string `json:"long_url"`
- }
- request.Action = "long2short"
- request.LongURL = url
- data, err := marshal(request)
- if err != nil {
- return "", err
- }
- reply, err := postRequest(weixinShortURL+"?access_token=", wx.tokenChan, data)
- if err != nil {
- return "", err
- }
- var shortURL struct {
- URL string `json:"short_url"`
- }
- if err := json.Unmarshal(reply, &shortURL); err != nil {
- return "", err
- }
- return shortURL.URL, nil
- }
- // CreateMenu used to create custom menu.
- func (wx *Weixin) CreateMenu(menu *Menu) error {
- data, err := marshal(menu)
- if err != nil {
- return err
- }
- _, err = postRequest(weixinHost+"/menu/create?access_token=", wx.tokenChan, data)
- return err
- }
- // GetMenu used to get menu.
- func (wx *Weixin) GetMenu() (*Menu, error) {
- reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
- if err != nil {
- return nil, err
- }
- var result struct {
- MenuCtx *Menu `json:"menu"`
- }
- if err := json.Unmarshal(reply, &result); err != nil {
- return nil, err
- }
- return result.MenuCtx, nil
- }
- // DeleteMenu used to delete menu.
- func (wx *Weixin) DeleteMenu() error {
- _, err := sendGetRequest(weixinHost+"/menu/delete?access_token=", wx.tokenChan)
- return err
- }
- // SetTemplateIndustry used to set template industry.
- func (wx *Weixin) SetTemplateIndustry(id1 string, id2 string) error {
- var industry struct {
- ID1 string `json:"industry_id1,omitempty"`
- ID2 string `json:"industry_id2,omitempty"`
- }
- industry.ID1 = id1
- industry.ID2 = id2
- data, err := marshal(industry)
- if err != nil {
- return err
- }
- _, err = postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
- return err
- }
- // AddTemplate used to add template.
- func (wx *Weixin) AddTemplate(shortid string) (string, error) {
- var request struct {
- Shortid string `json:"template_id_short,omitempty"`
- }
- request.Shortid = shortid
- data, err := marshal(request)
- if err != nil {
- return "", err
- }
- reply, err := postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
- if err != nil {
- return "", err
- }
- var templateID struct {
- ID string `json:"template_id,omitempty"`
- }
- if err := json.Unmarshal(reply, &templateID); err != nil {
- return "", err
- }
- return templateID.ID, nil
- }
- // PostTemplateMessage used to post template message.
- func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data TmplData) (int32, error) {
- var msg struct {
- ToUser string `json:"touser"`
- TemplateID string `json:"template_id"`
- URL string `json:"url,omitempty"`
- Data TmplData `json:"data,omitempty"`
- }
- msg.ToUser = touser
- msg.TemplateID = templateid
- msg.URL = url
- msg.Data = data
- msgStr, err := marshal(msg)
- if err != nil {
- return 0, err
- }
- reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
- if err != nil {
- return 0, err
- }
- var resp struct {
- MsgID int32 `json:"msgid,omitempty"`
- }
- if err := json.Unmarshal(reply, &resp); err != nil {
- return 0, err
- }
- return resp.MsgID, nil
- }
- // PostTemplateMessageMiniProgram 兼容模板消息跳转小程序
- func (wx *Weixin) PostTemplateMessageMiniProgram(msg *TmplMsg) (int64, error) {
- msgStr, err := marshal(msg)
- if err != nil {
- return 0, err
- }
- reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
- if err != nil {
- return 0, err
- }
- var resp struct {
- MsgID int64 `json:"msgid,omitempty"`
- }
- if err := json.Unmarshal(reply, &resp); err != nil {
- return 0, err
- }
- return resp.MsgID, nil
- }
- // CreateRedirectURL used to create redirect url
- func (wx *Weixin) CreateRedirectURL(urlStr string, scope string, state string) string {
- return fmt.Sprintf(weixinRedirectURL, wx.appID, url.QueryEscape(urlStr), scope, state)
- }
- // GetUserAccessToken used to get open id
- func (wx *Weixin) GetUserAccessToken(code string) (*UserAccessToken, error) {
- resp, err := http.Get(fmt.Sprintf(weixinUserAccessTokenURL, wx.appID, wx.appSecret, code))
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
- var res UserAccessToken
- if err := json.Unmarshal(body, &res); err != nil {
- return nil, err
- }
- return &res, nil
- }
- // GetUserInfo used to get user info
- func (wx *Weixin) GetUserInfo(openid string) (*UserInfo, error) {
- reply, err := sendGetRequest(fmt.Sprintf("%s?openid=%s&lang=zh_CN&access_token=", weixinUserInfo, openid), wx.tokenChan)
- if err != nil {
- return nil, err
- }
- var result UserInfo
- if err := json.Unmarshal(reply, &result); err != nil {
- return nil, err
- }
- return &result, nil
- }
- // GetJsAPITicket used to get js api ticket.
- func (wx *Weixin) GetJsAPITicket() (string, error) {
- for i := 0; i < retryMaxN; i++ {
- ticket := <-wx.ticketChan
- if time.Since(ticket.expires).Seconds() < 0 {
- return ticket.ticket, nil
- }
- }
- return "", errors.New("Get JsApi Ticket Timeout")
- }
- // JsSignature used to sign js url.
- func (wx *Weixin) JsSignature(url string, timestamp int64, noncestr string) (string, error) {
- ticket, err := wx.GetJsAPITicket()
- if err != nil {
- return "", err
- }
- h := sha1.New()
- h.Write([]byte(fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", // nolint
- ticket, noncestr, timestamp, url)))
- return fmt.Sprintf("%x", h.Sum(nil)), nil
- }
- // CreateHandlerFunc used to create handler function.
- func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- wx.ServeHTTP(w, r)
- }
- }
- // ServeHTTP used to process weixin request and send response.
- func (wx *Weixin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if !checkSignature(wx.token, w, r) {
- http.Error(w, "", http.StatusUnauthorized)
- return
- }
- // Verify request
- if r.Method == "GET" {
- fmt.Fprintf(w, r.FormValue("echostr")) // nolint
- return
- }
- // Process message
- data, err := ioutil.ReadAll(r.Body)
- if err != nil {
- log.Println("Weixin receive message failed:", err)
- http.Error(w, "", http.StatusBadRequest)
- } else {
- var msg Request
- if err := xml.Unmarshal(data, &msg); err != nil {
- log.Println("Weixin parse message failed:", err)
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- if len(wx.encodingAESKey) > 0 && len(msg.Encrypt) > 0 {
- // check encrypt
- d, err := base64.StdEncoding.DecodeString(msg.Encrypt)
- if err != nil {
- log.Println("Weixin decode base64 message failed:", err)
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- if len(d) <= 20 {
- log.Println("Weixin invalid aes message:", err)
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- // valid
- strs := sort.StringSlice{wx.token, r.FormValue("timestamp"), r.FormValue("nonce"), msg.Encrypt}
- sort.Strings(strs)
- if fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(strs, "")))) != r.FormValue("msg_signature") {
- log.Println("Weixin check message sign failed!")
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- // decode
- key := wx.encodingAESKey
- b, err := aes.NewCipher(key)
- if err != nil {
- log.Println("Weixin create cipher failed:", err)
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- bs := b.BlockSize()
- bm := cipher.NewCBCDecrypter(b, key[:bs])
- data = make([]byte, len(d))
- bm.CryptBlocks(data, d)
- data = fixPKCS7UnPadding(data)
- len := binary.BigEndian.Uint32(data[16:20])
- if err := xml.Unmarshal(data[20:(20+len)], &msg); err != nil {
- log.Println("Weixin parse aes message failed:", err)
- http.Error(w, "", http.StatusBadRequest)
- return
- }
- }
- wx.routeRequest(w, &msg)
- }
- return
- }
- func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
- requestPath := r.MsgType
- if requestPath == msgEvent {
- requestPath += "." + r.Event
- }
- for _, route := range wx.routes {
- if !route.regex.MatchString(requestPath) {
- continue
- }
- writer := responseWriter{}
- writer.wx = wx
- writer.writer = w
- writer.toUserName = r.FromUserName
- writer.fromUserName = r.ToUserName
- route.handler(writer, r)
- return
- }
- http.Error(w, "", http.StatusNotFound)
- return
- }
- func marshal(v interface{}) ([]byte, error) {
- data, err := json.Marshal(v)
- if err == nil {
- data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
- data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
- data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
- }
- return data, err
- }
- func fixPKCS7UnPadding(data []byte) []byte {
- length := len(data)
- unpadding := int(data[length-1])
- return data[:(length - unpadding)]
- }
- func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
- r.ParseForm() // nolint
- signature := r.FormValue("signature")
- timestamp := r.FormValue("timestamp")
- nonce := r.FormValue("nonce")
- strs := sort.StringSlice{t, timestamp, nonce}
- sort.Strings(strs)
- var str string
- for _, s := range strs {
- str += s
- }
- h := sha1.New()
- h.Write([]byte(str)) // nolint
- return fmt.Sprintf("%x", h.Sum(nil)) == signature
- }
- func authAccessToken(appid string, secret string) (string, time.Duration) {
- resp, err := http.Get(weixinHost + "/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret)
- if err != nil {
- log.Println("Get access token failed: ", err)
- } else {
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- log.Println("Read access token failed: ", err)
- } else {
- var res struct {
- AccessToken string `json:"access_token"`
- ExpiresIn int64 `json:"expires_in"`
- }
- if err := json.Unmarshal(body, &res); err != nil {
- log.Println("Parse access token failed: ", err)
- } else {
- //log.Printf("AuthAccessToken token=%s expires_in=%d", res.AccessToken, res.ExpiresIn)
- return res.AccessToken, time.Duration(res.ExpiresIn * 1000 * 1000 * 1000)
- }
- }
- }
- return "", 0
- }
- func getJsAPITicket(c chan AccessToken) (*jsAPITicket, error) {
- reply, err := sendGetRequest(weixinJsApiTicketURL+"?type=jsapi&access_token=", c)
- if err != nil {
- return nil, err
- }
- var res struct {
- Ticket string `json:"ticket"`
- ExpiresIn int64 `json:"expires_in"`
- }
- if err := json.Unmarshal(reply, &res); err != nil {
- return nil, err
- }
- var ticket jsAPITicket
- ticket.ticket = res.Ticket
- ticket.expires = time.Now().Add(time.Duration(res.ExpiresIn * 1000 * 1000 * 1000))
- return &ticket, nil
- }
- func (wx *Weixin) createAccessToken(c chan AccessToken, appid string, secret string) {
- token := AccessToken{"", time.Now()}
- c <- token
- for {
- swapped := atomic.CompareAndSwapInt32(&wx.refreshToken, 1, 0)
- if swapped || time.Since(token.Expires).Seconds() >= 0 {
- var expires time.Duration
- token.Token, expires = authAccessToken(appid, secret)
- token.Expires = time.Now().Add(expires)
- }
- c <- token
- }
- }
- func createJsAPITicket(cin chan AccessToken, c chan jsAPITicket) {
- ticket := jsAPITicket{"", time.Now()}
- c <- ticket
- for {
- if time.Since(ticket.expires).Seconds() >= 0 {
- t, err := getJsAPITicket(cin)
- if err == nil {
- ticket = *t
- }
- }
- c <- ticket
- }
- }
- func sendGetRequest(reqURL string, c chan AccessToken) ([]byte, error) {
- for i := 0; i < retryMaxN; i++ {
- token := <-c
- if time.Since(token.Expires).Seconds() < 0 {
- r, err := http.Get(reqURL + token.Token)
- if err != nil {
- return nil, err
- }
- defer r.Body.Close()
- reply, err := ioutil.ReadAll(r.Body)
- if err != nil {
- return nil, err
- }
- var result response
- if err := json.Unmarshal(reply, &result); err != nil {
- return nil, err
- }
- switch result.ErrorCode {
- case 0:
- return reply, nil
- case 42001: // access_token timeout and retry
- continue
- default:
- return nil, fmt.Errorf("WeiXin send get request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
- }
- }
- }
- return nil, errors.New("WeiXin post request too many times:" + reqURL)
- }
- func postRequest(reqURL string, c chan AccessToken, data []byte) ([]byte, error) {
- for i := 0; i < retryMaxN; i++ {
- token := <-c
- if time.Since(token.Expires).Seconds() < 0 {
- r, err := http.Post(reqURL+token.Token, "application/json; charset=utf-8", bytes.NewReader(data))
- if err != nil {
- return nil, err
- }
- defer r.Body.Close()
- reply, err := ioutil.ReadAll(r.Body)
- if err != nil {
- return nil, err
- }
- var result response
- if err := json.Unmarshal(reply, &result); err != nil {
- return nil, err
- }
- switch result.ErrorCode {
- case 0:
- return reply, nil
- case 42001: // access_token timeout and retry
- continue
- default:
- return nil, fmt.Errorf("WeiXin send post request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
- }
- }
- }
- return nil, errors.New("WeiXin post request too many times:" + reqURL)
- }
- func postMessage(c chan AccessToken, msg interface{}) error {
- data, err := marshal(msg)
- if err != nil {
- return err
- }
- _, err = postRequest(weixinHost+"/message/custom/send?access_token=", c, data)
- return err
- }
- // nolint: gocyclo
- func uploadMedia(c chan AccessToken, mediaType string, filename string, reader io.Reader) (string, error) {
- reqURL := weixinFileURL + "/upload?type=" + mediaType + "&access_token="
- for i := 0; i < retryMaxN; i++ {
- token := <-c
- if time.Since(token.Expires).Seconds() < 0 {
- bodyBuf := &bytes.Buffer{}
- bodyWriter := multipart.NewWriter(bodyBuf)
- fileWriter, err := bodyWriter.CreateFormFile("filename", filename)
- if err != nil {
- return "", err
- }
- if _, err = io.Copy(fileWriter, reader); err != nil {
- return "", err
- }
- contentType := bodyWriter.FormDataContentType()
- bodyWriter.Close() // nolint
- r, err := http.Post(reqURL+token.Token, contentType, bodyBuf)
- if err != nil {
- return "", err
- }
- defer r.Body.Close()
- reply, err := ioutil.ReadAll(r.Body)
- if err != nil {
- return "", err
- }
- var result struct {
- response
- Type string `json:"type"`
- MediaID string `json:"media_id"`
- CreatedAt int64 `json:"created_at"`
- }
- err = json.Unmarshal(reply, &result)
- if err != nil {
- return "", err
- }
- switch result.ErrorCode {
- case 0:
- return result.MediaID, nil
- case 42001: // access_token timeout and retry
- continue
- default:
- return "", fmt.Errorf("WeiXin upload[%d]: %s", result.ErrorCode, result.ErrorMessage)
- }
- }
- }
- return "", errors.New("WeiXin upload media too many times")
- }
- func downloadMedia(c chan AccessToken, mediaID string, writer io.Writer) error {
- reqURL := weixinFileURL + "/get?media_id=" + mediaID + "&access_token="
- for i := 0; i < retryMaxN; i++ {
- token := <-c
- if time.Since(token.Expires).Seconds() < 0 {
- r, err := http.Get(reqURL + token.Token)
- if err != nil {
- return err
- }
- defer r.Body.Close()
- if r.Header.Get("Content-Type") != "text/plain" {
- _, err = io.Copy(writer, r.Body)
- return err
- }
- reply, err := ioutil.ReadAll(r.Body)
- if err != nil {
- return err
- }
- var result response
- if err := json.Unmarshal(reply, &result); err != nil {
- return err
- }
- switch result.ErrorCode {
- case 0:
- return nil
- case 42001: // access_token timeout and retry
- continue
- default:
- return fmt.Errorf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage)
- }
- }
- }
- return errors.New("WeiXin download media too many times")
- }
- // Format reply message header.
- func (w responseWriter) replyHeader() string {
- return fmt.Sprintf(replyHeader, w.toUserName, w.fromUserName, time.Now().Unix())
- }
- // Return weixin instance.
- func (w responseWriter) GetWeixin() *Weixin {
- return w.wx
- }
- // Return user data.
- func (w responseWriter) GetUserData() interface{} {
- return w.wx.userData
- }
- func (w responseWriter) replyMsg(msg string) {
- w.writer.Write([]byte(msg))
- }
- // ReplyOK used to reply empty message.
- func (w responseWriter) ReplyOK() {
- w.replyMsg("success")
- }
- // ReplyText used to reply text message.
- func (w responseWriter) ReplyText(text string) {
- w.replyMsg(fmt.Sprintf(replyText, w.replyHeader(), text))
- }
- // ReplyImage used to reply image message.
- func (w responseWriter) ReplyImage(mediaID string) {
- w.replyMsg(fmt.Sprintf(replyImage, w.replyHeader(), mediaID))
- }
- // ReplyVoice used to reply voice message.
- func (w responseWriter) ReplyVoice(mediaID string) {
- w.replyMsg(fmt.Sprintf(replyVoice, w.replyHeader(), mediaID))
- }
- // ReplyVideo used to reply video message
- func (w responseWriter) ReplyVideo(mediaID string, title string, description string) {
- w.replyMsg(fmt.Sprintf(replyVideo, w.replyHeader(), mediaID, title, description))
- }
- // ReplyMusic used to reply music message
- func (w responseWriter) ReplyMusic(m *Music) {
- msg := fmt.Sprintf(replyMusic, w.replyHeader(), m.Title, m.Description, m.MusicUrl, m.HQMusicUrl, m.ThumbMediaId)
- w.replyMsg(msg)
- }
- // ReplyNews used to reply news message (max 10 news)
- func (w responseWriter) ReplyNews(articles []Article) {
- var ctx string
- for _, article := range articles {
- ctx += fmt.Sprintf(replyArticle, article.Title, article.Description, article.PicUrl, article.Url)
- }
- msg := fmt.Sprintf(replyNews, w.replyHeader(), len(articles), ctx)
- w.replyMsg(msg)
- }
- // TransferCustomerService used to tTransfer customer service
- func (w responseWriter) TransferCustomerService(serviceID string) {
- msg := fmt.Sprintf(transferCustomerService, serviceID, w.fromUserName, time.Now().Unix())
- w.replyMsg(msg)
- }
- // PostText used to Post text message
- func (w responseWriter) PostText(text string) error {
- return w.wx.PostText(w.toUserName, text)
- }
- // Post image message
- func (w responseWriter) PostImage(mediaID string) error {
- return w.wx.PostImage(w.toUserName, mediaID)
- }
- // Post voice message
- func (w responseWriter) PostVoice(mediaID string) error {
- return w.wx.PostVoice(w.toUserName, mediaID)
- }
- // Post video message
- func (w responseWriter) PostVideo(mediaID string, title string, desc string) error {
- return w.wx.PostVideo(w.toUserName, mediaID, title, desc)
- }
- // Post music message
- func (w responseWriter) PostMusic(music *Music) error {
- return w.wx.PostMusic(w.toUserName, music)
- }
- // Post news message
- func (w responseWriter) PostNews(articles []Article) error {
- return w.wx.PostNews(w.toUserName, articles)
- }
- // Post template message
- func (w responseWriter) PostTemplateMessage(templateid string, url string, data TmplData) (int32, error) {
- return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
- }
- // Upload media from local file
- func (w responseWriter) UploadMediaFromFile(mediaType string, filepath string) (string, error) {
- return w.wx.UploadMediaFromFile(mediaType, filepath)
- }
- // Download media and save to local file
- func (w responseWriter) DownloadMediaToFile(mediaID string, filepath string) error {
- return w.wx.DownloadMediaToFile(mediaID, filepath)
- }
- // Upload media with reader
- func (w responseWriter) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
- return w.wx.UploadMedia(mediaType, filename, reader)
- }
- // Download media with writer
- func (w responseWriter) DownloadMedia(mediaID string, writer io.Writer) error {
- return w.wx.DownloadMedia(mediaID, writer)
- }
- /*************************************
- 以下为自定义扩展
- **************************************/
- func (wx *Weixin) PostCustomMsg(url string, obj interface{}) (bs []byte, err error) {
- var data []byte
- data, err = json.Marshal(obj)
- if err != nil {
- return
- }
- bs, err = postRequest(url, wx.tokenChan, data)
- return
- }
- //Post custom message(消息结构体完全由开发人员自定义)
- func (wx *Weixin) PostTextCustom(url string, obj interface{}) error {
- data, err := json.Marshal(obj)
- if err != nil {
- return err
- }
- log.Println("custom msg:", string(data))
- _, err = postRequest(url, wx.tokenChan, data)
- return err
- }
|