From 686071002a21451a940fa9c9912df0ffdfa8b487 Mon Sep 17 00:00:00 2001 From: wzj <244142824@qq.com> Date: Sat, 27 Nov 2021 17:04:57 +0800 Subject: [PATCH] first commit --- .gitignore | 6 + 20210208142819.png | Bin 0 -> 50788 bytes LICENSE | 21 ++ ONLINE.md | 13 ++ README.md | 328 +++++++++++++++++++++++++++ dotNetCore.cs | 58 +++++ go-scf/.gitignore | 9 + go-scf/README.md | 137 +++++++++++ go-scf/build.sh | 5 + go-scf/consts/consts.go | 18 ++ go-scf/dal/dal.go | 50 ++++ go-scf/go.mod | 8 + go-scf/go.sum | 17 ++ go-scf/main.go | 52 +++++ go-scf/model/model.go | 27 +++ go-scf/service/wecomchan.go | 90 ++++++++ go-scf/utils/utils.go | 30 +++ go-wecomchan/Dockerfile | 26 +++ go-wecomchan/Dockerfile.architecture | 24 ++ go-wecomchan/README.md | 121 ++++++++++ go-wecomchan/docker-compose.yml | 37 +++ go-wecomchan/go.mod | 5 + go-wecomchan/go.sum | 97 ++++++++ go-wecomchan/wecomchan.go | 302 ++++++++++++++++++++++++ index.php | 87 +++++++ 25 files changed, 1568 insertions(+) create mode 100644 .gitignore create mode 100644 20210208142819.png create mode 100644 LICENSE create mode 100644 ONLINE.md create mode 100644 README.md create mode 100644 dotNetCore.cs create mode 100644 go-scf/.gitignore create mode 100644 go-scf/README.md create mode 100755 go-scf/build.sh create mode 100644 go-scf/consts/consts.go create mode 100644 go-scf/dal/dal.go create mode 100644 go-scf/go.mod create mode 100644 go-scf/go.sum create mode 100644 go-scf/main.go create mode 100644 go-scf/model/model.go create mode 100644 go-scf/service/wecomchan.go create mode 100644 go-scf/utils/utils.go create mode 100644 go-wecomchan/Dockerfile create mode 100644 go-wecomchan/Dockerfile.architecture create mode 100644 go-wecomchan/README.md create mode 100644 go-wecomchan/docker-compose.yml create mode 100644 go-wecomchan/go.mod create mode 100644 go-wecomchan/go.sum create mode 100644 go-wecomchan/wecomchan.go create mode 100644 index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a46a259 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +demo.php +show.php +.DS_Store +.idea +go-wecomchan/wecomchan +go-wecomchan/wecomchan.exe \ No newline at end of file diff --git a/20210208142819.png b/20210208142819.png new file mode 100644 index 0000000000000000000000000000000000000000..f699765ee582115eb46d02db7cf97e9b4b8cad8d GIT binary patch literal 50788 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 0) { + $access_token = $info['access_token']; + $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); + $data = new \stdClass(); + $data->touser = $wecom_touid; + $data->agentid = $wecom_aid; + $data->msgtype = "text"; + $data->text = ["content"=> $text]; + $data->duplicate_check_interval = 600; + + $data_json = json_encode($data); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + return $response; + } + return false; +} + +``` + +使用实例: + +```php +$ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); +print_r( $ret ); +``` + +PYTHON版: + +```python +import json,requests,base64 +def send_to_wecom(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"text", + "text":{ + "content":text + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False + +def send_to_wecom_image(base64_content,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' + upload_response = requests.post(upload_url, files={ + "picture": base64.b64decode(base64_content) + }).json() + if "media_id" in upload_response: + media_id = upload_response['media_id'] + else: + return False + + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"image", + "image":{ + "media_id": media_id + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False + +def send_to_wecom_markdown(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'): + get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" + response = requests.get(get_token_url).content + access_token = json.loads(response).get('access_token') + if access_token and len(access_token) > 0: + send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' + data = { + "touser":wecom_touid, + "agentid":wecom_aid, + "msgtype":"markdown", + "markdown":{ + "content":text + }, + "duplicate_check_interval":600 + } + response = requests.post(send_msg_url,data=json.dumps(data)).content + return response + else: + return False +``` + +使用实例: + +```python +ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom('文本中支持超链接', "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom_image("此处填写图片Base64", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +ret = send_to_wecom_markdown("**Markdown 内容**", "企业ID③", "应用ID①", "应用secret②"); +print( ret ); +``` + +TypeScript 版: + +```typescript +import request from 'superagent' + +async function sendToWecom(body: { + text: string + wecomCId: string + wecomSecret: string + wecomAgentId: string + wecomTouid?: string +}): Promise<{ errcode: number; errmsg: string; invaliduser: string }> { + body.wecomTouid = body.wecomTouid ?? '@all' + const getTokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${body.wecomCId}&corpsecret=${body.wecomSecret}` + const getTokenRes = await request.get(getTokenUrl) + const accessToken = getTokenRes.body.access_token + if (accessToken?.length <= 0) { + throw new Error('获取 accessToken 失败') + } + const sendMsgUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}` + const sendMsgRes = await request.post(sendMsgUrl).send({ + touser: body.wecomTouid, + agentid: body.wecomAgentId, + msgtype: 'text', + text: { + content: body.text, + }, + duplicate_check_interval: 600, + }) + return sendMsgRes.body +} +``` + +使用实例: + +```typescript +sendToWecom({ + text: '推送测试\r\n测试换行', + wecomAgentId: '应用ID①', + wecomSecret: '应用secret②', + wecomCId: '企业ID③', +}) + .then((res) => { + console.log(res) + }) + .catch((err) => { + console.log(err) + }) +``` + +.NET Core 版: + +```C# +using System; +using RestSharp; +using Newtonsoft.Json; +namespace WeCom.Demo +{ + class WeCom + { + public string SendToWeCom( + string text,// 推送消息 + string weComCId,// 企业Id① + string weComSecret,// 应用secret② + string weComAId,// 应用ID③ + string weComTouId = "@all") + { + // 获取Token + string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; + string token = JsonConvert + .DeserializeObject(new RestClient(getTokenUrl) + .Get(new RestRequest()).Content).access_token; + System.Console.WriteLine(token); + if (!String.IsNullOrWhiteSpace(token)) + { + var request = new RestRequest(); + var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); + var data = new + { + touser = weComTouId, + agentid = weComAId, + msgtype = "text", + text = new + { + content = text + }, + duplicate_check_interval = 600 + }; + string serJson = JsonConvert.SerializeObject(data); + System.Console.WriteLine(serJson); + request.Method = Method.POST; + request.AddHeader("Accept", "application/json"); + request.Parameters.Clear(); + request.AddParameter("application/json", serJson, ParameterType.RequestBody); + return client.Execute(request).Content; + } + return "-1"; + } +} + + +``` +使用实例: +```C# + static void Main(string[] args) + { // 测试 + Console.Write(new WeCom().SendToWeCom( + "msginfo", + "企业Id①" + , "应用secret②", + "应用ID③" + )); + } + + } +``` + +其他版本的函数可参照上边的逻辑自行编写,欢迎PR。 + +发送图片、卡片、文件或 Markdown 消息的高级用法见 [企业微信API](https://work.weixin.qq.com/api/doc/90000/90135/90236)。 + + + diff --git a/dotNetCore.cs b/dotNetCore.cs new file mode 100644 index 0000000..bd748ad --- /dev/null +++ b/dotNetCore.cs @@ -0,0 +1,58 @@ +using System; +using RestSharp; +using Newtonsoft.Json; +namespace WeCom.Demo +{ + class WeCom + { + public string SendToWeCom( + string text,// 推送消息 + string weComCId,// 企业Id① + string weComSecret,// 应用secret② + string weComAId,// 应用ID③ + string weComTouId = "@all") + { + // 获取Token + string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}"; + string token = JsonConvert + .DeserializeObject(new RestClient(getTokenUrl) + .Get(new RestRequest()).Content).access_token; + System.Console.WriteLine(token); + if (!String.IsNullOrWhiteSpace(token)) + { + var request = new RestRequest(); + var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"); + var data = new + { + touser = weComTouId, + agentid = weComAId, + msgtype = "text", + text = new + { + content = text + }, + duplicate_check_interval = 600 + }; + string serJson = JsonConvert.SerializeObject(data); + System.Console.WriteLine(serJson); + request.Method = Method.POST; + request.AddHeader("Accept", "application/json"); + request.Parameters.Clear(); + request.AddParameter("application/json", serJson, ParameterType.RequestBody); + return client.Execute(request).Content; + } + return "-1"; + } + static void Main(string[] args) + { // 测试 + Console.Write(new WeCom().SendToWeCom( + "msginfo", + "企业Id①" + , "应用secret②", + "应用ID③" + )); + } + + } +} + diff --git a/go-scf/.gitignore b/go-scf/.gitignore new file mode 100644 index 0000000..0d84dba --- /dev/null +++ b/go-scf/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +main +msg_notice +*.exe +*.zip +*_test.go +*.zip +.DS_Store +config.yaml \ No newline at end of file diff --git a/go-scf/README.md b/go-scf/README.md new file mode 100644 index 0000000..600d863 --- /dev/null +++ b/go-scf/README.md @@ -0,0 +1,137 @@ +# 腾讯云云函数部署Server酱📣 + +本项目是对 [Wecom酱](https://github.com/easychen/wecomchan) 进行的扩展,可以通过企业微信 OpenAPI 向微信推送消息,实现微信消息提醒。 + +利用 [腾讯云云函数](https://cloud.tencent.com/product/scf) ServerLess 的能力,以极低的费用(按量付费,且有大量免费额度)来完成部署 + +优点: + +- 便宜:说是免费也不过分 +- 简单:不需要购买vps, 也不需要备案, 腾讯云速度有保障. +- 易搭建:一个可执行二进制文件,直接上传至腾讯云函数控制面板即可,虽然使用 Golang 编写,但是搭建无需 Golang 环境 +- Serverless:无服务器,函数调用完资源会释放 + +## 🖐️ 简单介绍 + +我们要实现的目标是把消息推送到微信上,此处借助了使用 企业微信,可以创建机器人,利用微信的 OpenAPI 来实现消息推送,本项目做了一个简单的封装。 + +欢迎PR代码。 + +> 老用户注意: +> +> 自 2.0 版本之后,不再需要 `config.yaml` 文件,配置改为从云函数的环境变量中读取,请直接下载 `main.zip` 上传至云函数并且设置环境变量即可。 + +## 👋 使用方法 + +### 1. 注册企业 & 创建机器人 & 获取相关配置信息 + +此处不再赘述,项目主页有完整的操作方法,见:https://github.com/riba2534/wecomchan + +### 2. 下载编译好的二进制文件 + +下载文件 [版本发布页面](https://github.com/riba2534/wecomchan/releases): + +- [main.zip](https://github.com/riba2534/wecomchan/releases/download/2.1/main.zip) :云函数可执行二进制文件,不用改动,等会直接上传即可。 + +### 3. 在腾讯云中创建云函数 & 配置环境变量 + +打开云函数控制台:https://console.cloud.tencent.com/scf/list + +点击新建: + +![image-20210705014652334](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705014652334.png) + +如图所示选择 + +1. 自定义创建,函数类型为 `事件函数` +2. 填 `wecomchan` +3. 运行环境选择 Go1 +4. 函数代码选择本地上传ZIP包,直接上传刚才下载的 `main.zip` +5. 在 `高级配置` 中配置环境变量,6 个环境变量,**缺一不可**,(后续想改环境变量,直接在创建好的函数中编辑即可) + +环境变量配置说明 + +| key | value | 备注 | +| :------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FUNC_NAME` | 填 `wecomchan` | | +| `SEND_KEY` | 最终调用HTTP接口时校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | | +| `WECOM_CID` | 企业微信公司ID | | +| `WECOM_SECRET` | 企业微信应用Secret | | +| `WECOM_AID` | 企业微信应用ID | | +| `WECOM_TOUID` | `@all` | 此处指推送消息的默认发送对象,填 `@all`,则代表向该企业的全部成员推送消息(如果是个人用的话,一个企业中只有你自己,直接填 `@all` 即可),如果想指定具体发送的人,后面会说明怎么发。 | + +6. 在 `触发器配置` 中,新增 `API网关触发`,保持默认配置即可。 +7. 点击完成 + +![基础配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204518173.png) + +![高级配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707204936310.png) + +![触发器配置](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707205811630.png) + +稍等一会,进入你创建的函数: + +![image-20210705015301810](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015301810.png) + +图中所示的访问路径就是函数的请求路径,至此,所有的配置完成。 + +## 👌 发起HTTP请求测试是否成功 + +现已支持 `GET`、`POST` 方法进行请求。 + +> 当发送的文本中存在有换行符或其他字符时,请把 msg 参数进行 url 编码(使用 GET 方法注意,POST不需要) + +### 简单使用: + +在你刚才获得的路径之后拼几个GET参数,在后面加上:`?sendkey=你配置的sendkey&msg_type=text&msg=hello` + +![image-20210705015727720](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015727720.png) + +可以看见返回 success 字样。 + +观察手机推送,也可以收到消息: + +![image-20210705015804023](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210705015804023.png) + +之后,想怎么用就是你的事了,想给自己的微信推送,只需要给这个 URL 发一条 HTTP 请求即可。 + +### 给指定成员推送消息: + +如果你的需求是给企业微信中的指定成员发送消息而不是所有成员,则在 GET 请求中多加一个参数 `to_user`,值为 成员ID列表,如果想指定多个成员,则多个成员ID之间用 `|` 隔开。如请求:`https://xxxxx/wecomchan?sendkey=123456&msg_type=text&msg=测试消息&to_user=User1|User2` ,也能收到消息。 + +![image-20210707211125345](https://image-1252109614.cos.ap-beijing.myqcloud.com/img/image-20210707211125345.png) + +> 成员的 ID 在企业微信后台,`通讯录`,点开指定成员资料,有个 `账号` 字段,该字段即为该成员的ID. + +### 使用 `POST` 进行请求 + +大部分情况下,`GET` 请求已经可以很好的满足发送一些短消息的需求,但是当消息体过长时,云函数可能报参数过长错误,故在 `V2.1` 版本加入 `POST` 请求支持。 + +与 `GET` 请求不同的是,`POST` 请求不从 [Query string](https://en.wikipedia.org/wiki/Query_string) 获取参数,所有参数改为从 [HTTP message body](https://en.wikipedia.org/wiki/HTTP_message_body) 中获取,这里要求 Body 中必须是 `JSON` 格式,参数字段名称仍与 `GET` 请求的名称保持一致,且 `json` 的 `key` 和 `value` 必须是 `string` 类型,Body 格式例如: + +```json +{ + "sendkey": "123456", + "msg_type": "text", + "msg": "这是一条POST消息", + "to_user": "User1|User2" +} +``` + +### 参数说明: + +下表为请求的参数说明(`GET` 与 `POST` 字段名相同): + +| 参数名称 | 说明 | 是否可选 | +| ---------- | --------------------------------------------------------------------------------------------------------------- | -------- | +| `sendkey` | 校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | 必须 | +| `msg_type` | 消息类型,目前只有纯文本一种类型,值为 `text` | 必须 | +| `msg` | 消息内容,支持多行和UTF8字符,在程序中构建字符串时加上**换行符**即可,如果有特殊符号,记得使用 `urlencode` 编码 | 必须 | +| `to_user` | 如果需要给企业内指定成员发消息,可在此参数中指定成员。如果不传本参数,默认所有成员。 | 可选 | + +👇👇👇 + +--- + +如果发现bug,或者对本项目有任何建议,欢迎联系 `riba2534@qq.com` 或者直接提 [Issue](https://github.com/riba2534/wecomchan/issues). + diff --git a/go-scf/build.sh b/go-scf/build.sh new file mode 100755 index 0000000..d18a659 --- /dev/null +++ b/go-scf/build.sh @@ -0,0 +1,5 @@ +set -ex + +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main && upx -9 main + +zip main.zip main \ No newline at end of file diff --git a/go-scf/consts/consts.go b/go-scf/consts/consts.go new file mode 100644 index 0000000..2376f16 --- /dev/null +++ b/go-scf/consts/consts.go @@ -0,0 +1,18 @@ +package consts + +var ( + FUNC_NAME string + SEND_KEY string + WECOM_CID string + WECOM_SECRET string + WECOM_AID string + WECOM_TOUID string +) + +// 微信发消息API +const ( + // https://work.weixin.qq.com/api/doc/90000/90135/90236 + WeComMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" + // https://work.weixin.qq.com/api/doc/90000/90135/91039 + WeComAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" +) diff --git a/go-scf/dal/dal.go b/go-scf/dal/dal.go new file mode 100644 index 0000000..ff86f3e --- /dev/null +++ b/go-scf/dal/dal.go @@ -0,0 +1,50 @@ +package dal + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/model" +) + +var AccessToken string + +func loadAccessToken() { + client := http.Client{Timeout: 10 * time.Second} + req, _ := http.NewRequest("GET", fmt.Sprintf(consts.WeComAccessTokenURL, consts.WECOM_CID, consts.WECOM_SECRET), nil) + resp, err := client.Do(req) + if err != nil { + fmt.Println("getAccessToken err=", err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Println("getAccessToken statusCode is not 200") + } + respBodyBytes, _ := ioutil.ReadAll(resp.Body) + assesTokenResp := &model.AssesTokenResp{} + if err := jsoniter.Unmarshal(respBodyBytes, assesTokenResp); err != nil { + fmt.Println("getAccessToken json Unmarshal failed, err=", err) + panic(err) + } + if assesTokenResp.Errcode != 0 { + fmt.Println("getAccessToken assesTokenResp.Errcode != 0, err=", assesTokenResp.Errmsg) + panic(err) + } + AccessToken = assesTokenResp.AccessToken +} + +func Init() { + loadAccessToken() + fmt.Printf("[Init] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) + go func() { + for { + time.Sleep(30 * time.Minute) + loadAccessToken() + fmt.Printf("[Goroutine] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken) + } + }() +} diff --git a/go-scf/go.mod b/go-scf/go.mod new file mode 100644 index 0000000..80bed7d --- /dev/null +++ b/go-scf/go.mod @@ -0,0 +1,8 @@ +module github.com/riba2534/wecomchan/go-scf + +go 1.16 + +require ( + github.com/json-iterator/go v1.1.11 + github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 +) diff --git a/go-scf/go.sum b/go-scf/go.sum new file mode 100644 index 0000000..e6e7262 --- /dev/null +++ b/go-scf/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 h1:JdeXp/XPi7lBmpQNSUxElMAvwppMlFSiamTtXYRFuUc= +github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= diff --git a/go-scf/main.go b/go-scf/main.go new file mode 100644 index 0000000..5bfad7d --- /dev/null +++ b/go-scf/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/dal" + "github.com/riba2534/wecomchan/go-scf/service" + "github.com/riba2534/wecomchan/go-scf/utils" + "github.com/tencentyun/scf-go-lib/cloudfunction" + "github.com/tencentyun/scf-go-lib/events" +) + +func init() { + consts.FUNC_NAME = utils.GetEnvDefault("FUNC_NAME", "") + consts.SEND_KEY = utils.GetEnvDefault("SEND_KEY", "") + consts.WECOM_CID = utils.GetEnvDefault("WECOM_CID", "") + consts.WECOM_SECRET = utils.GetEnvDefault("WECOM_SECRET", "") + consts.WECOM_AID = utils.GetEnvDefault("WECOM_AID", "") + consts.WECOM_TOUID = utils.GetEnvDefault("WECOM_TOUID", "@all") + if consts.FUNC_NAME == "" || consts.SEND_KEY == "" || consts.WECOM_CID == "" || + consts.WECOM_SECRET == "" || consts.WECOM_AID == "" || consts.WECOM_TOUID == "" { + fmt.Printf("os.env load Fail, please check your os env.\nFUNC_NAME=%s\nSEND_KEY=%s\nWECOM_CID=%s\nWECOM_SECRET=%s\nWECOM_AID=%s\nWECOM_TOUID=%s\n", consts.FUNC_NAME, consts.SEND_KEY, consts.WECOM_CID, consts.WECOM_SECRET, consts.WECOM_AID, consts.WECOM_TOUID) + panic("os.env param error") + } + fmt.Println("os.env load success!") +} + +func HTTPHandler(ctx context.Context, event events.APIGatewayRequest) (events.APIGatewayResponse, error) { + path := event.Path + fmt.Println("req->", utils.MarshalToStringParam(event)) + var result interface{} + if strings.HasPrefix(path, "/"+consts.FUNC_NAME) { + result = service.WeComChanService(ctx, event) + } else { + // 匹配失败返回原始HTTP请求 + result = event + } + return events.APIGatewayResponse{ + IsBase64Encoded: false, + StatusCode: 200, + Headers: map[string]string{}, + Body: utils.MarshalToStringParam(result), + }, nil +} + +func main() { + dal.Init() + cloudfunction.Start(HTTPHandler) +} diff --git a/go-scf/model/model.go b/go-scf/model/model.go new file mode 100644 index 0000000..1a470b7 --- /dev/null +++ b/go-scf/model/model.go @@ -0,0 +1,27 @@ +package model + +type AssesTokenResp struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} + +type MsgText struct { + Content string `json:"content"` +} + +// https://work.weixin.qq.com/api/doc/90002/90151/90854 +type WechatMsg struct { + ToUser string `json:"touser"` + AgentId string `json:"agentid"` + MsgType string `json:"msgtype"` + Text *MsgText `json:"text"` + DuplicateCheckInterval int `json:"duplicate_check_interval"` +} + +type PostResp struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Invaliduser string `json:"invaliduser"` +} diff --git a/go-scf/service/wecomchan.go b/go-scf/service/wecomchan.go new file mode 100644 index 0000000..6557dba --- /dev/null +++ b/go-scf/service/wecomchan.go @@ -0,0 +1,90 @@ +package service + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/riba2534/wecomchan/go-scf/consts" + "github.com/riba2534/wecomchan/go-scf/dal" + "github.com/riba2534/wecomchan/go-scf/model" + "github.com/riba2534/wecomchan/go-scf/utils" + "github.com/tencentyun/scf-go-lib/events" +) + +func WeComChanService(ctx context.Context, event events.APIGatewayRequest) map[string]interface{} { + sendKey := getQuery("sendkey", event) + msgType := getQuery("msg_type", event) + msg := getQuery("msg", event) + if msgType == "" || msg == "" { + return utils.MakeResp(-1, "param error") + } + if sendKey != consts.SEND_KEY { + return utils.MakeResp(-1, "sendkey error") + } + toUser := getQuery("to_user", event) + if toUser == "" { + toUser = consts.WECOM_TOUID + } + if err := postWechatMsg(dal.AccessToken, msg, msgType, toUser); err != nil { + return utils.MakeResp(0, err.Error()) + } + return utils.MakeResp(0, "success") +} + +func postWechatMsg(accessToken, msg, msgType, toUser string) error { + content := &model.WechatMsg{ + ToUser: toUser, + AgentId: consts.WECOM_AID, + MsgType: msgType, + DuplicateCheckInterval: 600, + Text: &model.MsgText{ + Content: msg, + }, + } + b, _ := jsoniter.Marshal(content) + client := http.Client{Timeout: 10 * time.Second} + req, _ := http.NewRequest("POST", fmt.Sprintf(consts.WeComMsgSendURL, accessToken), bytes.NewBuffer(b)) + req.Header.Set("Content-type", "application/json") + resp, err := client.Do(req) + if err != nil { + fmt.Println("[postWechatMsg] failed, err=", err) + return nil + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + fmt.Println("postWechatMsg statusCode is not 200") + return errors.New("statusCode is not 200") + } + respBodyBytes, _ := ioutil.ReadAll(resp.Body) + postResp := &model.PostResp{} + if err := jsoniter.Unmarshal(respBodyBytes, postResp); err != nil { + fmt.Println("postWechatMsg json Unmarshal failed, err=", err) + return err + } + if postResp.Errcode != 0 { + fmt.Println("postWechatMsg postResp.Errcode != 0, err=", postResp.Errmsg) + return errors.New(postResp.Errmsg) + } + return nil +} + +func getQuery(key string, event events.APIGatewayRequest) string { + switch event.Method { + case "GET": + value := event.QueryString[key] + if len(value) > 0 && value[0] != "" { + return value[0] + } + return "" + case "POST": + return jsoniter.Get([]byte(event.Body), key).ToString() + default: + return "" + } +} diff --git a/go-scf/utils/utils.go b/go-scf/utils/utils.go new file mode 100644 index 0000000..d802415 --- /dev/null +++ b/go-scf/utils/utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "os" + + jsoniter "github.com/json-iterator/go" +) + +func MarshalToStringParam(param interface{}) string { + s, err := jsoniter.MarshalToString(param) + if err != nil { + return "{}" + } + return s +} + +func MakeResp(code int, msg string) map[string]interface{} { + return map[string]interface{}{ + "code": code, + "msg": msg, + } +} + +func GetEnvDefault(key, defVal string) string { + val, ex := os.LookupEnv(key) + if !ex { + return defVal + } + return val +} diff --git a/go-wecomchan/Dockerfile b/go-wecomchan/Dockerfile new file mode 100644 index 0000000..beb15eb --- /dev/null +++ b/go-wecomchan/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.16.5-alpine3.13 as gobuilder + +# 替换为国内源 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories + +ENV GO111MODULE="on" +ENV GOPROXY="https://goproxy.cn,direct" +ENV CGO_ENABLED=0 + +WORKDIR /go/src/app +COPY . . + +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +RUN update-ca-certificates +RUN go build + +FROM scratch + +WORKDIR /root + +COPY --from=gobuilder /go/src/app/wecomchan . +COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 8080 + +CMD ["./wecomchan"] diff --git a/go-wecomchan/Dockerfile.architecture b/go-wecomchan/Dockerfile.architecture new file mode 100644 index 0000000..514de5e --- /dev/null +++ b/go-wecomchan/Dockerfile.architecture @@ -0,0 +1,24 @@ +FROM --platform=$TARGETPLATFORM golang:1.16.5-alpine3.13 as gobuilder + +ENV GO111MODULE="on" +ENV GOPROXY="https://goproxy.cn,direct" +ENV CGO_ENABLED=0 + +WORKDIR /go/src/app +COPY . . + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +RUN update-ca-certificates +RUN go build + +FROM scratch + +WORKDIR /root + +COPY --from=gobuilder /go/src/app/wecomchan . +COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 8080 + +CMD ["./wecomchan"] diff --git a/go-wecomchan/README.md b/go-wecomchan/README.md new file mode 100644 index 0000000..1a8b47f --- /dev/null +++ b/go-wecomchan/README.md @@ -0,0 +1,121 @@ +# go-wecomchan + +## what's new + +添加 Dockerfile.architecture 使用docker buildx支持构建多架构镜像。 + +关于docker buildx build 使用方式参考官方文档: + +[https://docs.docker.com/engine/reference/commandline/buildx_build/](https://docs.docker.com/engine/reference/commandline/buildx_build/) + +## 配置说明 + +直接使用和构建二进制文件使用需要golang环境,并且网络可以安装依赖。 +docker构建镜像使用,需要安装docker,不依赖golang以及网络。 + +## 修改默认值 + +修改的sendkey,企业微信公司ID 等默认值为你的企业中的相关信息,如不设置运行时和打包后都可通过环境变量传入。 + +```golang +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", "") +``` + +## 直接使用 + +如果没有添加默认值,需要先引入环境变量,以SENDKEY为例: + +`export SENDKEY=set_a_sendkey` +依次引入环境变量后,执行 +`go run .` + +## build命令构建二进制文件使用 + +1. 构建命令 +`go build` + +2. 启动 +`./wecomchan` + +## 构建docker镜像使用(推荐,不依赖golang,不依赖网络) + +新增打包好的镜像可以直接使用 + +- 推送文本or图片:`docker pull aozakiaoko/go-wecomchan` +Docker Hub 地址为:[https://hub.docker.com/r/aozakiaoko/go-wecomchan](https://hub.docker.com/r/aozakiaoko/go-wecomchan) + +已经更新latest镜像为 @fcbhank 的最新代码,并支持arm64设备。也可通过aozakiaoko/go-wecomchan:v2 获取最新镜像。 + +- v2_推送文本or图片:`docker pull fcbhank/go-wecomchan` +Docker Hub 地址为:[https://hub.docker.com/r/fcbhank/go-wecomchan](https://hub.docker.com/r/fcbhank/go-wecomchan) + +1. 构建镜像 +`docker build -t go-wecomchan .` + +2. 修改默认值后启动镜像 +`docker run -dit -p 8080:8080 go-wecomchan` + +3. 通过环境变量启动镜像并启用redis + +```bash +docker run -dit -e SENDKEY=set_a_sendkey \ +-e WECOM_CID=企业微信公司ID \ +-e WECOM_SECRET=企业微信应用Secret \ +-e WECOM_AID=企业微信应用ID \ +-e WECOM_TOUID="@all" \ +-e REDIS_STAT=ON \ +-e REDIS_ADDR="localhost:6379" \ +-e REDIS_PASSWORD="" \ +# aozakiaoko/go-wecomchan 已经更新镜像为 @fcbhank 的最新代码,并支持arm64设备。 +# v2 fcbhank/go-wecomchan +-p 8080:8080 go-wecomchan +``` + +如不使用redis不要传入最后三个关于redis的环境变量(REDIS_STAT|REDIS_ADDR|REDIS_PASSWORD) + +4. 环境变量说明 + +|名称|描述| +|---|---| +|SENDKEY|发送时用来验证的key| +|WECOM_CID|企业微信公司ID| +|WECOM_SECRET|企业微信应用Secret| +|WECOM_AID|企业微信应用ID| +|WECOM_TOUID|需要发送给的人,详见[企业微信官方文档](https://work.weixin.qq.com/api/doc/90000/90135/90236#%E6%96%87%E6%9C%AC%E6%B6%88%E6%81%AF)| +|REDIS_STAT|是否启用redis换缓存token,ON-启用 OFF或空-不启用| +|REDIS_ADDR|redis服务器地址,如不启用redis缓存可不设置| +|REDIS_PASSWORD|redis的连接密码,如不启用redis缓存可不设置| + +## 使用docker-compose 部署 + +修改docker-compose.yml 文件内上述的环境变量,之后执行 + +`docker-compose up -d` + +## 调用方式 +- v1_推送文本 +访问 `http://localhost:8080/wecomchan?sendkey=你配置的sendkey&&msg=需要发送的消息&&msg_type=text` + +- v2_推送文本or图片 + +```bash +# 推送文本消息 +curl --location --request GET 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg={你的文本消息}&msg_type=text' + +# 推送图片消息 +curl --location --request POST 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg_type=image' \ +--form 'media=@"test.jpg"' +``` + +## 后续预计添加 + +* [x] Dockerfile 打包镜像(不依赖网络环境) +* [x] 通过环境变量传递企业微信id,secret等,镜像一次构建多次使用 +* [x] docker-compose redis + go-wecomchan 一键部署 \ No newline at end of file diff --git a/go-wecomchan/docker-compose.yml b/go-wecomchan/docker-compose.yml new file mode 100644 index 0000000..fa558f8 --- /dev/null +++ b/go-wecomchan/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' + +services: + go-wecomchan: + image: docker.io/aozakiaoko/go-wecomchan:latest + environment: + - SENDKEY=发送时用来验证的key + - WECOM_CID=企业微信公司ID + - WECOM_SECRET=企业微信应用Secret + - WECOM_AID=企业微信应用ID + - WECOM_TOUID=@all + - REDIS_STAT=ON + - REDIS_ADDR=redis:6379 + - REDIS_PASSWORD=redis的连接密码 + ports: + - 8080:8080 + networks: + - go-wecomchan + depends_on: + - redis + + redis: + image: docker.io/bitnami/redis:6.2 + environment: + - REDIS_PASSWORD=redis的连接密码 + - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL + networks: + - go-wecomchan + volumes: + - 'redis_data:/bitnami/redis/data' + +volumes: + redis_data: + driver: local + +networks: + go-wecomchan: diff --git a/go-wecomchan/go.mod b/go-wecomchan/go.mod new file mode 100644 index 0000000..388cf93 --- /dev/null +++ b/go-wecomchan/go.mod @@ -0,0 +1,5 @@ +module go/wecomchan + +go 1.16 + +require github.com/go-redis/redis/v8 v8.10.0 diff --git a/go-wecomchan/go.sum b/go-wecomchan/go.sum new file mode 100644 index 0000000..97595ea --- /dev/null +++ b/go-wecomchan/go.sum @@ -0,0 +1,97 @@ +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.10.0 h1:OZwrQKuZqdJ4QIM8wn8rnuz868Li91xA3J2DEq+TPGA= +github.com/go-redis/redis/v8 v8.10.0/go.mod h1:vXLTvigok0VtUX0znvbcEW1SOt4OA9CU1ZfnOtKOaiM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-wecomchan/wecomchan.go b/go-wecomchan/wecomchan.go new file mode 100644 index 0000000..d808b7a --- /dev/null +++ b/go-wecomchan/wecomchan.go @@ -0,0 +1,302 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math" + "mime/multipart" + "net/http" + "os" + "reflect" + "time" + + "github.com/go-redis/redis/v8" +) + +/*------------------------------- 环境变量配置 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) { + // 获取token + accessToken := GetAccessToken() + // 默认token有效 + tokenValid := true + + _ = req.ParseForm() + sendkey := req.FormValue("sendkey") + if sendkey != Sendkey { + log.Panicln("sendkey 错误,请检查") + } + msgContent := req.FormValue("msg") + msgType := req.FormValue("msg_type") + log.Println("mes_type=", msgType) + // 默认mediaId为空 + mediaId := "" + 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)) + } + http.HandleFunc("/wecomchan", wecomChan) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..a26b32d --- /dev/null +++ b/index.php @@ -0,0 +1,87 @@ +connect(REDIS_HOST, REDIS_PORT); + } + + return $GLOBALS['REDIS_INSTANCE']; +} + +function send_to_wecom($text, $wecom_cid, $wecom_secret, $wecom_aid, $wecom_touid = '@all') +{ + $access_token = false; + // 如果启用redis作为缓存 + if (REDIS_ON) { + $access_token = redis()->get(REDIS_KEY); + } + + if (!$access_token) { + $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true); + + if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) { + $access_token = $info['access_token']; + } + } + + if ($access_token) { + $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token); + $data = new \stdClass(); + $data->touser = $wecom_touid; + $data->agentid = $wecom_aid; + $data->msgtype = "text"; + $data->text = ["content"=> $text]; + $data->duplicate_check_interval = 600; + + $data_json = json_encode($data); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json); + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + if ($response !== false && REDIS_ON) { + redis()->set(REDIS_KEY, $access_token, ['nx', 'ex'=>REDIS_EXPIRED]); + } + return $response; + } + + + return false; +}