|
@@ -13,6 +13,7 @@ import (
|
|
|
"log"
|
|
|
"mime/multipart"
|
|
|
"net/http"
|
|
|
+ "net/url"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"regexp"
|
|
@@ -23,45 +24,71 @@ import (
|
|
|
|
|
|
const (
|
|
|
// Event type
|
|
|
- msgEvent = "event"
|
|
|
- EventSubscribe = "subscribe"
|
|
|
- EventUnsubscribe = "unsubscribe"
|
|
|
- EventScan = "scan"
|
|
|
- EventClick = "CLICK"
|
|
|
+ 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"
|
|
|
- MsgTypeLocation = "location"
|
|
|
- MsgTypeLink = "link"
|
|
|
- MsgTypeEvent = msgEvent + ".*"
|
|
|
- MsgTypeEventSubscribe = msgEvent + "\\." + EventSubscribe
|
|
|
- MsgTypeEventUnsubscribe = msgEvent + "\\." + EventUnsubscribe
|
|
|
- MsgTypeEventScan = msgEvent + "\\." + EventScan
|
|
|
- MsgTypeEventClick = msgEvent + "\\." + EventClick
|
|
|
+ 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"
|
|
|
- MenuButtonTypeMedia = "media_id"
|
|
|
- MenuButtonTypePicOrAlbum = "pic_photo_or_album"
|
|
|
+ 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"
|
|
|
+ // 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"
|
|
|
- weixinFileURL = "http://file.api.weixin.qq.com/cgi-bin/media"
|
|
|
- weixinUserInfo = "https://api.weixin.qq.com/cgi-bin/user/info"
|
|
|
+ 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"
|
|
|
+ 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>"
|
|
|
- reply2CustomerService = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></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>"
|
|
@@ -70,10 +97,11 @@ const (
|
|
|
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>"
|
|
|
+ reply2CustomerService = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
|
|
|
|
|
|
// QR scene request
|
|
|
- requestQRScene = "{\"expire_seconds\":%d,\"action_name\":\"QR_SCENE\",\"action_info\":{\"scene\":{\"scene_id\":%d}}}"
|
|
|
- requestQRLimitScene = "{\"action_name\":\"QR_LIMIT_SCENE\",\"action_info\":{\"scene\":{\"scene_id\":%d}}}"
|
|
|
+ requestQRScene = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
|
|
|
+ requestQRLimitScene = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
|
|
|
)
|
|
|
|
|
|
// Common message header
|
|
@@ -107,6 +135,7 @@ type Request struct {
|
|
|
Longitude float32
|
|
|
Precision float32
|
|
|
Recognition string
|
|
|
+ Status string
|
|
|
}
|
|
|
|
|
|
// Use to reply music message
|
|
@@ -142,10 +171,35 @@ type MenuButton struct {
|
|
|
Type string `json:"type,omitempty"`
|
|
|
Key string `json:"key,omitempty"`
|
|
|
Url string `json:"url,omitempty"`
|
|
|
- Mediaid int `json:"media_id"`
|
|
|
+ MediaId string `json:"media_id,omitempty"`
|
|
|
SubButtons []MenuButton `json:"sub_button,omitempty"`
|
|
|
}
|
|
|
|
|
|
+type UserAccessToken struct {
|
|
|
+ AccessToken string `json:"access_token"`
|
|
|
+ RefreshToken string `json:"refresh_token"`
|
|
|
+ ExpireSeconds int `json:"expires_in"`
|
|
|
+ OpenId string `json:"openid"`
|
|
|
+ Scope string `json:"scope"`
|
|
|
+ UnionId string `json:"unionid,omitempty"`
|
|
|
+}
|
|
|
+
|
|
|
+type UserInfo struct {
|
|
|
+ Subscribe int `json:"subscribe,omitempty"`
|
|
|
+ Language string `json:"language,omitempty"`
|
|
|
+ OpenId string `json:"openid,omitempty"`
|
|
|
+ UnionId string `json:"unionid,omitempty"`
|
|
|
+ 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"`
|
|
|
+ SubscribeTime int64 `json:"subscribe_time,omitempty"`
|
|
|
+ Remark string `json:"remark,omitempty"`
|
|
|
+ GroupId int `json:"groupid,omitempty"`
|
|
|
+}
|
|
|
+
|
|
|
// Use to output reply
|
|
|
type ResponseWriter interface {
|
|
|
// Get weixin
|
|
@@ -168,6 +222,7 @@ type ResponseWriter interface {
|
|
|
PostVideo(mediaId string, title string, description string) error
|
|
|
PostMusic(music *Music) error
|
|
|
PostNews(articles []Article) error
|
|
|
+ PostTemplateMessage(templateid string, url string, data interface{}) (string, error)
|
|
|
// Media operator
|
|
|
UploadMediaFromFile(mediaType string, filepath string) (string, error)
|
|
|
DownloadMediaToFile(mediaId string, filepath string) error
|
|
@@ -201,11 +256,19 @@ type accessToken struct {
|
|
|
expires time.Time
|
|
|
}
|
|
|
|
|
|
+type jsApiTicket struct {
|
|
|
+ ticket string
|
|
|
+ expires time.Time
|
|
|
+}
|
|
|
+
|
|
|
type Weixin struct {
|
|
|
- token string
|
|
|
- routes []*route
|
|
|
- tokenChan chan accessToken
|
|
|
- userData interface{}
|
|
|
+ token string
|
|
|
+ routes []*route
|
|
|
+ tokenChan chan accessToken
|
|
|
+ ticketChan chan jsApiTicket
|
|
|
+ userData interface{}
|
|
|
+ appId string
|
|
|
+ appSecret string
|
|
|
}
|
|
|
|
|
|
//
|
|
@@ -219,9 +282,13 @@ func (qr *QRScene) ToURL() string {
|
|
|
func New(token string, appid string, secret string) *Weixin {
|
|
|
wx := &Weixin{}
|
|
|
wx.token = token
|
|
|
+ wx.appId = appid
|
|
|
+ wx.appSecret = secret
|
|
|
if len(appid) > 0 && len(secret) > 0 {
|
|
|
wx.tokenChan = make(chan accessToken)
|
|
|
go createAccessToken(wx.tokenChan, appid, secret)
|
|
|
+ wx.ticketChan = make(chan jsApiTicket)
|
|
|
+ go createJsApiTicket(wx.tokenChan, wx.ticketChan)
|
|
|
}
|
|
|
return wx
|
|
|
}
|
|
@@ -232,6 +299,14 @@ func NewWithUserData(token string, appid string, secret string, userData interfa
|
|
|
return wx
|
|
|
}
|
|
|
|
|
|
+func (wx *Weixin) GetAppId() string {
|
|
|
+ return wx.appId
|
|
|
+}
|
|
|
+
|
|
|
+func (wx *Weixin) GetAppSecret() string {
|
|
|
+ return wx.appSecret
|
|
|
+}
|
|
|
+
|
|
|
// Register request callback.
|
|
|
func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
|
|
|
regex, err := regexp.Compile(pattern)
|
|
@@ -387,6 +462,21 @@ func (wx *Weixin) DownloadMedia(mediaId string, writer io.Writer) error {
|
|
|
return downloadMedia(wx.tokenChan, mediaId, writer)
|
|
|
}
|
|
|
|
|
|
+// Get ip list
|
|
|
+func (wx *Weixin) GetIpList() ([]string, error) {
|
|
|
+ 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
|
|
|
+}
|
|
|
+
|
|
|
// 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)))
|
|
@@ -413,6 +503,31 @@ func (wx *Weixin) CreateQRLimitScene(sceneId int) (*QRScene, error) {
|
|
|
return &qr, nil
|
|
|
}
|
|
|
|
|
|
+// 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
|
|
|
+}
|
|
|
+
|
|
|
// Custom menu
|
|
|
func (wx *Weixin) CreateMenu(menu *Menu) error {
|
|
|
data, err := json.Marshal(menu)
|
|
@@ -431,16 +546,14 @@ func (wx *Weixin) GetMenu() (*Menu, error) {
|
|
|
reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
- } else {
|
|
|
- var result struct {
|
|
|
- MenuCtx *Menu `json:"menu"`
|
|
|
- }
|
|
|
- if err := json.Unmarshal(reply, &result); err != nil {
|
|
|
- return nil, err
|
|
|
- } else {
|
|
|
- return result.MenuCtx, nil
|
|
|
- }
|
|
|
}
|
|
|
+ var result struct {
|
|
|
+ MenuCtx *Menu `json:"menu"`
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(reply, &result); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return result.MenuCtx, nil
|
|
|
}
|
|
|
|
|
|
func (wx *Weixin) DeleteMenu() error {
|
|
@@ -448,6 +561,129 @@ func (wx *Weixin) DeleteMenu() error {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
+// Template
|
|
|
+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
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+}
|
|
|
+
|
|
|
+func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data interface{}) (string, error) {
|
|
|
+ var msg struct {
|
|
|
+ ToUser string `json:"touser"`
|
|
|
+ TemplateId string `json:"template_id"`
|
|
|
+ Url string `json:"url,omitempty"`
|
|
|
+ Data interface{} `json:"data,omitempty"`
|
|
|
+ }
|
|
|
+ msg.ToUser = touser
|
|
|
+ msg.TemplateId = templateid
|
|
|
+ msg.Url = url
|
|
|
+ msg.Data = data
|
|
|
+ msgStr, err := marshal(msg)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ var resp struct {
|
|
|
+ MsgId string `json:"msgid,omitempty"`
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(reply, &resp); err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ return resp.MsgId, nil
|
|
|
+}
|
|
|
+
|
|
|
+// 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)
|
|
|
+}
|
|
|
+
|
|
|
+// 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
|
|
|
+}
|
|
|
+
|
|
|
+// 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
|
|
|
+}
|
|
|
+
|
|
|
+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")
|
|
|
+}
|
|
|
+
|
|
|
+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",
|
|
|
+ ticket, noncestr, timestamp, url)))
|
|
|
+ return fmt.Sprintf("%x", h.Sum(nil)), nil
|
|
|
+}
|
|
|
+
|
|
|
// Create handler func
|
|
|
func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
@@ -505,6 +741,16 @@ func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
|
|
|
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 checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
|
|
|
r.ParseForm()
|
|
|
var signature string = r.FormValue("signature")
|
|
@@ -547,6 +793,25 @@ func authAccessToken(appid string, secret string) (string, time.Duration) {
|
|
|
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 createAccessToken(c chan accessToken, appid string, secret string) {
|
|
|
token := accessToken{"", time.Now()}
|
|
|
c <- token
|
|
@@ -560,6 +825,20 @@ func createAccessToken(c chan accessToken, appid string, secret string) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+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
|
|
@@ -698,24 +977,22 @@ func downloadMedia(c chan accessToken, mediaId string, writer io.Writer) error {
|
|
|
if r.Header.Get("Content-Type") != "text/plain" {
|
|
|
_, err := io.Copy(writer, r.Body)
|
|
|
return err
|
|
|
- } else {
|
|
|
- reply, err := ioutil.ReadAll(r.Body)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- var result response
|
|
|
- if err := json.Unmarshal(reply, &result); err != nil {
|
|
|
- return err
|
|
|
- } else {
|
|
|
- switch result.ErrorCode {
|
|
|
- case 0:
|
|
|
- return nil
|
|
|
- case 42001: // access_token timeout and retry
|
|
|
- continue
|
|
|
- default:
|
|
|
- return errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
|
|
|
- }
|
|
|
- }
|
|
|
+ }
|
|
|
+ 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 errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -823,6 +1100,11 @@ 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 interface{}) (string, 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)
|