wecomchan/go-wecomchan/wecomchan.go

333 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math"
"mime/multipart"
"net/http"
"os"
"reflect"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/julienschmidt/httprouter"
)
/*------------------------------- 环境变量配置 begin -------------------------------*/
var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey")
var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID")
var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret")
var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID")
var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all")
var RedisStat = GetEnvDefault("REDIS_STAT", "OFF")
var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379")
var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "")
var ctx = context.Background()
/*------------------------------- 环境变量配置 end -------------------------------*/
/*------------------------------- 企业微信服务端API begin -------------------------------*/
var GetTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
var SendMessageApi = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s"
var UploadMediaApi = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
/*------------------------------- 企业微信服务端API end -------------------------------*/
const RedisTokenKey = "access_token"
type Msg struct {
Content string `json:"content"`
}
type Pic struct {
MediaId string `json:"media_id"`
}
type JsonData struct {
ToUser string `json:"touser"`
AgentId string `json:"agentid"`
MsgType string `json:"msgtype"`
DuplicateCheckInterval int `json:"duplicate_check_interval"`
Text Msg `json:"text"`
Image Pic `json:"image"`
}
// GetEnvDefault 获取配置信息,未获取到则取默认值
func GetEnvDefault(key, defVal string) string {
val, ex := os.LookupEnv(key)
if !ex {
return defVal
}
return val
}
// ParseJson 将json字符串解析为map
func ParseJson(jsonStr string) map[string]interface{} {
var wecomResponse map[string]interface{}
if string(jsonStr) != "" {
err := json.Unmarshal([]byte(string(jsonStr)), &wecomResponse)
if err != nil {
log.Println("生成json字符串错误")
}
}
return wecomResponse
}
// GetRemoteToken 从企业微信服务端API获取access_token存在redis服务则缓存
func GetRemoteToken(corpId, appSecret string) string {
getTokenUrl := fmt.Sprintf(GetTokenApi, corpId, appSecret)
log.Println("getTokenUrl==>", getTokenUrl)
resp, err := http.Get(getTokenUrl)
if err != nil {
log.Println(err)
}
defer resp.Body.Close()
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
}
tokenResponse := ParseJson(string(respData))
log.Println("企业微信获取access_token接口返回==>", tokenResponse)
accessToken := tokenResponse[RedisTokenKey].(string)
if RedisStat == "ON" {
log.Println("prepare to set redis key")
rdb := RedisClient()
// access_token有效时间为7200秒(2小时)
set, err := rdb.SetNX(ctx, RedisTokenKey, accessToken, 7000*time.Second).Result()
log.Println(set)
if err != nil {
log.Println(err)
}
}
return accessToken
}
// RedisClient redis客户端
func RedisClient() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: RedisAddr,
Password: RedisPassword, // no password set
DB: 0, // use default DB
})
return rdb
}
// PostMsg 推送消息
func PostMsg(postData JsonData, postUrl string) string {
postJson, _ := json.Marshal(postData)
log.Println("postJson ", string(postJson))
log.Println("postUrl ", postUrl)
msgReq, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(postJson))
if err != nil {
log.Println(err)
}
msgReq.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(msgReq)
if err != nil {
log.Fatalln("企业微信发送应用消息接口报错==>", err)
}
defer msgReq.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
mediaResp := ParseJson(string(body))
log.Println("企业微信发送应用消息接口返回==>", mediaResp)
return string(body)
}
// UploadMedia 上传临时素材并返回mediaId
func UploadMedia(msgType string, req *http.Request, accessToken string) (string, float64) {
// 企业微信图片上传不能大于2M
_ = req.ParseMultipartForm(2 << 20)
imgFile, imgHeader, err := req.FormFile("media")
log.Printf("文件大小==>%d字节", imgHeader.Size)
if err != nil {
log.Fatalln("图片文件出错==>", err)
// 自定义code无效的图片文件
return "", 400
}
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
if createFormFile, err := writer.CreateFormFile("media", imgHeader.Filename); err == nil {
readAll, _ := ioutil.ReadAll(imgFile)
createFormFile.Write(readAll)
}
writer.Close()
uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType)
log.Println("uploadMediaUrl==>", uploadMediaUrl)
newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf)
newRequest.Header.Set("Content-Type", writer.FormDataContentType())
log.Println("Content-Type ", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(newRequest)
respData, _ := ioutil.ReadAll(resp.Body)
mediaResp := ParseJson(string(respData))
log.Println("企业微信上传临时素材接口返回==>", mediaResp)
if err != nil {
log.Fatalln("上传临时素材出错==>", err)
return "", mediaResp["errcode"].(float64)
} else {
return mediaResp["media_id"].(string), float64(0)
}
}
// ValidateToken 判断accessToken是否失效
// true-未失效, false-失效需重新获取
func ValidateToken(errcode interface{}) bool {
codeTyp := reflect.TypeOf(errcode)
log.Println("errcode的数据类型==>", codeTyp)
if !codeTyp.Comparable() {
log.Printf("type is not comparable: %v", codeTyp)
return true
}
// 如果errcode为42001表明token已失效则清空redis中的token缓存
// 已知codeType为float64
if math.Abs(errcode.(float64)-float64(42001)) < 1e-3 {
if RedisStat == "ON" {
log.Printf("token已失效开始删除redis中的key==>%s", RedisTokenKey)
rdb := RedisClient()
rdb.Del(ctx, RedisTokenKey)
log.Printf("删除redis中的key==>%s完毕", RedisTokenKey)
}
log.Println("现需重新获取token")
return false
}
return true
}
// GetAccessToken 获取企业微信的access_token
func GetAccessToken() string {
accessToken := ""
if RedisStat == "ON" {
log.Println("尝试从redis获取token")
rdb := RedisClient()
value, err := rdb.Get(ctx, RedisTokenKey).Result()
if err == redis.Nil {
log.Println("access_token does not exist, need get it from remote API")
}
accessToken = value
}
if accessToken == "" {
log.Println("get access_token from remote API")
accessToken = GetRemoteToken(WecomCid, WecomSecret)
} else {
log.Println("get access_token from redis")
}
return accessToken
}
// InitJsonData 初始化Json公共部分数据
func InitJsonData(msgType string) JsonData {
return JsonData{
ToUser: WecomToUid,
AgentId: WecomAid,
MsgType: msgType,
DuplicateCheckInterval: 600,
}
}
// 主函数入口
func main() {
// 设置日志内容显示文件名和行号
log.SetFlags(log.LstdFlags | log.Lshortfile)
wecomChan := func(res http.ResponseWriter, req *http.Request, ps httprouter.Params) {
_ = req.ParseForm()
sendkey := req.Form.Get("sendkey")
urlpath := ps.ByName("urlpath")
if len(sendkey) == 0 && len(urlpath) > 6 && urlpath != "wecomchan" {
sendkey = urlpath[1 : len(urlpath)-5]
}
if sendkey != Sendkey {
log.Panicln("sendkey 错误,请检查")
}
msgContent := req.Form.Get("msg")
if len(msgContent) == 0 {
textArray := make([]string, 3)
lenOfArr := 0
title := req.Form.Get("title")
if (len(title)) > 0 {
textArray[lenOfArr] = title
lenOfArr++
}
text := req.Form.Get("text")
if (len(text)) > 0 {
textArray[lenOfArr] = text
lenOfArr++
}
desp := req.Form.Get("desp")
if (len(desp)) > 0 {
textArray[lenOfArr] = desp
lenOfArr++
}
msgContent = strings.Join(textArray[0:lenOfArr], "\n")
}
msgType := req.Form.Get("msg_type")
if len(msgType) == 0 {
msgType = "text"
}
log.Println("mes_type=", msgType)
// 默认mediaId为空
mediaId := ""
// 获取token
accessToken := GetAccessToken()
// 默认token有效
tokenValid := true
if msgType != "image" {
log.Println("消息类型不是图片")
} else {
// token有效则跳出循环继续执行否则重试3次
for i := 0; i <= 3; i++ {
var errcode float64
mediaId, errcode = UploadMedia(msgType, req, accessToken)
log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode)
tokenValid = ValidateToken(errcode)
if tokenValid {
break
}
accessToken = GetAccessToken()
}
}
// 准备发送应用消息所需参数
postData := InitJsonData(msgType)
postData.Text = Msg{
Content: msgContent,
}
postData.Image = Pic{
MediaId: mediaId,
}
postStatus := ""
for i := 0; i <= 3; i++ {
sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
postStatus = PostMsg(postData, sendMessageUrl)
postResponse := ParseJson(postStatus)
errcode := postResponse["errcode"]
log.Println("发送应用消息接口返回errcode==>", errcode)
tokenValid = ValidateToken(errcode)
// token有效则跳出循环继续执行否则重试3次
if tokenValid {
break
}
// 刷新token
accessToken = GetAccessToken()
}
res.Header().Set("Content-type", "application/json")
_, _ = res.Write([]byte(postStatus))
}
router := httprouter.New()
router.GET("/*urlpath", wecomChan)
router.POST("/*urlpath", wecomChan)
log.Fatal(http.ListenAndServe(":8080", router))
}