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 = "%s"
replyImage = "%s"
replyVoice = "%s"
replyVideo = "%s"
replyMusic = "%s"
replyNews = "%s%d%s"
replyHeader = "%d"
replyArticle = "-
"
transferCustomerService = "" + replyHeader + ""
// 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
}