weixin.go 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327
  1. package weixin
  2. import (
  3. "bytes"
  4. "crypto/aes"
  5. "crypto/cipher"
  6. "crypto/sha1"
  7. "encoding/base64"
  8. "encoding/binary"
  9. "encoding/json"
  10. "encoding/xml"
  11. "errors"
  12. "fmt"
  13. "io"
  14. "io/ioutil"
  15. "log"
  16. "mime/multipart"
  17. "net/http"
  18. "net/url"
  19. "os"
  20. "path/filepath"
  21. "regexp"
  22. "sort"
  23. "strings"
  24. "sync/atomic"
  25. "time"
  26. )
  27. // nolint
  28. const (
  29. // Event type
  30. msgEvent = "event"
  31. EventSubscribe = "subscribe"
  32. EventUnsubscribe = "unsubscribe"
  33. EventScan = "SCAN"
  34. EventView = "VIEW"
  35. EventClick = "CLICK"
  36. EventLocation = "LOCATION"
  37. EventTemplateSent = "TEMPLATESENDJOBFINISH"
  38. // Message type
  39. MsgTypeDefault = ".*"
  40. MsgTypeText = "text"
  41. MsgTypeImage = "image"
  42. MsgTypeVoice = "voice"
  43. MsgTypeVideo = "video"
  44. MsgTypeShortVideo = "shortvideo"
  45. MsgTypeLocation = "location"
  46. MsgTypeLink = "link"
  47. MsgTypeEvent = msgEvent + ".*"
  48. MsgTypeEventSubscribe = msgEvent + "\\." + EventSubscribe
  49. MsgTypeEventUnsubscribe = msgEvent + "\\." + EventUnsubscribe
  50. MsgTypeEventScan = msgEvent + "\\." + EventScan
  51. MsgTypeEventView = msgEvent + "\\." + EventView
  52. MsgTypeEventClick = msgEvent + "\\." + EventClick
  53. MsgTypeEventLocation = msgEvent + "\\." + EventLocation
  54. MsgTypeEventTemplateSent = msgEvent + "\\." + EventTemplateSent
  55. // Media type
  56. MediaTypeImage = "image"
  57. MediaTypeVoice = "voice"
  58. MediaTypeVideo = "video"
  59. MediaTypeThumb = "thumb"
  60. // Button type
  61. MenuButtonTypeKey = "click"
  62. MenuButtonTypeUrl = "view"
  63. MenuButtonTypeScancodePush = "scancode_push"
  64. MenuButtonTypeScancodeWaitmsg = "scancode_waitmsg"
  65. MenuButtonTypePicSysphoto = "pic_sysphoto"
  66. MenuButtonTypePicPhotoOrAlbum = "pic_photo_or_album"
  67. MenuButtonTypePicWeixin = "pic_weixin"
  68. MenuButtonTypeLocationSelect = "location_select"
  69. MenuButtonTypeMediaId = "media_id"
  70. MenuButtonTypeViewLimited = "view_limited"
  71. MenuButtonTypeMiniProgram = "miniprogram"
  72. // Template Status
  73. TemplateSentStatusSuccess = "success"
  74. TemplateSentStatusUserBlock = "failed:user block"
  75. TemplateSentStatusSystemFailed = "failed:system failed"
  76. // Redirect Scope
  77. RedirectURLScopeBasic = "snsapi_base"
  78. RedirectURLScopeUserInfo = "snsapi_userinfo"
  79. // Weixin host URL
  80. weixinHost = "https://api.weixin.qq.com/cgi-bin"
  81. weixinQRScene = "https://api.weixin.qq.com/cgi-bin/qrcode"
  82. weixinShowQRScene = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
  83. weixinMaterialURL = "https://api.weixin.qq.com/cgi-bin/material"
  84. weixinShortURL = "https://api.weixin.qq.com/cgi-bin/shorturl"
  85. weixinUserInfo = "https://api.weixin.qq.com/cgi-bin/user/info"
  86. weixinFileURL = "http://file.api.weixin.qq.com/cgi-bin/media"
  87. weixinTemplate = "https://api.weixin.qq.com/cgi-bin/template"
  88. weixinRedirectURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
  89. weixinUserAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
  90. weixinJsApiTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
  91. // Max retry count
  92. retryMaxN = 3
  93. // Reply format
  94. replyText = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
  95. replyImage = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
  96. replyVoice = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
  97. replyVideo = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
  98. 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>"
  99. replyNews = "<xml>%s<MsgType><![CDATA[news]]></MsgType><ArticleCount>%d</ArticleCount><Articles>%s</Articles></xml>"
  100. replyHeader = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
  101. replyArticle = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
  102. transferCustomerService = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
  103. // Material request
  104. requestMaterial = `{"type":"%s","offset":%d,"count":%d}`
  105. // QR scene request
  106. requestQRScene = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
  107. requestQRSceneStr = `{"expire_seconds":%d,"action_name":"QR_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
  108. requestQRLimitScene = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
  109. requestQRLimitSceneStr = `{"action_name":"QR_LIMIT_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
  110. )
  111. // MessageHeader is the header of common message.
  112. type MessageHeader struct {
  113. ToUserName string
  114. FromUserName string
  115. CreateTime int
  116. MsgType string
  117. Encrypt string
  118. }
  119. // Request is weixin event request.
  120. type Request struct {
  121. MessageHeader
  122. MsgId int64 // nolint
  123. Content string
  124. PicUrl string // nolint
  125. MediaId string // nolint
  126. Format string
  127. ThumbMediaId string // nolint
  128. LocationX float32 `xml:"Location_X"`
  129. LocationY float32 `xml:"Location_Y"`
  130. Scale float32
  131. Label string
  132. Title string
  133. Description string
  134. Url string // nolint
  135. Event string
  136. EventKey string
  137. Ticket string
  138. Latitude float32
  139. Longitude float32
  140. Precision float32
  141. Recognition string
  142. Status string
  143. }
  144. // Music is the response of music message.
  145. type Music struct {
  146. Title string `json:"title"`
  147. Description string `json:"description"`
  148. MusicUrl string `json:"musicurl"` // nolint
  149. HQMusicUrl string `json:"hqmusicurl"` // nolint
  150. ThumbMediaId string `json:"thumb_media_id"` // nolint
  151. }
  152. // Article is the response of news message.
  153. type Article struct {
  154. Title string `json:"title"`
  155. Description string `json:"description"`
  156. PicUrl string `json:"picurl"` // nolint
  157. Url string `json:"url"` // nolint
  158. }
  159. // QRScene is the QR code.
  160. type QRScene struct {
  161. Ticket string `json:"ticket"`
  162. ExpireSeconds int `json:"expire_seconds"`
  163. Url string `json:"url,omitempty"` // nolint
  164. }
  165. // Menu is custom menu.
  166. type Menu struct {
  167. Buttons []MenuButton `json:"button,omitempty"`
  168. }
  169. // MenuButton is the button of custom menu.
  170. type MenuButton struct {
  171. Name string `json:"name"`
  172. Type string `json:"type,omitempty"`
  173. Key string `json:"key,omitempty"`
  174. Url string `json:"url,omitempty"` // nolint
  175. MediaId string `json:"media_id,omitempty"` // nolint
  176. SubButtons []MenuButton `json:"sub_button,omitempty"`
  177. AppId string `json:"appid,omitempty"` // nolint
  178. PagePath string `json:"pagepath,omitempty"`
  179. }
  180. // UserAccessToken access token for user.
  181. type UserAccessToken struct {
  182. AccessToken string `json:"access_token"`
  183. RefreshToken string `json:"refresh_token"`
  184. ExpireSeconds int `json:"expires_in"`
  185. OpenId string `json:"openid"` // nolint
  186. Scope string `json:"scope"`
  187. UnionId string `json:"unionid,omitempty"` // nolint
  188. }
  189. // UserInfo store user information.
  190. type UserInfo struct {
  191. Subscribe int `json:"subscribe,omitempty"`
  192. Language string `json:"language,omitempty"`
  193. OpenId string `json:"openid,omitempty"` // nolint
  194. UnionId string `json:"unionid,omitempty"` // nolint
  195. Nickname string `json:"nickname,omitempty"`
  196. Sex int `json:"sex,omitempty"`
  197. City string `json:"city,omitempty"`
  198. Country string `json:"country,omitempty"`
  199. Province string `json:"province,omitempty"`
  200. HeadImageUrl string `json:"headimgurl,omitempty"` // nolint
  201. SubscribeTime int64 `json:"subscribe_time,omitempty"`
  202. Remark string `json:"remark,omitempty"`
  203. GroupId int `json:"groupid,omitempty"` // nolint
  204. }
  205. // Material data.
  206. type Material struct {
  207. MediaId string `json:"media_id,omitempty"` // nolint
  208. Name string `json:"name,omitempty"`
  209. UpdateTime int64 `json:"update_time,omitempty"`
  210. CreateTime int64 `json:"create_time,omitempty"`
  211. Url string `json:"url,omitempty"` // nolint
  212. Content struct {
  213. NewsItem []struct {
  214. Title string `json:"title,omitempty"`
  215. ThumbMediaId string `json:"thumb_media_id,omitempty"` // nolint
  216. ShowCoverPic int `json:"show_cover_pic,omitempty"`
  217. Author string `json:"author,omitempty"`
  218. Digest string `json:"digest,omitempty"`
  219. Content string `json:"content,omitempty"`
  220. Url string `json:"url,omitempty"` // nolint
  221. ContentSourceUrl string `json:"content_source_url,omitempty"` // nolint
  222. } `json:"news_item,omitempty"`
  223. } `json:"content,omitempty"`
  224. }
  225. // Materials is the list of material
  226. type Materials struct {
  227. TotalCount int `json:"total_count,omitempty"`
  228. ItemCount int `json:"item_count,omitempty"`
  229. Items []Material `json:"item,omitempty"`
  230. }
  231. // TmplData for mini program
  232. type TmplData map[string]TmplItem
  233. // TmplItem for mini program
  234. type TmplItem struct {
  235. Value string `json:"value,omitempty"`
  236. Color string `json:"color,omitempty"`
  237. }
  238. // TmplMiniProgram for mini program
  239. type TmplMiniProgram struct {
  240. AppId string `json:"appid,omitempty"` // nolint
  241. PagePath string `json:"pagepath,omitempty"`
  242. }
  243. // TmplMsg for mini program
  244. type TmplMsg struct {
  245. ToUser string `json:"touser"`
  246. TemplateId string `json:"template_id"` // nolint
  247. Url string `json:"url,omitempty"` // nolint 若填写跳转小程序 则此为版本过低的替代跳转url
  248. MiniProgram *TmplMiniProgram `json:"miniprogram,omitempty"` // 跳转小程序 选填
  249. Data TmplData `json:"data,omitempty"`
  250. Color string `json:"color,omitempty"` // 全局颜色
  251. }
  252. // ResponseWriter is used to output reply
  253. // nolint
  254. type ResponseWriter interface {
  255. // Get weixin
  256. GetWeixin() *Weixin
  257. GetUserData() interface{}
  258. // Reply message
  259. replyMsg(msg string)
  260. ReplyOK()
  261. ReplyText(text string)
  262. ReplyImage(mediaId string)
  263. ReplyVoice(mediaId string)
  264. ReplyVideo(mediaId string, title string, description string)
  265. ReplyMusic(music *Music)
  266. ReplyNews(articles []Article)
  267. TransferCustomerService(serviceId string)
  268. // Post message
  269. PostText(text string) error
  270. PostImage(mediaId string) error
  271. PostVoice(mediaId string) error
  272. PostVideo(mediaId string, title string, description string) error
  273. PostMusic(music *Music) error
  274. PostNews(articles []Article) error
  275. PostTemplateMessage(templateid string, url string, data TmplData) (int32, error)
  276. // Media operator
  277. UploadMediaFromFile(mediaType string, filepath string) (string, error)
  278. DownloadMediaToFile(mediaId string, filepath string) error
  279. UploadMedia(mediaType string, filename string, reader io.Reader) (string, error)
  280. DownloadMedia(mediaId string, writer io.Writer) error
  281. }
  282. type responseWriter struct {
  283. wx *Weixin
  284. writer http.ResponseWriter
  285. toUserName string
  286. fromUserName string
  287. }
  288. type response struct {
  289. ErrorCode int `json:"errcode,omitempty"`
  290. ErrorMessage string `json:"errmsg,omitempty"`
  291. }
  292. // HandlerFunc is callback function handler
  293. type HandlerFunc func(ResponseWriter, *Request)
  294. type route struct {
  295. regex *regexp.Regexp
  296. handler HandlerFunc
  297. }
  298. // AccessToken define weixin access token.
  299. type AccessToken struct {
  300. Token string
  301. Expires time.Time
  302. }
  303. type jsAPITicket struct {
  304. ticket string
  305. expires time.Time
  306. }
  307. // Weixin instance
  308. type Weixin struct {
  309. token string
  310. routes []*route
  311. tokenChan chan AccessToken
  312. ticketChan chan jsAPITicket
  313. userData interface{}
  314. appID string
  315. appSecret string
  316. refreshToken int32
  317. encodingAESKey []byte
  318. }
  319. // ToURL convert qr scene to url.
  320. func (qr *QRScene) ToURL() string {
  321. return (weixinShowQRScene + "?ticket=" + qr.Ticket)
  322. }
  323. // New create a Weixin instance.
  324. func New(token string, appid string, secret string) *Weixin {
  325. wx := &Weixin{}
  326. wx.token = token
  327. wx.appID = appid
  328. wx.appSecret = secret
  329. wx.refreshToken = 0
  330. wx.encodingAESKey = []byte{}
  331. if len(appid) > 0 && len(secret) > 0 {
  332. wx.tokenChan = make(chan AccessToken)
  333. go wx.createAccessToken(wx.tokenChan, appid, secret)
  334. wx.ticketChan = make(chan jsAPITicket)
  335. go createJsAPITicket(wx.tokenChan, wx.ticketChan)
  336. }
  337. return wx
  338. }
  339. // NewWithUserData create data with userdata.
  340. func NewWithUserData(token string, appid string, secret string, userData interface{}) *Weixin {
  341. wx := New(token, appid, secret)
  342. wx.userData = userData
  343. return wx
  344. }
  345. // SetEncodingAESKey set AES key
  346. func (wx *Weixin) SetEncodingAESKey(key string) error {
  347. k, err := base64.StdEncoding.DecodeString(key + "=")
  348. if err != nil {
  349. return err
  350. }
  351. wx.encodingAESKey = k
  352. return nil
  353. }
  354. // GetAppId retrun app id.
  355. func (wx *Weixin) GetAppId() string { // nolint
  356. return wx.appID
  357. }
  358. // GetAppSecret return app secret.
  359. func (wx *Weixin) GetAppSecret() string {
  360. return wx.appSecret
  361. }
  362. // RefreshAccessToken update access token.
  363. func (wx *Weixin) RefreshAccessToken() {
  364. atomic.StoreInt32(&wx.refreshToken, 1)
  365. <-wx.tokenChan
  366. }
  367. // GetAccessToken read access token.
  368. func (wx *Weixin) GetAccessToken() AccessToken {
  369. for i := 0; i < retryMaxN; i++ {
  370. token := <-wx.tokenChan
  371. if time.Since(token.Expires).Seconds() < 0 {
  372. return token
  373. }
  374. }
  375. return AccessToken{}
  376. }
  377. // HandleFunc used to register request callback.
  378. func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
  379. regex, err := regexp.Compile(pattern)
  380. if err != nil {
  381. panic(err)
  382. }
  383. route := &route{regex, handler}
  384. wx.routes = append(wx.routes, route)
  385. }
  386. // PostText used to post text message.
  387. func (wx *Weixin) PostText(touser string, text string) error {
  388. var msg struct {
  389. ToUser string `json:"touser"`
  390. MsgType string `json:"msgtype"`
  391. Text struct {
  392. Content string `json:"content"`
  393. } `json:"text"`
  394. }
  395. msg.ToUser = touser
  396. msg.MsgType = "text"
  397. msg.Text.Content = text
  398. return postMessage(wx.tokenChan, &msg)
  399. }
  400. // PostImage used to post image message.
  401. func (wx *Weixin) PostImage(touser string, mediaID string) error {
  402. var msg struct {
  403. ToUser string `json:"touser"`
  404. MsgType string `json:"msgtype"`
  405. Image struct {
  406. MediaID string `json:"media_id"`
  407. } `json:"image"`
  408. }
  409. msg.ToUser = touser
  410. msg.MsgType = "image"
  411. msg.Image.MediaID = mediaID
  412. return postMessage(wx.tokenChan, &msg)
  413. }
  414. // PostVoice used to post voice message.
  415. func (wx *Weixin) PostVoice(touser string, mediaID string) error {
  416. var msg struct {
  417. ToUser string `json:"touser"`
  418. MsgType string `json:"msgtype"`
  419. Voice struct {
  420. MediaID string `json:"media_id"`
  421. } `json:"voice"`
  422. }
  423. msg.ToUser = touser
  424. msg.MsgType = "voice"
  425. msg.Voice.MediaID = mediaID
  426. return postMessage(wx.tokenChan, &msg)
  427. }
  428. // PostVideo used to post video message.
  429. func (wx *Weixin) PostVideo(touser string, m string, t string, d string) error {
  430. var msg struct {
  431. ToUser string `json:"touser"`
  432. MsgType string `json:"msgtype"`
  433. Video struct {
  434. MediaID string `json:"media_id"`
  435. Title string `json:"title"`
  436. Description string `json:"description"`
  437. } `json:"video"`
  438. }
  439. msg.ToUser = touser
  440. msg.MsgType = "video"
  441. msg.Video.MediaID = m
  442. msg.Video.Title = t
  443. msg.Video.Description = d
  444. return postMessage(wx.tokenChan, &msg)
  445. }
  446. // PostMusic used to post music message.
  447. func (wx *Weixin) PostMusic(touser string, music *Music) error {
  448. var msg struct {
  449. ToUser string `json:"touser"`
  450. MsgType string `json:"msgtype"`
  451. Music *Music `json:"music"`
  452. }
  453. msg.ToUser = touser
  454. msg.MsgType = "video"
  455. msg.Music = music
  456. return postMessage(wx.tokenChan, &msg)
  457. }
  458. // PostNews used to post news message.
  459. func (wx *Weixin) PostNews(touser string, articles []Article) error {
  460. var msg struct {
  461. ToUser string `json:"touser"`
  462. MsgType string `json:"msgtype"`
  463. News struct {
  464. Articles []Article `json:"articles"`
  465. } `json:"news"`
  466. }
  467. msg.ToUser = touser
  468. msg.MsgType = "news"
  469. msg.News.Articles = articles
  470. return postMessage(wx.tokenChan, &msg)
  471. }
  472. // UploadMediaFromFile used to upload media from local file.
  473. func (wx *Weixin) UploadMediaFromFile(mediaType string, fp string) (string, error) {
  474. file, err := os.Open(fp)
  475. if err != nil {
  476. return "", err
  477. }
  478. defer file.Close()
  479. return wx.UploadMedia(mediaType, filepath.Base(fp), file)
  480. }
  481. // DownloadMediaToFile used to download media and save to local file.
  482. func (wx *Weixin) DownloadMediaToFile(mediaID string, fp string) error {
  483. file, err := os.Create(fp)
  484. if err != nil {
  485. return err
  486. }
  487. defer file.Close()
  488. return wx.DownloadMedia(mediaID, file)
  489. }
  490. // UploadMedia used to upload media with media.
  491. func (wx *Weixin) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
  492. return uploadMedia(wx.tokenChan, mediaType, filename, reader)
  493. }
  494. // DownloadMedia used to download media with media.
  495. func (wx *Weixin) DownloadMedia(mediaID string, writer io.Writer) error {
  496. return downloadMedia(wx.tokenChan, mediaID, writer)
  497. }
  498. // BatchGetMaterial used to batch get Material.
  499. func (wx *Weixin) BatchGetMaterial(materialType string, offset int, count int) (*Materials, error) {
  500. reply, err := postRequest(weixinMaterialURL+"/batchget_material?access_token=", wx.tokenChan,
  501. []byte(fmt.Sprintf(requestMaterial, materialType, offset, count)))
  502. if err != nil {
  503. return nil, err
  504. }
  505. var materials Materials
  506. if err := json.Unmarshal(reply, &materials); err != nil {
  507. return nil, err
  508. }
  509. return &materials, nil
  510. }
  511. // GetIpList used to get ip list.
  512. func (wx *Weixin) GetIpList() ([]string, error) { // nolint
  513. reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
  514. if err != nil {
  515. return nil, err
  516. }
  517. var result struct {
  518. IPList []string `json:"ip_list"`
  519. }
  520. if err := json.Unmarshal(reply, &result); err != nil {
  521. return nil, err
  522. }
  523. return result.IPList, nil
  524. }
  525. // CreateQRScene used to create QR scene.
  526. func (wx *Weixin) CreateQRScene(sceneID int, expires int) (*QRScene, error) {
  527. reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRScene, expires, sceneID)))
  528. if err != nil {
  529. return nil, err
  530. }
  531. var qr QRScene
  532. if err := json.Unmarshal(reply, &qr); err != nil {
  533. return nil, err
  534. }
  535. return &qr, nil
  536. }
  537. // CreateQRSceneByString used to create QR scene by str.
  538. func (wx *Weixin) CreateQRSceneByString(sceneStr string, expires int) (*QRScene, error) {
  539. reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRSceneStr, expires, sceneStr)))
  540. if err != nil {
  541. return nil, err
  542. }
  543. var qr QRScene
  544. if err := json.Unmarshal(reply, &qr); err != nil {
  545. return nil, err
  546. }
  547. return &qr, nil
  548. }
  549. // CreateQRLimitScene used to create QR limit scene.
  550. func (wx *Weixin) CreateQRLimitScene(sceneID int) (*QRScene, error) {
  551. reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitScene, sceneID)))
  552. if err != nil {
  553. return nil, err
  554. }
  555. var qr QRScene
  556. if err := json.Unmarshal(reply, &qr); err != nil {
  557. return nil, err
  558. }
  559. return &qr, nil
  560. }
  561. // CreateQRLimitSceneByString used to create QR limit scene by str.
  562. func (wx *Weixin) CreateQRLimitSceneByString(sceneStr string) (*QRScene, error) {
  563. reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitSceneStr, sceneStr)))
  564. if err != nil {
  565. return nil, err
  566. }
  567. var qr QRScene
  568. if err := json.Unmarshal(reply, &qr); err != nil {
  569. return nil, err
  570. }
  571. return &qr, nil
  572. }
  573. // ShortURL used to convert long url to short url
  574. func (wx *Weixin) ShortURL(url string) (string, error) {
  575. var request struct {
  576. Action string `json:"action"`
  577. LongURL string `json:"long_url"`
  578. }
  579. request.Action = "long2short"
  580. request.LongURL = url
  581. data, err := marshal(request)
  582. if err != nil {
  583. return "", err
  584. }
  585. reply, err := postRequest(weixinShortURL+"?access_token=", wx.tokenChan, data)
  586. if err != nil {
  587. return "", err
  588. }
  589. var shortURL struct {
  590. URL string `json:"short_url"`
  591. }
  592. if err := json.Unmarshal(reply, &shortURL); err != nil {
  593. return "", err
  594. }
  595. return shortURL.URL, nil
  596. }
  597. // CreateMenu used to create custom menu.
  598. func (wx *Weixin) CreateMenu(menu *Menu) error {
  599. data, err := marshal(menu)
  600. if err != nil {
  601. return err
  602. }
  603. _, err = postRequest(weixinHost+"/menu/create?access_token=", wx.tokenChan, data)
  604. return err
  605. }
  606. // GetMenu used to get menu.
  607. func (wx *Weixin) GetMenu() (*Menu, error) {
  608. reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
  609. if err != nil {
  610. return nil, err
  611. }
  612. var result struct {
  613. MenuCtx *Menu `json:"menu"`
  614. }
  615. if err := json.Unmarshal(reply, &result); err != nil {
  616. return nil, err
  617. }
  618. return result.MenuCtx, nil
  619. }
  620. // DeleteMenu used to delete menu.
  621. func (wx *Weixin) DeleteMenu() error {
  622. _, err := sendGetRequest(weixinHost+"/menu/delete?access_token=", wx.tokenChan)
  623. return err
  624. }
  625. // SetTemplateIndustry used to set template industry.
  626. func (wx *Weixin) SetTemplateIndustry(id1 string, id2 string) error {
  627. var industry struct {
  628. ID1 string `json:"industry_id1,omitempty"`
  629. ID2 string `json:"industry_id2,omitempty"`
  630. }
  631. industry.ID1 = id1
  632. industry.ID2 = id2
  633. data, err := marshal(industry)
  634. if err != nil {
  635. return err
  636. }
  637. _, err = postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
  638. return err
  639. }
  640. // AddTemplate used to add template.
  641. func (wx *Weixin) AddTemplate(shortid string) (string, error) {
  642. var request struct {
  643. Shortid string `json:"template_id_short,omitempty"`
  644. }
  645. request.Shortid = shortid
  646. data, err := marshal(request)
  647. if err != nil {
  648. return "", err
  649. }
  650. reply, err := postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
  651. if err != nil {
  652. return "", err
  653. }
  654. var templateID struct {
  655. ID string `json:"template_id,omitempty"`
  656. }
  657. if err := json.Unmarshal(reply, &templateID); err != nil {
  658. return "", err
  659. }
  660. return templateID.ID, nil
  661. }
  662. // PostTemplateMessage used to post template message.
  663. func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data TmplData) (int32, error) {
  664. var msg struct {
  665. ToUser string `json:"touser"`
  666. TemplateID string `json:"template_id"`
  667. URL string `json:"url,omitempty"`
  668. Data TmplData `json:"data,omitempty"`
  669. }
  670. msg.ToUser = touser
  671. msg.TemplateID = templateid
  672. msg.URL = url
  673. msg.Data = data
  674. msgStr, err := marshal(msg)
  675. if err != nil {
  676. return 0, err
  677. }
  678. reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
  679. if err != nil {
  680. return 0, err
  681. }
  682. var resp struct {
  683. MsgID int32 `json:"msgid,omitempty"`
  684. }
  685. if err := json.Unmarshal(reply, &resp); err != nil {
  686. return 0, err
  687. }
  688. return resp.MsgID, nil
  689. }
  690. // PostTemplateMessageMiniProgram 兼容模板消息跳转小程序
  691. func (wx *Weixin) PostTemplateMessageMiniProgram(msg *TmplMsg) (int64, error) {
  692. msgStr, err := marshal(msg)
  693. if err != nil {
  694. return 0, err
  695. }
  696. reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
  697. if err != nil {
  698. return 0, err
  699. }
  700. var resp struct {
  701. MsgID int64 `json:"msgid,omitempty"`
  702. }
  703. if err := json.Unmarshal(reply, &resp); err != nil {
  704. return 0, err
  705. }
  706. return resp.MsgID, nil
  707. }
  708. // CreateRedirectURL used to create redirect url
  709. func (wx *Weixin) CreateRedirectURL(urlStr string, scope string, state string) string {
  710. return fmt.Sprintf(weixinRedirectURL, wx.appID, url.QueryEscape(urlStr), scope, state)
  711. }
  712. // GetUserAccessToken used to get open id
  713. func (wx *Weixin) GetUserAccessToken(code string) (*UserAccessToken, error) {
  714. resp, err := http.Get(fmt.Sprintf(weixinUserAccessTokenURL, wx.appID, wx.appSecret, code))
  715. if err != nil {
  716. return nil, err
  717. }
  718. defer resp.Body.Close()
  719. body, err := ioutil.ReadAll(resp.Body)
  720. if err != nil {
  721. return nil, err
  722. }
  723. var res UserAccessToken
  724. if err := json.Unmarshal(body, &res); err != nil {
  725. return nil, err
  726. }
  727. return &res, nil
  728. }
  729. // GetUserInfo used to get user info
  730. func (wx *Weixin) GetUserInfo(openid string) (*UserInfo, error) {
  731. reply, err := sendGetRequest(fmt.Sprintf("%s?openid=%s&lang=zh_CN&access_token=", weixinUserInfo, openid), wx.tokenChan)
  732. if err != nil {
  733. return nil, err
  734. }
  735. var result UserInfo
  736. if err := json.Unmarshal(reply, &result); err != nil {
  737. return nil, err
  738. }
  739. return &result, nil
  740. }
  741. // GetJsAPITicket used to get js api ticket.
  742. func (wx *Weixin) GetJsAPITicket() (string, error) {
  743. for i := 0; i < retryMaxN; i++ {
  744. ticket := <-wx.ticketChan
  745. if time.Since(ticket.expires).Seconds() < 0 {
  746. return ticket.ticket, nil
  747. }
  748. }
  749. return "", errors.New("Get JsApi Ticket Timeout")
  750. }
  751. // JsSignature used to sign js url.
  752. func (wx *Weixin) JsSignature(url string, timestamp int64, noncestr string) (string, error) {
  753. ticket, err := wx.GetJsAPITicket()
  754. if err != nil {
  755. return "", err
  756. }
  757. h := sha1.New()
  758. h.Write([]byte(fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s", // nolint
  759. ticket, noncestr, timestamp, url)))
  760. return fmt.Sprintf("%x", h.Sum(nil)), nil
  761. }
  762. // CreateHandlerFunc used to create handler function.
  763. func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
  764. return func(w http.ResponseWriter, r *http.Request) {
  765. wx.ServeHTTP(w, r)
  766. }
  767. }
  768. // ServeHTTP used to process weixin request and send response.
  769. func (wx *Weixin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  770. if !checkSignature(wx.token, w, r) {
  771. http.Error(w, "", http.StatusUnauthorized)
  772. return
  773. }
  774. // Verify request
  775. if r.Method == "GET" {
  776. fmt.Fprintf(w, r.FormValue("echostr")) // nolint
  777. return
  778. }
  779. // Process message
  780. data, err := ioutil.ReadAll(r.Body)
  781. if err != nil {
  782. log.Println("Weixin receive message failed:", err)
  783. http.Error(w, "", http.StatusBadRequest)
  784. } else {
  785. var msg Request
  786. if err := xml.Unmarshal(data, &msg); err != nil {
  787. log.Println("Weixin parse message failed:", err)
  788. http.Error(w, "", http.StatusBadRequest)
  789. return
  790. }
  791. if len(wx.encodingAESKey) > 0 && len(msg.Encrypt) > 0 {
  792. // check encrypt
  793. d, err := base64.StdEncoding.DecodeString(msg.Encrypt)
  794. if err != nil {
  795. log.Println("Weixin decode base64 message failed:", err)
  796. http.Error(w, "", http.StatusBadRequest)
  797. return
  798. }
  799. if len(d) <= 20 {
  800. log.Println("Weixin invalid aes message:", err)
  801. http.Error(w, "", http.StatusBadRequest)
  802. return
  803. }
  804. // valid
  805. strs := sort.StringSlice{wx.token, r.FormValue("timestamp"), r.FormValue("nonce"), msg.Encrypt}
  806. sort.Strings(strs)
  807. if fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(strs, "")))) != r.FormValue("msg_signature") {
  808. log.Println("Weixin check message sign failed!")
  809. http.Error(w, "", http.StatusBadRequest)
  810. return
  811. }
  812. // decode
  813. key := wx.encodingAESKey
  814. b, err := aes.NewCipher(key)
  815. if err != nil {
  816. log.Println("Weixin create cipher failed:", err)
  817. http.Error(w, "", http.StatusBadRequest)
  818. return
  819. }
  820. bs := b.BlockSize()
  821. bm := cipher.NewCBCDecrypter(b, key[:bs])
  822. data = make([]byte, len(d))
  823. bm.CryptBlocks(data, d)
  824. data = fixPKCS7UnPadding(data)
  825. len := binary.BigEndian.Uint32(data[16:20])
  826. if err := xml.Unmarshal(data[20:(20+len)], &msg); err != nil {
  827. log.Println("Weixin parse aes message failed:", err)
  828. http.Error(w, "", http.StatusBadRequest)
  829. return
  830. }
  831. }
  832. wx.routeRequest(w, &msg)
  833. }
  834. return
  835. }
  836. func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
  837. requestPath := r.MsgType
  838. if requestPath == msgEvent {
  839. requestPath += "." + r.Event
  840. }
  841. for _, route := range wx.routes {
  842. if !route.regex.MatchString(requestPath) {
  843. continue
  844. }
  845. writer := responseWriter{}
  846. writer.wx = wx
  847. writer.writer = w
  848. writer.toUserName = r.FromUserName
  849. writer.fromUserName = r.ToUserName
  850. route.handler(writer, r)
  851. return
  852. }
  853. http.Error(w, "", http.StatusNotFound)
  854. return
  855. }
  856. func marshal(v interface{}) ([]byte, error) {
  857. data, err := json.Marshal(v)
  858. if err == nil {
  859. data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
  860. data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
  861. data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
  862. }
  863. return data, err
  864. }
  865. func fixPKCS7UnPadding(data []byte) []byte {
  866. length := len(data)
  867. unpadding := int(data[length-1])
  868. return data[:(length - unpadding)]
  869. }
  870. func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
  871. r.ParseForm() // nolint
  872. signature := r.FormValue("signature")
  873. timestamp := r.FormValue("timestamp")
  874. nonce := r.FormValue("nonce")
  875. strs := sort.StringSlice{t, timestamp, nonce}
  876. sort.Strings(strs)
  877. var str string
  878. for _, s := range strs {
  879. str += s
  880. }
  881. h := sha1.New()
  882. h.Write([]byte(str)) // nolint
  883. return fmt.Sprintf("%x", h.Sum(nil)) == signature
  884. }
  885. func authAccessToken(appid string, secret string) (string, time.Duration) {
  886. resp, err := http.Get(weixinHost + "/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret)
  887. if err != nil {
  888. log.Println("Get access token failed: ", err)
  889. } else {
  890. defer resp.Body.Close()
  891. body, err := ioutil.ReadAll(resp.Body)
  892. if err != nil {
  893. log.Println("Read access token failed: ", err)
  894. } else {
  895. var res struct {
  896. AccessToken string `json:"access_token"`
  897. ExpiresIn int64 `json:"expires_in"`
  898. }
  899. if err := json.Unmarshal(body, &res); err != nil {
  900. log.Println("Parse access token failed: ", err)
  901. } else {
  902. //log.Printf("AuthAccessToken token=%s expires_in=%d", res.AccessToken, res.ExpiresIn)
  903. return res.AccessToken, time.Duration(res.ExpiresIn * 1000 * 1000 * 1000)
  904. }
  905. }
  906. }
  907. return "", 0
  908. }
  909. func getJsAPITicket(c chan AccessToken) (*jsAPITicket, error) {
  910. reply, err := sendGetRequest(weixinJsApiTicketURL+"?type=jsapi&access_token=", c)
  911. if err != nil {
  912. return nil, err
  913. }
  914. var res struct {
  915. Ticket string `json:"ticket"`
  916. ExpiresIn int64 `json:"expires_in"`
  917. }
  918. if err := json.Unmarshal(reply, &res); err != nil {
  919. return nil, err
  920. }
  921. var ticket jsAPITicket
  922. ticket.ticket = res.Ticket
  923. ticket.expires = time.Now().Add(time.Duration(res.ExpiresIn * 1000 * 1000 * 1000))
  924. return &ticket, nil
  925. }
  926. func (wx *Weixin) createAccessToken(c chan AccessToken, appid string, secret string) {
  927. token := AccessToken{"", time.Now()}
  928. c <- token
  929. for {
  930. swapped := atomic.CompareAndSwapInt32(&wx.refreshToken, 1, 0)
  931. if swapped || time.Since(token.Expires).Seconds() >= 0 {
  932. var expires time.Duration
  933. token.Token, expires = authAccessToken(appid, secret)
  934. token.Expires = time.Now().Add(expires)
  935. }
  936. c <- token
  937. }
  938. }
  939. func createJsAPITicket(cin chan AccessToken, c chan jsAPITicket) {
  940. ticket := jsAPITicket{"", time.Now()}
  941. c <- ticket
  942. for {
  943. if time.Since(ticket.expires).Seconds() >= 0 {
  944. t, err := getJsAPITicket(cin)
  945. if err == nil {
  946. ticket = *t
  947. }
  948. }
  949. c <- ticket
  950. }
  951. }
  952. func sendGetRequest(reqURL string, c chan AccessToken) ([]byte, error) {
  953. for i := 0; i < retryMaxN; i++ {
  954. token := <-c
  955. if time.Since(token.Expires).Seconds() < 0 {
  956. r, err := http.Get(reqURL + token.Token)
  957. if err != nil {
  958. return nil, err
  959. }
  960. defer r.Body.Close()
  961. reply, err := ioutil.ReadAll(r.Body)
  962. if err != nil {
  963. return nil, err
  964. }
  965. var result response
  966. if err := json.Unmarshal(reply, &result); err != nil {
  967. return nil, err
  968. }
  969. switch result.ErrorCode {
  970. case 0:
  971. return reply, nil
  972. case 42001: // access_token timeout and retry
  973. continue
  974. default:
  975. return nil, fmt.Errorf("WeiXin send get request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
  976. }
  977. }
  978. }
  979. return nil, errors.New("WeiXin post request too many times:" + reqURL)
  980. }
  981. func postRequest(reqURL string, c chan AccessToken, data []byte) ([]byte, error) {
  982. for i := 0; i < retryMaxN; i++ {
  983. token := <-c
  984. if time.Since(token.Expires).Seconds() < 0 {
  985. r, err := http.Post(reqURL+token.Token, "application/json; charset=utf-8", bytes.NewReader(data))
  986. if err != nil {
  987. return nil, err
  988. }
  989. defer r.Body.Close()
  990. reply, err := ioutil.ReadAll(r.Body)
  991. if err != nil {
  992. return nil, err
  993. }
  994. var result response
  995. if err := json.Unmarshal(reply, &result); err != nil {
  996. return nil, err
  997. }
  998. switch result.ErrorCode {
  999. case 0:
  1000. return reply, nil
  1001. case 42001: // access_token timeout and retry
  1002. continue
  1003. default:
  1004. return nil, fmt.Errorf("WeiXin send post request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
  1005. }
  1006. }
  1007. }
  1008. return nil, errors.New("WeiXin post request too many times:" + reqURL)
  1009. }
  1010. func postMessage(c chan AccessToken, msg interface{}) error {
  1011. data, err := marshal(msg)
  1012. if err != nil {
  1013. return err
  1014. }
  1015. _, err = postRequest(weixinHost+"/message/custom/send?access_token=", c, data)
  1016. return err
  1017. }
  1018. // nolint: gocyclo
  1019. func uploadMedia(c chan AccessToken, mediaType string, filename string, reader io.Reader) (string, error) {
  1020. reqURL := weixinFileURL + "/upload?type=" + mediaType + "&access_token="
  1021. for i := 0; i < retryMaxN; i++ {
  1022. token := <-c
  1023. if time.Since(token.Expires).Seconds() < 0 {
  1024. bodyBuf := &bytes.Buffer{}
  1025. bodyWriter := multipart.NewWriter(bodyBuf)
  1026. fileWriter, err := bodyWriter.CreateFormFile("filename", filename)
  1027. if err != nil {
  1028. return "", err
  1029. }
  1030. if _, err = io.Copy(fileWriter, reader); err != nil {
  1031. return "", err
  1032. }
  1033. contentType := bodyWriter.FormDataContentType()
  1034. bodyWriter.Close() // nolint
  1035. r, err := http.Post(reqURL+token.Token, contentType, bodyBuf)
  1036. if err != nil {
  1037. return "", err
  1038. }
  1039. defer r.Body.Close()
  1040. reply, err := ioutil.ReadAll(r.Body)
  1041. if err != nil {
  1042. return "", err
  1043. }
  1044. var result struct {
  1045. response
  1046. Type string `json:"type"`
  1047. MediaID string `json:"media_id"`
  1048. CreatedAt int64 `json:"created_at"`
  1049. }
  1050. err = json.Unmarshal(reply, &result)
  1051. if err != nil {
  1052. return "", err
  1053. }
  1054. switch result.ErrorCode {
  1055. case 0:
  1056. return result.MediaID, nil
  1057. case 42001: // access_token timeout and retry
  1058. continue
  1059. default:
  1060. return "", fmt.Errorf("WeiXin upload[%d]: %s", result.ErrorCode, result.ErrorMessage)
  1061. }
  1062. }
  1063. }
  1064. return "", errors.New("WeiXin upload media too many times")
  1065. }
  1066. func downloadMedia(c chan AccessToken, mediaID string, writer io.Writer) error {
  1067. reqURL := weixinFileURL + "/get?media_id=" + mediaID + "&access_token="
  1068. for i := 0; i < retryMaxN; i++ {
  1069. token := <-c
  1070. if time.Since(token.Expires).Seconds() < 0 {
  1071. r, err := http.Get(reqURL + token.Token)
  1072. if err != nil {
  1073. return err
  1074. }
  1075. defer r.Body.Close()
  1076. if r.Header.Get("Content-Type") != "text/plain" {
  1077. _, err = io.Copy(writer, r.Body)
  1078. return err
  1079. }
  1080. reply, err := ioutil.ReadAll(r.Body)
  1081. if err != nil {
  1082. return err
  1083. }
  1084. var result response
  1085. if err := json.Unmarshal(reply, &result); err != nil {
  1086. return err
  1087. }
  1088. switch result.ErrorCode {
  1089. case 0:
  1090. return nil
  1091. case 42001: // access_token timeout and retry
  1092. continue
  1093. default:
  1094. return fmt.Errorf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage)
  1095. }
  1096. }
  1097. }
  1098. return errors.New("WeiXin download media too many times")
  1099. }
  1100. // Format reply message header.
  1101. func (w responseWriter) replyHeader() string {
  1102. return fmt.Sprintf(replyHeader, w.toUserName, w.fromUserName, time.Now().Unix())
  1103. }
  1104. // Return weixin instance.
  1105. func (w responseWriter) GetWeixin() *Weixin {
  1106. return w.wx
  1107. }
  1108. // Return user data.
  1109. func (w responseWriter) GetUserData() interface{} {
  1110. return w.wx.userData
  1111. }
  1112. func (w responseWriter) replyMsg(msg string) {
  1113. w.writer.Write([]byte(msg))
  1114. }
  1115. // ReplyOK used to reply empty message.
  1116. func (w responseWriter) ReplyOK() {
  1117. w.replyMsg("success")
  1118. }
  1119. // ReplyText used to reply text message.
  1120. func (w responseWriter) ReplyText(text string) {
  1121. w.replyMsg(fmt.Sprintf(replyText, w.replyHeader(), text))
  1122. }
  1123. // ReplyImage used to reply image message.
  1124. func (w responseWriter) ReplyImage(mediaID string) {
  1125. w.replyMsg(fmt.Sprintf(replyImage, w.replyHeader(), mediaID))
  1126. }
  1127. // ReplyVoice used to reply voice message.
  1128. func (w responseWriter) ReplyVoice(mediaID string) {
  1129. w.replyMsg(fmt.Sprintf(replyVoice, w.replyHeader(), mediaID))
  1130. }
  1131. // ReplyVideo used to reply video message
  1132. func (w responseWriter) ReplyVideo(mediaID string, title string, description string) {
  1133. w.replyMsg(fmt.Sprintf(replyVideo, w.replyHeader(), mediaID, title, description))
  1134. }
  1135. // ReplyMusic used to reply music message
  1136. func (w responseWriter) ReplyMusic(m *Music) {
  1137. msg := fmt.Sprintf(replyMusic, w.replyHeader(), m.Title, m.Description, m.MusicUrl, m.HQMusicUrl, m.ThumbMediaId)
  1138. w.replyMsg(msg)
  1139. }
  1140. // ReplyNews used to reply news message (max 10 news)
  1141. func (w responseWriter) ReplyNews(articles []Article) {
  1142. var ctx string
  1143. for _, article := range articles {
  1144. ctx += fmt.Sprintf(replyArticle, article.Title, article.Description, article.PicUrl, article.Url)
  1145. }
  1146. msg := fmt.Sprintf(replyNews, w.replyHeader(), len(articles), ctx)
  1147. w.replyMsg(msg)
  1148. }
  1149. // TransferCustomerService used to tTransfer customer service
  1150. func (w responseWriter) TransferCustomerService(serviceID string) {
  1151. msg := fmt.Sprintf(transferCustomerService, serviceID, w.fromUserName, time.Now().Unix())
  1152. w.replyMsg(msg)
  1153. }
  1154. // PostText used to Post text message
  1155. func (w responseWriter) PostText(text string) error {
  1156. return w.wx.PostText(w.toUserName, text)
  1157. }
  1158. // Post image message
  1159. func (w responseWriter) PostImage(mediaID string) error {
  1160. return w.wx.PostImage(w.toUserName, mediaID)
  1161. }
  1162. // Post voice message
  1163. func (w responseWriter) PostVoice(mediaID string) error {
  1164. return w.wx.PostVoice(w.toUserName, mediaID)
  1165. }
  1166. // Post video message
  1167. func (w responseWriter) PostVideo(mediaID string, title string, desc string) error {
  1168. return w.wx.PostVideo(w.toUserName, mediaID, title, desc)
  1169. }
  1170. // Post music message
  1171. func (w responseWriter) PostMusic(music *Music) error {
  1172. return w.wx.PostMusic(w.toUserName, music)
  1173. }
  1174. // Post news message
  1175. func (w responseWriter) PostNews(articles []Article) error {
  1176. return w.wx.PostNews(w.toUserName, articles)
  1177. }
  1178. // Post template message
  1179. func (w responseWriter) PostTemplateMessage(templateid string, url string, data TmplData) (int32, error) {
  1180. return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
  1181. }
  1182. // Upload media from local file
  1183. func (w responseWriter) UploadMediaFromFile(mediaType string, filepath string) (string, error) {
  1184. return w.wx.UploadMediaFromFile(mediaType, filepath)
  1185. }
  1186. // Download media and save to local file
  1187. func (w responseWriter) DownloadMediaToFile(mediaID string, filepath string) error {
  1188. return w.wx.DownloadMediaToFile(mediaID, filepath)
  1189. }
  1190. // Upload media with reader
  1191. func (w responseWriter) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
  1192. return w.wx.UploadMedia(mediaType, filename, reader)
  1193. }
  1194. // Download media with writer
  1195. func (w responseWriter) DownloadMedia(mediaID string, writer io.Writer) error {
  1196. return w.wx.DownloadMedia(mediaID, writer)
  1197. }
  1198. /*************************************
  1199. 以下为自定义扩展
  1200. **************************************/
  1201. func (wx *Weixin) PostCustomMsg(url string, obj interface{}) (bs []byte, err error) {
  1202. var data []byte
  1203. data, err = json.Marshal(obj)
  1204. if err != nil {
  1205. return
  1206. }
  1207. bs, err = postRequest(url, wx.tokenChan, data)
  1208. return
  1209. }
  1210. //Post custom message(消息结构体完全由开发人员自定义)
  1211. func (wx *Weixin) PostTextCustom(url string, obj interface{}) error {
  1212. data, err := json.Marshal(obj)
  1213. if err != nil {
  1214. return err
  1215. }
  1216. log.Println("custom msg:", string(data))
  1217. _, err = postRequest(url, wx.tokenChan, data)
  1218. return err
  1219. }