├── .gitignore
├── 20210208142819.png
├── LICENSE
├── ONLINE.md
├── README.md
├── dotNetCore.cs
├── go-scf
├── .gitignore
├── README.md
├── build.sh
├── consts
│ └── consts.go
├── dal
│ └── dal.go
├── go.mod
├── go.sum
├── main.go
├── model
│ └── model.go
├── service
│ └── wecomchan.go
└── utils
│ └── utils.go
├── go-wecomchan
├── Dockerfile
├── Dockerfile.architecture
├── README.md
├── docker-compose.yml
├── go.mod
├── go.sum
└── wecomchan.go
├── index.php
├── python-aliyunfc
├── README.md
├── main-code.zip
└── pic
│ ├── image-20220205142747826.png
│ ├── image-20220205142906239.png
│ ├── image-20220205143309699.png
│ └── image-20220205144020332.png
├── python-baiduCFC
├── README.md
├── baidu-code.zip
└── pic
│ ├── image-20220517004653408.png
│ ├── image-20220517004717978.png
│ ├── image-20220517004919062.png
│ ├── image-20220517005019692.png
│ ├── image-20220517005724481.png
│ ├── image-20220517005727886.png
│ ├── image-20220517010037726.png
│ ├── image-20220517011904665.png
│ └── image-20220517013849304.png
└── python-huaweiFG
├── README.md
└── main.py
/.gitignore:
--------------------------------------------------------------------------------
1 | demo.php
2 | show.php
3 | .DS_Store
4 | .idea
5 | go-wecomchan/wecomchan
6 | go-wecomchan/wecomchan.exe
--------------------------------------------------------------------------------
/20210208142819.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/20210208142819.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Easy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ONLINE.md:
--------------------------------------------------------------------------------
1 | # 在线服务搭建指南(PHP版)
2 |
3 | ## 安装条件
4 |
5 | - PHP7.4+
6 | - JSON &&CURL 模块
7 | - 可访问外部网络的运行环境
8 |
9 | ## 安装说明
10 |
11 | 1. 用编辑器打开 `index.php`,按提示修改头部 define 的值( sendkey自己随意写,其他参见企业微信配置文档 )
12 | 1. 将 `index.php` 上传运行环境
13 | 1. 通过 `http://指向运行环境的域名/?sendkey=你设定的sendkey&text=你要发送的内容` 即可发送内容
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wecom酱
2 |
3 | 通过企业微信向微信推送消息的解决方案。包括:
4 |
5 | 1. 配置说明(本页下方)
6 | 2. 推送函数(支持多种语言,见本页下方)
7 | 3. 自行搭建的在线服务源码
8 | 1. [PHP版搭建说明](ONLINE.md)
9 | 2. [Go版说明](go-wecomchan/README.md)
10 | 3. [Go适配华为函数工作流版本](https://github.com/Colo-Thor/wecomchan/releases/tag/2.2)
11 | 4. [腾讯云云函数搭建说明](go-scf/) ⚠️ 2022年5月起的最低月消费[已经取消了](https://cloud.tencent.com/document/product/583/104909)
12 | 5. [阿里云云函数搭建说明](python-aliyunfc/)
13 | 6. [百度智能云函数搭建说明](python-baiduCFC/)
14 | 7. [Python版华为函数工作流搭建说明](python-huaweiFG/)
15 |
16 | ## 🎈 本项目属于方糖推送生态。该生态包含项目如下:
17 |
18 | - [Server酱Turbo](https://sct.ftqq.com):支持企业微信、微信服务号、钉钉、飞书群机器人等多通道的在线服务,无需搭建直接使用,每天有免费额度
19 | - [Wecom酱](https://github.com/easychen/wecomchan):通过企业微信推送消息到微信的消息推送函数和在线服务方案,开源免费,可自己搭建。支持多语言。
20 | - [PushDeer](https://github.com/easychen/pushdeer):可自行搭建的、无需安装APP的开源推送方案。同时也提供安装APP的降级方案给低版本/没有快应用的系统。支持作为Server酱的通道进行推送,所有支持Server酱的软件和插件都能直接整合PushDeer。
21 |
22 | ## 企业微信应用消息配置说明
23 |
24 | 优点:
25 |
26 | 1. 一次配置,持续使用
27 | 1. 配置好以后,只需要微信就能收消息,不再需要安装企业微信客户端
28 |
29 | PS:消息接口无需认证即可使用,个人用微信就可以注册
30 |
31 | ### 具体操作
32 |
33 | #### 第一步,注册企业
34 |
35 | 用电脑打开[企业微信官网](https://work.weixin.qq.com/),注册一个企业
36 |
37 | #### 第二步,创建应用
38 |
39 | 注册成功后,点「管理企业」进入管理界面,选择「应用管理」 → 「自建」 → 「创建应用」
40 |
41 | 
42 |
43 | 应用名称填入「Server酱」,应用logo到[这里](./20210208142819.png)下载,可见范围选择公司名。
44 |
45 |
46 | 
47 |
48 | 创建完成后进入应用详情页,可以得到应用ID( `agentid` )①,应用Secret( `secret` )②。
49 |
50 | 注意:`secret`推送到手机端时,只能在`企业微信客户端`中查看。
51 |
52 | #### 第三步,添加可信IP
53 |
54 | > 2022年6月20日之后创建的应用,需要额外配置可信IP。
55 |
56 | 在「应用详情页」的最下方,开发者接口分类中,找到「企业可信IP」,点击「配置」,并填入服务器IP即可。
57 |
58 | 注意,如果你使用云函数等公用IP的云服务,可能需要在(云函数或其他服务的)设置界面中打开「固定公网IP」来获得一个独立的IP。否则有可能报「第三方服务IP」错误。
59 |
60 | #### 第四步,获取企业ID
61 |
62 | 进入「[我的企业](https://work.weixin.qq.com/wework_admin/frame#profile)」页面,拉到最下边,可以看到企业ID③,复制并填到上方。
63 |
64 | 推送UID直接填 `@all` ,推送给公司全员。
65 |
66 | #### 第五步,推送消息到微信
67 |
68 | 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到下边扫描二维码,关注以后即可收到推送的消息。
69 |
70 | 
71 |
72 | PS:如果出现`接口请求正常,企业微信接受消息正常,个人微信无法收到消息`的情况:
73 |
74 | 1. 进入「我的企业」 → 「[微信插件](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)」,拉到最下方,勾选 “允许成员在微信插件中接收和回复聊天消息”
75 | 
76 |
77 | 2. 在企业微信客户端 「我」 → 「设置」 → 「新消息通知」中关闭 “仅在企业微信中接受消息” 限制条件
78 | 
79 |
80 | #### 第六步,通过以下函数发送消息:
81 |
82 | PS:为使用方便,以下函数没有对 `access_token` 进行缓存。对于个人低频调用已经够用。带缓存的实现可查看 `index.php` 中的示例代码(依赖Redis实现)。
83 |
84 | PHP版:
85 |
86 | ```php
87 | function send_to_wecom($text, $wecom_cid, $wecom_aid, $wecom_secret, $wecom_touid = '@all')
88 | {
89 | $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true);
90 |
91 | if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) {
92 | $access_token = $info['access_token'];
93 | $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token);
94 | $data = new \stdClass();
95 | $data->touser = $wecom_touid;
96 | $data->agentid = $wecom_aid;
97 | $data->msgtype = "text";
98 | $data->text = ["content"=> $text];
99 | $data->duplicate_check_interval = 600;
100 |
101 | $data_json = json_encode($data);
102 | $ch = curl_init();
103 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
104 | curl_setopt($ch, CURLOPT_URL, $url);
105 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
106 | @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
107 | curl_setopt($ch, CURLOPT_POST, true);
108 | curl_setopt($ch, CURLOPT_TIMEOUT, 5);
109 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
110 |
111 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
112 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
113 |
114 | $response = curl_exec($ch);
115 | return $response;
116 | }
117 | return false;
118 | }
119 |
120 | ```
121 |
122 | 使用实例:
123 |
124 | ```php
125 | $ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②");
126 | print_r( $ret );
127 | ```
128 |
129 | PYTHON版:
130 |
131 | ```python
132 | import json,requests,base64
133 | def send_to_wecom(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'):
134 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
135 | response = requests.get(get_token_url).content
136 | access_token = json.loads(response).get('access_token')
137 | if access_token and len(access_token) > 0:
138 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
139 | data = {
140 | "touser":wecom_touid,
141 | "agentid":wecom_aid,
142 | "msgtype":"text",
143 | "text":{
144 | "content":text
145 | },
146 | "duplicate_check_interval":600
147 | }
148 | response = requests.post(send_msg_url,data=json.dumps(data)).content
149 | return response
150 | else:
151 | return False
152 |
153 | def send_to_wecom_image(base64_content,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'):
154 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
155 | response = requests.get(get_token_url).content
156 | access_token = json.loads(response).get('access_token')
157 | if access_token and len(access_token) > 0:
158 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image'
159 | upload_response = requests.post(upload_url, files={
160 | "picture": base64.b64decode(base64_content)
161 | }).json()
162 | if "media_id" in upload_response:
163 | media_id = upload_response['media_id']
164 | else:
165 | return False
166 |
167 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
168 | data = {
169 | "touser":wecom_touid,
170 | "agentid":wecom_aid,
171 | "msgtype":"image",
172 | "image":{
173 | "media_id": media_id
174 | },
175 | "duplicate_check_interval":600
176 | }
177 | response = requests.post(send_msg_url,data=json.dumps(data)).content
178 | return response
179 | else:
180 | return False
181 |
182 | def send_to_wecom_markdown(text,wecom_cid,wecom_aid,wecom_secret,wecom_touid='@all'):
183 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
184 | response = requests.get(get_token_url).content
185 | access_token = json.loads(response).get('access_token')
186 | if access_token and len(access_token) > 0:
187 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
188 | data = {
189 | "touser":wecom_touid,
190 | "agentid":wecom_aid,
191 | "msgtype":"markdown",
192 | "markdown":{
193 | "content":text
194 | },
195 | "duplicate_check_interval":600
196 | }
197 | response = requests.post(send_msg_url,data=json.dumps(data)).content
198 | return response
199 | else:
200 | return False
201 | ```
202 |
203 | 使用实例:
204 |
205 | ```python
206 | ret = send_to_wecom("推送测试\r\n测试换行", "企业ID③", "应用ID①", "应用secret②");
207 | print( ret );
208 | ret = send_to_wecom('文本中支持超链接', "企业ID③", "应用ID①", "应用secret②");
209 | print( ret );
210 | ret = send_to_wecom_image("此处填写图片Base64", "企业ID③", "应用ID①", "应用secret②");
211 | print( ret );
212 | ret = send_to_wecom_markdown("**Markdown 内容**", "企业ID③", "应用ID①", "应用secret②");
213 | print( ret );
214 | ```
215 |
216 | TypeScript 版:
217 |
218 | ```typescript
219 | import request from 'superagent'
220 |
221 | async function sendToWecom(body: {
222 | text: string
223 | wecomCId: string
224 | wecomSecret: string
225 | wecomAgentId: string
226 | wecomTouid?: string
227 | }): Promise<{ errcode: number; errmsg: string; invaliduser: string }> {
228 | body.wecomTouid = body.wecomTouid ?? '@all'
229 | const getTokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${body.wecomCId}&corpsecret=${body.wecomSecret}`
230 | const getTokenRes = await request.get(getTokenUrl)
231 | const accessToken = getTokenRes.body.access_token
232 | if (accessToken?.length <= 0) {
233 | throw new Error('获取 accessToken 失败')
234 | }
235 | const sendMsgUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`
236 | const sendMsgRes = await request.post(sendMsgUrl).send({
237 | touser: body.wecomTouid,
238 | agentid: body.wecomAgentId,
239 | msgtype: 'text',
240 | text: {
241 | content: body.text,
242 | },
243 | duplicate_check_interval: 600,
244 | })
245 | return sendMsgRes.body
246 | }
247 | ```
248 |
249 | 使用实例:
250 |
251 | ```typescript
252 | sendToWecom({
253 | text: '推送测试\r\n测试换行',
254 | wecomAgentId: '应用ID①',
255 | wecomSecret: '应用secret②',
256 | wecomCId: '企业ID③',
257 | })
258 | .then((res) => {
259 | console.log(res)
260 | })
261 | .catch((err) => {
262 | console.log(err)
263 | })
264 | ```
265 |
266 | .NET Core 版:
267 |
268 | ```C#
269 | using System;
270 | using RestSharp;
271 | using Newtonsoft.Json;
272 | namespace WeCom.Demo
273 | {
274 | class WeCom
275 | {
276 | public string SendToWeCom(
277 | string text,// 推送消息
278 | string weComCId,// 企业Id①
279 | string weComSecret,// 应用secret②
280 | string weComAId,// 应用ID③
281 | string weComTouId = "@all")
282 | {
283 | // 获取Token
284 | string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}";
285 | string token = JsonConvert
286 | .DeserializeObject(new RestClient(getTokenUrl)
287 | .Get(new RestRequest()).Content).access_token;
288 | System.Console.WriteLine(token);
289 | if (!String.IsNullOrWhiteSpace(token))
290 | {
291 | var request = new RestRequest();
292 | var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}");
293 | var data = new
294 | {
295 | touser = weComTouId,
296 | agentid = weComAId,
297 | msgtype = "text",
298 | text = new
299 | {
300 | content = text
301 | },
302 | duplicate_check_interval = 600
303 | };
304 | string serJson = JsonConvert.SerializeObject(data);
305 | System.Console.WriteLine(serJson);
306 | request.Method = Method.POST;
307 | request.AddHeader("Accept", "application/json");
308 | request.Parameters.Clear();
309 | request.AddParameter("application/json", serJson, ParameterType.RequestBody);
310 | return client.Execute(request).Content;
311 | }
312 | return "-1";
313 | }
314 | }
315 |
316 |
317 | ```
318 | 使用实例:
319 | ```C#
320 | static void Main(string[] args)
321 | { // 测试
322 | Console.Write(new WeCom().SendToWeCom(
323 | "msginfo",
324 | "企业Id①"
325 | , "应用secret②",
326 | "应用ID③"
327 | ));
328 | }
329 |
330 | }
331 | ```
332 |
333 | - [纯Bash版本参考](https://gitee.com/Hemingway2003/pushservice/blob/master/wecom.sh)
334 |
335 | 其他版本的函数可参照上边的逻辑自行编写,欢迎PR。
336 |
337 | 发送图片、卡片、文件或 Markdown 消息的高级用法见 [企业微信API](https://work.weixin.qq.com/api/doc/90000/90135/90236)。
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/dotNetCore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using RestSharp;
3 | using Newtonsoft.Json;
4 | namespace WeCom.Demo
5 | {
6 | class WeCom
7 | {
8 | public string SendToWeCom(
9 | string text,// 推送消息
10 | string weComCId,// 企业Id①
11 | string weComSecret,// 应用secret②
12 | string weComAId,// 应用ID③
13 | string weComTouId = "@all")
14 | {
15 | // 获取Token
16 | string getTokenUrl = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={weComCId}&corpsecret={weComSecret}";
17 | string token = JsonConvert
18 | .DeserializeObject(new RestClient(getTokenUrl)
19 | .Get(new RestRequest()).Content).access_token;
20 | System.Console.WriteLine(token);
21 | if (!String.IsNullOrWhiteSpace(token))
22 | {
23 | var request = new RestRequest();
24 | var client = new RestClient($"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}");
25 | var data = new
26 | {
27 | touser = weComTouId,
28 | agentid = weComAId,
29 | msgtype = "text",
30 | text = new
31 | {
32 | content = text
33 | },
34 | duplicate_check_interval = 600
35 | };
36 | string serJson = JsonConvert.SerializeObject(data);
37 | System.Console.WriteLine(serJson);
38 | request.Method = Method.POST;
39 | request.AddHeader("Accept", "application/json");
40 | request.Parameters.Clear();
41 | request.AddParameter("application/json", serJson, ParameterType.RequestBody);
42 | return client.Execute(request).Content;
43 | }
44 | return "-1";
45 | }
46 | static void Main(string[] args)
47 | { // 测试
48 | Console.Write(new WeCom().SendToWeCom(
49 | "msginfo",
50 | "企业Id①"
51 | , "应用secret②",
52 | "应用ID③"
53 | ));
54 | }
55 |
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/go-scf/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | main
3 | msg_notice
4 | *.exe
5 | *.zip
6 | *_test.go
7 | *.zip
8 | .DS_Store
9 | config.yaml
--------------------------------------------------------------------------------
/go-scf/README.md:
--------------------------------------------------------------------------------
1 | # 腾讯云云函数部署Server酱📣
2 |
3 | > 腾讯云函数2022年5月23日起的月最低消费[已经取消了](https://cloud.tencent.com/document/product/583/104909)
4 |
5 | 本项目是对 [Wecom酱](https://github.com/easychen/wecomchan) 进行的扩展,可以通过企业微信 OpenAPI 向微信推送消息,实现微信消息提醒。
6 |
7 | 利用 [腾讯云云函数](https://cloud.tencent.com/product/scf) ServerLess 的能力,以极低的费用(按量付费,且有大量免费额度)来完成部署
8 |
9 | 优点:
10 |
11 | - 便宜:说是免费也不过分
12 | - 简单:不需要购买vps, 也不需要备案, 腾讯云速度有保障.
13 | - 易搭建:一个可执行二进制文件,直接上传至腾讯云函数控制面板即可,虽然使用 Golang 编写,但是搭建无需 Golang 环境
14 | - Serverless:无服务器,函数调用完资源会释放
15 |
16 | ## 🖐️ 简单介绍
17 |
18 | 我们要实现的目标是把消息推送到微信上,此处借助了使用 企业微信,可以创建机器人,利用微信的 OpenAPI 来实现消息推送,本项目做了一个简单的封装。
19 |
20 | 欢迎PR代码。
21 |
22 | > 老用户注意:
23 | >
24 | > 自 2.0 版本之后,不再需要 `config.yaml` 文件,配置改为从云函数的环境变量中读取,请直接下载 `main.zip` 上传至云函数并且设置环境变量即可。
25 |
26 | ## 👋 使用方法
27 |
28 | ### 1. 注册企业 & 创建机器人 & 获取相关配置信息
29 |
30 | 此处不再赘述,项目主页有完整的操作方法,见:https://github.com/riba2534/wecomchan
31 |
32 | ### 2. 下载编译好的二进制文件
33 |
34 | 下载文件 [版本发布页面](https://github.com/riba2534/wecomchan/releases):
35 |
36 | - [main.zip](https://github.com/riba2534/wecomchan/releases/download/2.1/main.zip) :云函数可执行二进制文件,不用改动,等会直接上传即可。
37 |
38 | ### 3. 在腾讯云中创建云函数 & 配置环境变量
39 |
40 | 打开云函数控制台:https://console.cloud.tencent.com/scf/list
41 |
42 | 点击新建:
43 |
44 | 
45 |
46 | 如图所示选择
47 |
48 | 1. 自定义创建,函数类型为 `事件函数`
49 | 2. 填 `wecomchan`
50 | 3. 运行环境选择 Go1
51 | 4. 函数代码选择本地上传ZIP包,直接上传刚才下载的 `main.zip`
52 | 5. 在 `高级配置` 中配置环境变量,6 个环境变量,**缺一不可**,(后续想改环境变量,直接在创建好的函数中编辑即可)
53 |
54 | 环境变量配置说明
55 |
56 | | key | value | 备注 |
57 | | :------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58 | | `FUNC_NAME` | 填 `wecomchan` | |
59 | | `SEND_KEY` | 最终调用HTTP接口时校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | |
60 | | `WECOM_CID` | 企业微信公司ID | |
61 | | `WECOM_SECRET` | 企业微信应用Secret | |
62 | | `WECOM_AID` | 企业微信应用ID | |
63 | | `WECOM_TOUID` | `@all` | 此处指推送消息的默认发送对象,填 `@all`,则代表向该企业的全部成员推送消息(如果是个人用的话,一个企业中只有你自己,直接填 `@all` 即可),如果想指定具体发送的人,后面会说明怎么发。 |
64 |
65 | 6. 在 `触发器配置` 中,新增 `API网关触发`,保持默认配置即可。
66 | 7. 点击完成
67 |
68 | 
69 |
70 | 
71 |
72 | 
73 |
74 | 稍等一会,进入你创建的函数:
75 |
76 | 
77 |
78 | 图中所示的访问路径就是函数的请求路径,至此,所有的配置完成。
79 |
80 | ## 👌 发起HTTP请求测试是否成功
81 |
82 | 现已支持 `GET`、`POST` 方法进行请求。
83 |
84 | > 当发送的文本中存在有换行符或其他字符时,请把 msg 参数进行 url 编码(使用 GET 方法注意,POST不需要)
85 |
86 | ### 简单使用:
87 |
88 | 在你刚才获得的路径之后拼几个GET参数,在后面加上:`?sendkey=你配置的sendkey&msg_type=text&msg=hello`
89 |
90 | 
91 |
92 | 可以看见返回 success 字样。
93 |
94 | 观察手机推送,也可以收到消息:
95 |
96 | 
97 |
98 | 之后,想怎么用就是你的事了,想给自己的微信推送,只需要给这个 URL 发一条 HTTP 请求即可。
99 |
100 | ### 给指定成员推送消息:
101 |
102 | 如果你的需求是给企业微信中的指定成员发送消息而不是所有成员,则在 GET 请求中多加一个参数 `to_user`,值为 成员ID列表,如果想指定多个成员,则多个成员ID之间用 `|` 隔开。如请求:`https://xxxxx/wecomchan?sendkey=123456&msg_type=text&msg=测试消息&to_user=User1|User2` ,也能收到消息。
103 |
104 | 
105 |
106 | > 成员的 ID 在企业微信后台,`通讯录`,点开指定成员资料,有个 `账号` 字段,该字段即为该成员的ID.
107 |
108 | ### 使用 `POST` 进行请求
109 |
110 | 大部分情况下,`GET` 请求已经可以很好的满足发送一些短消息的需求,但是当消息体过长时,云函数可能报参数过长错误,故在 `V2.1` 版本加入 `POST` 请求支持。
111 |
112 | 与 `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 格式例如:
113 |
114 | ```json
115 | {
116 | "sendkey": "123456",
117 | "msg_type": "text",
118 | "msg": "这是一条POST消息",
119 | "to_user": "User1|User2"
120 | }
121 | ```
122 |
123 | ### 参数说明:
124 |
125 | 下表为请求的参数说明(`GET` 与 `POST` 字段名相同):
126 |
127 | | 参数名称 | 说明 | 是否可选 |
128 | | ---------- | --------------------------------------------------------------------------------------------------------------- | -------- |
129 | | `sendkey` | 校验是否是本人调用的密钥,随意设置,最终发起HTTP请求携带即可 | 必须 |
130 | | `msg_type` | 消息类型,目前只有纯文本一种类型,值为 `text` | 必须 |
131 | | `msg` | 消息内容,支持多行和UTF8字符,在程序中构建字符串时加上**换行符**即可,如果有特殊符号,记得使用 `urlencode` 编码 | 必须 |
132 | | `to_user` | 如果需要给企业内指定成员发消息,可在此参数中指定成员。如果不传本参数,默认所有成员。 | 可选 |
133 |
134 | 👇👇👇
135 |
136 | ---
137 |
138 | 如果发现bug,或者对本项目有任何建议,欢迎联系 `riba2534@qq.com` 或者直接提 [Issue](https://github.com/riba2534/wecomchan/issues).
139 |
140 |
--------------------------------------------------------------------------------
/go-scf/build.sh:
--------------------------------------------------------------------------------
1 | set -ex
2 |
3 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main && upx -9 main
4 |
5 | zip main.zip main
--------------------------------------------------------------------------------
/go-scf/consts/consts.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | var (
4 | FUNC_NAME string
5 | SEND_KEY string
6 | WECOM_CID string
7 | WECOM_SECRET string
8 | WECOM_AID string
9 | WECOM_TOUID string
10 | )
11 |
12 | // 微信发消息API
13 | const (
14 | // https://work.weixin.qq.com/api/doc/90000/90135/90236
15 | WeComMsgSendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s"
16 | // https://work.weixin.qq.com/api/doc/90000/90135/91039
17 | WeComAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
18 | )
19 |
--------------------------------------------------------------------------------
/go-scf/dal/dal.go:
--------------------------------------------------------------------------------
1 | package dal
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "time"
8 |
9 | jsoniter "github.com/json-iterator/go"
10 | "github.com/riba2534/wecomchan/go-scf/consts"
11 | "github.com/riba2534/wecomchan/go-scf/model"
12 | )
13 |
14 | var AccessToken string
15 |
16 | func loadAccessToken() {
17 | client := http.Client{Timeout: 10 * time.Second}
18 | req, _ := http.NewRequest("GET", fmt.Sprintf(consts.WeComAccessTokenURL, consts.WECOM_CID, consts.WECOM_SECRET), nil)
19 | resp, err := client.Do(req)
20 | if err != nil {
21 | fmt.Println("getAccessToken err=", err)
22 | }
23 | defer resp.Body.Close()
24 | if resp.StatusCode != 200 {
25 | fmt.Println("getAccessToken statusCode is not 200")
26 | }
27 | respBodyBytes, _ := ioutil.ReadAll(resp.Body)
28 | assesTokenResp := &model.AssesTokenResp{}
29 | if err := jsoniter.Unmarshal(respBodyBytes, assesTokenResp); err != nil {
30 | fmt.Println("getAccessToken json Unmarshal failed, err=", err)
31 | panic(err)
32 | }
33 | if assesTokenResp.Errcode != 0 {
34 | fmt.Println("getAccessToken assesTokenResp.Errcode != 0, err=", assesTokenResp.Errmsg)
35 | panic(err)
36 | }
37 | AccessToken = assesTokenResp.AccessToken
38 | }
39 |
40 | func Init() {
41 | loadAccessToken()
42 | fmt.Printf("[Init] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken)
43 | go func() {
44 | for {
45 | time.Sleep(30 * time.Minute)
46 | loadAccessToken()
47 | fmt.Printf("[Goroutine] accessToken load success, time=%s, token=%s\n", time.Now().Format("2006-01-02 15:04:05"), AccessToken)
48 | }
49 | }()
50 | }
51 |
--------------------------------------------------------------------------------
/go-scf/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/riba2534/wecomchan/go-scf
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/json-iterator/go v1.1.11
7 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9
8 | )
9 |
--------------------------------------------------------------------------------
/go-scf/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
5 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
6 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
7 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
8 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
9 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
10 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
16 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 h1:JdeXp/XPi7lBmpQNSUxElMAvwppMlFSiamTtXYRFuUc=
17 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8=
18 |
--------------------------------------------------------------------------------
/go-scf/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/riba2534/wecomchan/go-scf/consts"
9 | "github.com/riba2534/wecomchan/go-scf/dal"
10 | "github.com/riba2534/wecomchan/go-scf/service"
11 | "github.com/riba2534/wecomchan/go-scf/utils"
12 | "github.com/tencentyun/scf-go-lib/cloudfunction"
13 | "github.com/tencentyun/scf-go-lib/events"
14 | )
15 |
16 | func init() {
17 | consts.FUNC_NAME = utils.GetEnvDefault("FUNC_NAME", "")
18 | consts.SEND_KEY = utils.GetEnvDefault("SEND_KEY", "")
19 | consts.WECOM_CID = utils.GetEnvDefault("WECOM_CID", "")
20 | consts.WECOM_SECRET = utils.GetEnvDefault("WECOM_SECRET", "")
21 | consts.WECOM_AID = utils.GetEnvDefault("WECOM_AID", "")
22 | consts.WECOM_TOUID = utils.GetEnvDefault("WECOM_TOUID", "@all")
23 | if consts.FUNC_NAME == "" || consts.SEND_KEY == "" || consts.WECOM_CID == "" ||
24 | consts.WECOM_SECRET == "" || consts.WECOM_AID == "" || consts.WECOM_TOUID == "" {
25 | 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)
26 | panic("os.env param error")
27 | }
28 | fmt.Println("os.env load success!")
29 | }
30 |
31 | func HTTPHandler(ctx context.Context, event events.APIGatewayRequest) (events.APIGatewayResponse, error) {
32 | path := event.Path
33 | fmt.Println("req->", utils.MarshalToStringParam(event))
34 | var result interface{}
35 | if strings.HasPrefix(path, "/"+consts.FUNC_NAME) {
36 | result = service.WeComChanService(ctx, event)
37 | } else {
38 | // 匹配失败返回原始HTTP请求
39 | result = event
40 | }
41 | return events.APIGatewayResponse{
42 | IsBase64Encoded: false,
43 | StatusCode: 200,
44 | Headers: map[string]string{},
45 | Body: utils.MarshalToStringParam(result),
46 | }, nil
47 | }
48 |
49 | func main() {
50 | dal.Init()
51 | cloudfunction.Start(HTTPHandler)
52 | }
53 |
--------------------------------------------------------------------------------
/go-scf/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type AssesTokenResp struct {
4 | Errcode int `json:"errcode"`
5 | Errmsg string `json:"errmsg"`
6 | AccessToken string `json:"access_token"`
7 | ExpiresIn int `json:"expires_in"`
8 | }
9 |
10 | type MsgText struct {
11 | Content string `json:"content"`
12 | }
13 |
14 | // https://work.weixin.qq.com/api/doc/90002/90151/90854
15 | type WechatMsg struct {
16 | ToUser string `json:"touser"`
17 | AgentId string `json:"agentid"`
18 | MsgType string `json:"msgtype"`
19 | Text *MsgText `json:"text"`
20 | DuplicateCheckInterval int `json:"duplicate_check_interval"`
21 | }
22 |
23 | type PostResp struct {
24 | Errcode int `json:"errcode"`
25 | Errmsg string `json:"errmsg"`
26 | Invaliduser string `json:"invaliduser"`
27 | }
28 |
--------------------------------------------------------------------------------
/go-scf/service/wecomchan.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "net/http"
10 | "time"
11 |
12 | jsoniter "github.com/json-iterator/go"
13 | "github.com/riba2534/wecomchan/go-scf/consts"
14 | "github.com/riba2534/wecomchan/go-scf/dal"
15 | "github.com/riba2534/wecomchan/go-scf/model"
16 | "github.com/riba2534/wecomchan/go-scf/utils"
17 | "github.com/tencentyun/scf-go-lib/events"
18 | )
19 |
20 | func WeComChanService(ctx context.Context, event events.APIGatewayRequest) map[string]interface{} {
21 | sendKey := getQuery("sendkey", event)
22 | msgType := getQuery("msg_type", event)
23 | msg := getQuery("msg", event)
24 | if msgType == "" || msg == "" {
25 | return utils.MakeResp(-1, "param error")
26 | }
27 | if sendKey != consts.SEND_KEY {
28 | return utils.MakeResp(-1, "sendkey error")
29 | }
30 | toUser := getQuery("to_user", event)
31 | if toUser == "" {
32 | toUser = consts.WECOM_TOUID
33 | }
34 | if err := postWechatMsg(dal.AccessToken, msg, msgType, toUser); err != nil {
35 | return utils.MakeResp(0, err.Error())
36 | }
37 | return utils.MakeResp(0, "success")
38 | }
39 |
40 | func postWechatMsg(accessToken, msg, msgType, toUser string) error {
41 | content := &model.WechatMsg{
42 | ToUser: toUser,
43 | AgentId: consts.WECOM_AID,
44 | MsgType: msgType,
45 | DuplicateCheckInterval: 600,
46 | Text: &model.MsgText{
47 | Content: msg,
48 | },
49 | }
50 | b, _ := jsoniter.Marshal(content)
51 | client := http.Client{Timeout: 10 * time.Second}
52 | req, _ := http.NewRequest("POST", fmt.Sprintf(consts.WeComMsgSendURL, accessToken), bytes.NewBuffer(b))
53 | req.Header.Set("Content-type", "application/json")
54 | resp, err := client.Do(req)
55 | if err != nil {
56 | fmt.Println("[postWechatMsg] failed, err=", err)
57 | return nil
58 | }
59 | defer resp.Body.Close()
60 | if resp.StatusCode != 200 {
61 | fmt.Println("postWechatMsg statusCode is not 200")
62 | return errors.New("statusCode is not 200")
63 | }
64 | respBodyBytes, _ := ioutil.ReadAll(resp.Body)
65 | postResp := &model.PostResp{}
66 | if err := jsoniter.Unmarshal(respBodyBytes, postResp); err != nil {
67 | fmt.Println("postWechatMsg json Unmarshal failed, err=", err)
68 | return err
69 | }
70 | if postResp.Errcode != 0 {
71 | fmt.Println("postWechatMsg postResp.Errcode != 0, err=", postResp.Errmsg)
72 | return errors.New(postResp.Errmsg)
73 | }
74 | return nil
75 | }
76 |
77 | func getQuery(key string, event events.APIGatewayRequest) string {
78 | switch event.Method {
79 | case "GET":
80 | value := event.QueryString[key]
81 | if len(value) > 0 && value[0] != "" {
82 | return value[0]
83 | }
84 | return ""
85 | case "POST":
86 | return jsoniter.Get([]byte(event.Body), key).ToString()
87 | default:
88 | return ""
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/go-scf/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 |
6 | jsoniter "github.com/json-iterator/go"
7 | )
8 |
9 | func MarshalToStringParam(param interface{}) string {
10 | s, err := jsoniter.MarshalToString(param)
11 | if err != nil {
12 | return "{}"
13 | }
14 | return s
15 | }
16 |
17 | func MakeResp(code int, msg string) map[string]interface{} {
18 | return map[string]interface{}{
19 | "code": code,
20 | "msg": msg,
21 | }
22 | }
23 |
24 | func GetEnvDefault(key, defVal string) string {
25 | val, ex := os.LookupEnv(key)
26 | if !ex {
27 | return defVal
28 | }
29 | return val
30 | }
31 |
--------------------------------------------------------------------------------
/go-wecomchan/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.16.5-alpine3.13 as gobuilder
2 |
3 | # 替换为国内源
4 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
5 |
6 | ENV GO111MODULE="on"
7 | ENV GOPROXY="https://goproxy.cn,direct"
8 | ENV CGO_ENABLED=0
9 |
10 | WORKDIR /go/src/app
11 | COPY . .
12 |
13 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates
14 | RUN update-ca-certificates
15 | RUN go build
16 |
17 | FROM scratch
18 |
19 | WORKDIR /root
20 |
21 | COPY --from=gobuilder /go/src/app/wecomchan .
22 | COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
23 |
24 | EXPOSE 8080
25 |
26 | CMD ["./wecomchan"]
27 |
--------------------------------------------------------------------------------
/go-wecomchan/Dockerfile.architecture:
--------------------------------------------------------------------------------
1 | FROM --platform=$TARGETPLATFORM golang:1.16.5-alpine3.13 as gobuilder
2 |
3 | ENV GO111MODULE="on"
4 | ENV GOPROXY="https://goproxy.cn,direct"
5 | ENV CGO_ENABLED=0
6 |
7 | WORKDIR /go/src/app
8 | COPY . .
9 |
10 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
11 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates
12 | RUN update-ca-certificates
13 | RUN go build
14 |
15 | FROM scratch
16 |
17 | WORKDIR /root
18 |
19 | COPY --from=gobuilder /go/src/app/wecomchan .
20 | COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
21 |
22 | EXPOSE 8080
23 |
24 | CMD ["./wecomchan"]
25 |
--------------------------------------------------------------------------------
/go-wecomchan/README.md:
--------------------------------------------------------------------------------
1 | # go-wecomchan
2 |
3 | ## what's new
4 |
5 | 添加 Dockerfile.architecture 使用docker buildx支持构建多架构镜像。
6 |
7 | 关于docker buildx build 使用方式参考官方文档:
8 |
9 | [https://docs.docker.com/engine/reference/commandline/buildx_build/](https://docs.docker.com/engine/reference/commandline/buildx_build/)
10 |
11 | ## 配置说明
12 |
13 | 直接使用和构建二进制文件使用需要golang环境,并且网络可以安装依赖。
14 | docker构建镜像使用,需要安装docker,不依赖golang以及网络。
15 |
16 | ## 修改默认值
17 |
18 | 修改的sendkey,企业微信公司ID 等默认值为你的企业中的相关信息,如不设置运行时和打包后都可通过环境变量传入。
19 |
20 | ```golang
21 | var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey")
22 | var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID")
23 | var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret")
24 | var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID")
25 | var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all")
26 | var RedisStat = GetEnvDefault("REDIS_STAT", "OFF")
27 | var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379")
28 | var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "")
29 | ```
30 |
31 | ## 直接使用
32 |
33 | 如果没有添加默认值,需要先引入环境变量,以SENDKEY为例:
34 |
35 | `export SENDKEY=set_a_sendkey`
36 | 依次引入环境变量后,执行
37 | `go run .`
38 |
39 | ## build命令构建二进制文件使用
40 |
41 | 1. 构建命令
42 | `go build`
43 |
44 | 2. 启动
45 | `./wecomchan`
46 |
47 | ## 构建docker镜像使用(推荐,不依赖golang,不依赖网络)
48 |
49 | 新增打包好的镜像可以直接使用
50 |
51 | - 推送文本or图片:`docker pull aozakiaoko/go-wecomchan`
52 | Docker Hub 地址为:[https://hub.docker.com/r/aozakiaoko/go-wecomchan](https://hub.docker.com/r/aozakiaoko/go-wecomchan)
53 |
54 | 已经更新latest镜像为 @fcbhank 的最新代码,并支持arm64设备。也可通过aozakiaoko/go-wecomchan:v2 获取最新镜像。
55 |
56 | - v2_推送文本or图片:`docker pull fcbhank/go-wecomchan`
57 | Docker Hub 地址为:[https://hub.docker.com/r/fcbhank/go-wecomchan](https://hub.docker.com/r/fcbhank/go-wecomchan)
58 |
59 | 1. 构建镜像
60 | `docker build -t go-wecomchan .`
61 |
62 | 2. 修改默认值后启动镜像
63 | `docker run -dit -p 8080:8080 go-wecomchan`
64 |
65 | 3. 通过环境变量启动镜像并启用redis
66 |
67 | ```bash
68 | docker run -dit -e SENDKEY=set_a_sendkey \
69 | -e WECOM_CID=企业微信公司ID \
70 | -e WECOM_SECRET=企业微信应用Secret \
71 | -e WECOM_AID=企业微信应用ID \
72 | -e WECOM_TOUID="@all" \
73 | -e REDIS_STAT=ON \
74 | -e REDIS_ADDR="localhost:6379" \
75 | -e REDIS_PASSWORD="" \
76 | # aozakiaoko/go-wecomchan 已经更新镜像为 @fcbhank 的最新代码,并支持arm64设备。
77 | # v2 fcbhank/go-wecomchan
78 | -p 8080:8080 go-wecomchan
79 | ```
80 |
81 | 如不使用redis不要传入最后三个关于redis的环境变量(REDIS_STAT|REDIS_ADDR|REDIS_PASSWORD)
82 |
83 | 4. 环境变量说明
84 |
85 | |名称|描述|
86 | |---|---|
87 | |SENDKEY|发送时用来验证的key|
88 | |WECOM_CID|企业微信公司ID|
89 | |WECOM_SECRET|企业微信应用Secret|
90 | |WECOM_AID|企业微信应用ID|
91 | |WECOM_TOUID|需要发送给的人,详见[企业微信官方文档](https://work.weixin.qq.com/api/doc/90000/90135/90236#%E6%96%87%E6%9C%AC%E6%B6%88%E6%81%AF)|
92 | |REDIS_STAT|是否启用redis换缓存token,ON-启用 OFF或空-不启用|
93 | |REDIS_ADDR|redis服务器地址,如不启用redis缓存可不设置|
94 | |REDIS_PASSWORD|redis的连接密码,如不启用redis缓存可不设置|
95 |
96 | ## 使用docker-compose 部署
97 |
98 | 修改docker-compose.yml 文件内上述的环境变量,之后执行
99 |
100 | `docker-compose up -d`
101 |
102 | ## 调用方式
103 | - v1_推送文本
104 | 访问 `http://localhost:8080/wecomchan?sendkey=你配置的sendkey&&msg=需要发送的消息&&msg_type=text`
105 |
106 | - v2_推送文本or图片
107 |
108 | ```bash
109 | # 推送文本消息
110 | curl --location --request GET 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg={你的文本消息}&msg_type=text'
111 |
112 | # 推送图片消息
113 | curl --location --request POST 'http://localhost:8080/wecomchan?sendkey={你的sendkey}&msg_type=image' \
114 | --form 'media=@"test.jpg"'
115 | ```
116 |
117 | ## 后续预计添加
118 |
119 | * [x] Dockerfile 打包镜像(不依赖网络环境)
120 | * [x] 通过环境变量传递企业微信id,secret等,镜像一次构建多次使用
121 | * [x] docker-compose redis + go-wecomchan 一键部署
--------------------------------------------------------------------------------
/go-wecomchan/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | go-wecomchan:
5 | image: docker.io/aozakiaoko/go-wecomchan:latest
6 | environment:
7 | - SENDKEY=发送时用来验证的key
8 | - WECOM_CID=企业微信公司ID
9 | - WECOM_SECRET=企业微信应用Secret
10 | - WECOM_AID=企业微信应用ID
11 | - WECOM_TOUID=@all
12 | - REDIS_STAT=ON
13 | - REDIS_ADDR=redis:6379
14 | - REDIS_PASSWORD=redis的连接密码
15 | ports:
16 | - 8080:8080
17 | networks:
18 | - go-wecomchan
19 | depends_on:
20 | - redis
21 |
22 | redis:
23 | image: docker.io/bitnami/redis:6.2
24 | environment:
25 | - REDIS_PASSWORD=redis的连接密码
26 | - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
27 | networks:
28 | - go-wecomchan
29 | volumes:
30 | - 'redis_data:/bitnami/redis/data'
31 |
32 | volumes:
33 | redis_data:
34 | driver: local
35 |
36 | networks:
37 | go-wecomchan:
38 |
--------------------------------------------------------------------------------
/go-wecomchan/go.mod:
--------------------------------------------------------------------------------
1 | module go/wecomchan
2 |
3 | go 1.16
4 |
5 | require github.com/go-redis/redis/v8 v8.10.0
6 |
--------------------------------------------------------------------------------
/go-wecomchan/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
2 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
8 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
10 | github.com/go-redis/redis/v8 v8.10.0 h1:OZwrQKuZqdJ4QIM8wn8rnuz868Li91xA3J2DEq+TPGA=
11 | github.com/go-redis/redis/v8 v8.10.0/go.mod h1:vXLTvigok0VtUX0znvbcEW1SOt4OA9CU1ZfnOtKOaiM=
12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
13 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
14 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
15 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
16 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
17 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
18 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
19 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
20 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
21 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
22 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
24 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
25 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
26 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
27 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
28 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
29 | github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
30 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
31 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
32 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
33 | github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
34 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
38 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
39 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
40 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
41 | go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
42 | go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
43 | go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
44 | go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
45 | go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
46 | go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
47 | go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
48 | go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
49 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
50 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
51 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
52 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
53 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
54 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
55 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
56 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
57 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
58 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
59 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
60 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
62 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
63 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
66 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
67 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
68 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
69 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
70 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 | golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
72 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
74 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
75 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
78 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
80 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
83 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
84 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
85 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
86 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
87 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
88 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
90 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
91 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
92 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
93 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
94 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
95 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
96 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
98 |
--------------------------------------------------------------------------------
/go-wecomchan/wecomchan.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 | "math"
11 | "mime/multipart"
12 | "net/http"
13 | "os"
14 | "reflect"
15 | "time"
16 |
17 | "github.com/go-redis/redis/v8"
18 | )
19 |
20 | /*------------------------------- 环境变量配置 begin -------------------------------*/
21 |
22 | var Sendkey = GetEnvDefault("SENDKEY", "set_a_sendkey")
23 | var WecomCid = GetEnvDefault("WECOM_CID", "企业微信公司ID")
24 | var WecomSecret = GetEnvDefault("WECOM_SECRET", "企业微信应用Secret")
25 | var WecomAid = GetEnvDefault("WECOM_AID", "企业微信应用ID")
26 | var WecomToUid = GetEnvDefault("WECOM_TOUID", "@all")
27 | var RedisStat = GetEnvDefault("REDIS_STAT", "OFF")
28 | var RedisAddr = GetEnvDefault("REDIS_ADDR", "localhost:6379")
29 | var RedisPassword = GetEnvDefault("REDIS_PASSWORD", "")
30 | var ctx = context.Background()
31 |
32 | /*------------------------------- 环境变量配置 end -------------------------------*/
33 |
34 | /*------------------------------- 企业微信服务端API begin -------------------------------*/
35 |
36 | var GetTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
37 | var SendMessageApi = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s"
38 | var UploadMediaApi = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
39 |
40 | /*------------------------------- 企业微信服务端API end -------------------------------*/
41 |
42 | const RedisTokenKey = "access_token"
43 |
44 | type Msg struct {
45 | Content string `json:"content"`
46 | }
47 | type Pic struct {
48 | MediaId string `json:"media_id"`
49 | }
50 | type JsonData struct {
51 | ToUser string `json:"touser"`
52 | AgentId string `json:"agentid"`
53 | MsgType string `json:"msgtype"`
54 | DuplicateCheckInterval int `json:"duplicate_check_interval"`
55 | Text Msg `json:"text"`
56 | Image Pic `json:"image"`
57 | }
58 |
59 | // GetEnvDefault 获取配置信息,未获取到则取默认值
60 | func GetEnvDefault(key, defVal string) string {
61 | val, ex := os.LookupEnv(key)
62 | if !ex {
63 | return defVal
64 | }
65 | return val
66 | }
67 |
68 | // ParseJson 将json字符串解析为map
69 | func ParseJson(jsonStr string) map[string]interface{} {
70 | var wecomResponse map[string]interface{}
71 | if string(jsonStr) != "" {
72 | err := json.Unmarshal([]byte(string(jsonStr)), &wecomResponse)
73 | if err != nil {
74 | log.Println("生成json字符串错误")
75 | }
76 | }
77 | return wecomResponse
78 | }
79 |
80 | // GetRemoteToken 从企业微信服务端API获取access_token,存在redis服务则缓存
81 | func GetRemoteToken(corpId, appSecret string) string {
82 | getTokenUrl := fmt.Sprintf(GetTokenApi, corpId, appSecret)
83 | log.Println("getTokenUrl==>", getTokenUrl)
84 | resp, err := http.Get(getTokenUrl)
85 | if err != nil {
86 | log.Println(err)
87 | }
88 | defer resp.Body.Close()
89 | respData, err := ioutil.ReadAll(resp.Body)
90 | if err != nil {
91 | log.Println(err)
92 | }
93 | tokenResponse := ParseJson(string(respData))
94 | log.Println("企业微信获取access_token接口返回==>", tokenResponse)
95 | accessToken := tokenResponse[RedisTokenKey].(string)
96 |
97 | if RedisStat == "ON" {
98 | log.Println("prepare to set redis key")
99 | rdb := RedisClient()
100 | // access_token有效时间为7200秒(2小时)
101 | set, err := rdb.SetNX(ctx, RedisTokenKey, accessToken, 7000*time.Second).Result()
102 | log.Println(set)
103 | if err != nil {
104 | log.Println(err)
105 | }
106 | }
107 | return accessToken
108 | }
109 |
110 | // RedisClient redis客户端
111 | func RedisClient() *redis.Client {
112 | rdb := redis.NewClient(&redis.Options{
113 | Addr: RedisAddr,
114 | Password: RedisPassword, // no password set
115 | DB: 0, // use default DB
116 | })
117 | return rdb
118 | }
119 |
120 | // PostMsg 推送消息
121 | func PostMsg(postData JsonData, postUrl string) string {
122 | postJson, _ := json.Marshal(postData)
123 | log.Println("postJson ", string(postJson))
124 | log.Println("postUrl ", postUrl)
125 | msgReq, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(postJson))
126 | if err != nil {
127 | log.Println(err)
128 | }
129 | msgReq.Header.Set("Content-Type", "application/json")
130 | client := &http.Client{}
131 | resp, err := client.Do(msgReq)
132 | if err != nil {
133 | log.Fatalln("企业微信发送应用消息接口报错==>", err)
134 | }
135 | defer msgReq.Body.Close()
136 | body, _ := ioutil.ReadAll(resp.Body)
137 | mediaResp := ParseJson(string(body))
138 | log.Println("企业微信发送应用消息接口返回==>", mediaResp)
139 | return string(body)
140 | }
141 |
142 | // UploadMedia 上传临时素材并返回mediaId
143 | func UploadMedia(msgType string, req *http.Request, accessToken string) (string, float64) {
144 | // 企业微信图片上传不能大于2M
145 | _ = req.ParseMultipartForm(2 << 20)
146 | imgFile, imgHeader, err := req.FormFile("media")
147 | log.Printf("文件大小==>%d字节", imgHeader.Size)
148 | if err != nil {
149 | log.Fatalln("图片文件出错==>", err)
150 | // 自定义code无效的图片文件
151 | return "", 400
152 | }
153 | buf := new(bytes.Buffer)
154 | writer := multipart.NewWriter(buf)
155 | if createFormFile, err := writer.CreateFormFile("media", imgHeader.Filename); err == nil {
156 | readAll, _ := ioutil.ReadAll(imgFile)
157 | createFormFile.Write(readAll)
158 | }
159 | writer.Close()
160 |
161 | uploadMediaUrl := fmt.Sprintf(UploadMediaApi, accessToken, msgType)
162 | log.Println("uploadMediaUrl==>", uploadMediaUrl)
163 | newRequest, _ := http.NewRequest("POST", uploadMediaUrl, buf)
164 | newRequest.Header.Set("Content-Type", writer.FormDataContentType())
165 | log.Println("Content-Type ", writer.FormDataContentType())
166 | client := &http.Client{}
167 | resp, err := client.Do(newRequest)
168 | respData, _ := ioutil.ReadAll(resp.Body)
169 | mediaResp := ParseJson(string(respData))
170 | log.Println("企业微信上传临时素材接口返回==>", mediaResp)
171 | if err != nil {
172 | log.Fatalln("上传临时素材出错==>", err)
173 | return "", mediaResp["errcode"].(float64)
174 | } else {
175 | return mediaResp["media_id"].(string), float64(0)
176 | }
177 | }
178 |
179 | // ValidateToken 判断accessToken是否失效
180 | // true-未失效, false-失效需重新获取
181 | func ValidateToken(errcode interface{}) bool {
182 | codeTyp := reflect.TypeOf(errcode)
183 | log.Println("errcode的数据类型==>", codeTyp)
184 | if !codeTyp.Comparable() {
185 | log.Printf("type is not comparable: %v", codeTyp)
186 | return true
187 | }
188 |
189 | // 如果errcode为42001表明token已失效,则清空redis中的token缓存
190 | // 已知codeType为float64
191 | if math.Abs(errcode.(float64)-float64(42001)) < 1e-3 {
192 | if RedisStat == "ON" {
193 | log.Printf("token已失效,开始删除redis中的key==>%s", RedisTokenKey)
194 | rdb := RedisClient()
195 | rdb.Del(ctx, RedisTokenKey)
196 | log.Printf("删除redis中的key==>%s完毕", RedisTokenKey)
197 | }
198 | log.Println("现需重新获取token")
199 | return false
200 | }
201 | return true
202 | }
203 |
204 | // GetAccessToken 获取企业微信的access_token
205 | func GetAccessToken() string {
206 | accessToken := ""
207 | if RedisStat == "ON" {
208 | log.Println("尝试从redis获取token")
209 | rdb := RedisClient()
210 | value, err := rdb.Get(ctx, RedisTokenKey).Result()
211 | if err == redis.Nil {
212 | log.Println("access_token does not exist, need get it from remote API")
213 | }
214 | accessToken = value
215 | }
216 | if accessToken == "" {
217 | log.Println("get access_token from remote API")
218 | accessToken = GetRemoteToken(WecomCid, WecomSecret)
219 | } else {
220 | log.Println("get access_token from redis")
221 | }
222 | return accessToken
223 | }
224 |
225 | // InitJsonData 初始化Json公共部分数据
226 | func InitJsonData(msgType string) JsonData {
227 | return JsonData{
228 | ToUser: WecomToUid,
229 | AgentId: WecomAid,
230 | MsgType: msgType,
231 | DuplicateCheckInterval: 600,
232 | }
233 | }
234 |
235 | // 主函数入口
236 | func main() {
237 | // 设置日志内容显示文件名和行号
238 | log.SetFlags(log.LstdFlags | log.Lshortfile)
239 | wecomChan := func(res http.ResponseWriter, req *http.Request) {
240 | // 获取token
241 | accessToken := GetAccessToken()
242 | // 默认token有效
243 | tokenValid := true
244 |
245 | _ = req.ParseForm()
246 | sendkey := req.FormValue("sendkey")
247 | if sendkey != Sendkey {
248 | log.Panicln("sendkey 错误,请检查")
249 | }
250 | msgContent := req.FormValue("msg")
251 | msgType := req.FormValue("msg_type")
252 | log.Println("mes_type=", msgType)
253 | // 默认mediaId为空
254 | mediaId := ""
255 | if msgType != "image" {
256 | log.Println("消息类型不是图片")
257 | } else {
258 | // token有效则跳出循环继续执行,否则重试3次
259 | for i := 0; i <= 3; i++ {
260 | var errcode float64
261 | mediaId, errcode = UploadMedia(msgType, req, accessToken)
262 | log.Printf("企业微信上传临时素材接口返回的media_id==>[%s], errcode==>[%f]\n", mediaId, errcode)
263 | tokenValid = ValidateToken(errcode)
264 | if tokenValid {
265 | break
266 | }
267 |
268 | accessToken = GetAccessToken()
269 | }
270 | }
271 |
272 | // 准备发送应用消息所需参数
273 | postData := InitJsonData(msgType)
274 | postData.Text = Msg{
275 | Content: msgContent,
276 | }
277 | postData.Image = Pic{
278 | MediaId: mediaId,
279 | }
280 |
281 | postStatus := ""
282 | for i := 0; i <= 3; i++ {
283 | sendMessageUrl := fmt.Sprintf(SendMessageApi, accessToken)
284 | postStatus = PostMsg(postData, sendMessageUrl)
285 | postResponse := ParseJson(postStatus)
286 | errcode := postResponse["errcode"]
287 | log.Println("发送应用消息接口返回errcode==>", errcode)
288 | tokenValid = ValidateToken(errcode)
289 | // token有效则跳出循环继续执行,否则重试3次
290 | if tokenValid {
291 | break
292 | }
293 | // 刷新token
294 | accessToken = GetAccessToken()
295 | }
296 |
297 | res.Header().Set("Content-type", "application/json")
298 | _, _ = res.Write([]byte(postStatus))
299 | }
300 | http.HandleFunc("/wecomchan", wecomChan)
301 | log.Fatal(http.ListenAndServe(":8080", nil))
302 | }
303 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | connect(REDIS_HOST, REDIS_PORT);
45 | $GLOBALS['REDIS_INSTANCE']->auth(REDIS_PASSWORD);
46 | }
47 |
48 | return $GLOBALS['REDIS_INSTANCE'];
49 | }
50 |
51 | function send_to_wecom($text, $wecom_cid, $wecom_secret, $wecom_aid, $wecom_touid = '@all')
52 | {
53 | $access_token = false;
54 | // 如果启用redis作为缓存
55 | if (REDIS_ON) {
56 | $access_token = redis()->get(REDIS_KEY);
57 | }
58 |
59 | if (!$access_token) {
60 | $info = @json_decode(file_get_contents("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=".urlencode($wecom_cid)."&corpsecret=".urlencode($wecom_secret)), true);
61 |
62 | if ($info && isset($info['access_token']) && strlen($info['access_token']) > 0) {
63 | $access_token = $info['access_token'];
64 | }
65 | }
66 |
67 | if ($access_token) {
68 | $url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='.urlencode($access_token);
69 | $data = new \stdClass();
70 | $data->touser = $wecom_touid;
71 | $data->agentid = $wecom_aid;
72 | $data->msgtype = "text";
73 | $data->text = ["content"=> $text];
74 | $data->duplicate_check_interval = 600;
75 |
76 | $data_json = json_encode($data);
77 | $ch = curl_init();
78 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
79 | curl_setopt($ch, CURLOPT_URL, $url);
80 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
81 | @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
82 | curl_setopt($ch, CURLOPT_POST, true);
83 | curl_setopt($ch, CURLOPT_TIMEOUT, 5);
84 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_json);
85 |
86 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
87 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
88 |
89 | $response = curl_exec($ch);
90 | if ($response !== false && REDIS_ON) {
91 | redis()->set(REDIS_KEY, $access_token, ['nx', 'ex'=>REDIS_EXPIRED]);
92 | }
93 | return $response;
94 | }
95 |
96 |
97 | return false;
98 | }
99 |
--------------------------------------------------------------------------------
/python-aliyunfc/README.md:
--------------------------------------------------------------------------------
1 | ## 阿里云函数部署 Wecom 酱
2 |
3 | 部署步骤:
4 |
5 | 1. 按照[首页](https://github.com/easychen/wecomchan)的说明配置企业微信, 准备好以下信息.
6 | 1. 企业 ID
7 | 2. 应用 secret
8 | 3. 应用 ID
9 | 4. 你自己设置的密码 (sendkey)
10 |
11 | 2. 打开[阿里云函数计算](https://fcnext.console.aliyun.com/overview), 创建服务.
12 |
13 | 
14 |
15 | 3. 服务名称自选, 点击确定.
16 |
17 | 
18 |
19 | 4. 创建函数, 函数名称自选, 运行环境 Python3, 内存规格 128MB, 其余保持默认.
20 |
21 | 
22 |
23 | 5. 选择`上传代码 - 上传 zip 包`, 上传[这个文件](main-code.zip), 使用网页代码编辑器修改 `index.py` 中的变量为第一步获取的变量, 完成后点击`部署代码`.
24 |
25 | 6. 完成!
26 |
27 | #### 使用方法
28 |
29 | 将以下内容以 json 格式 POST 到函数的公网访问地址即可.
30 |
31 | | 字段 | 说明 | 是否必须 |
32 | | ---- | ------------------------------------------------- | --------------- |
33 | | key | 设置的 sendkey | 是 |
34 | | type | text, image, markdown 或 file 其中之一 | 否, 默认为 text |
35 | | msg | 消息主体(需要推送的文本或图片/文件的 Base64 编码) | 是 |
36 |
37 | 例:
38 |
39 | ```
40 | {"key":"123", "msg": "Hello, World!"}
41 | ```
42 |
43 | ```
44 | {"key":"123", "type": "markdown", "msg": "**Markdown Here!**"}
45 | ```
46 |
47 |
--------------------------------------------------------------------------------
/python-aliyunfc/main-code.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/main-code.zip
--------------------------------------------------------------------------------
/python-aliyunfc/pic/image-20220205142747826.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205142747826.png
--------------------------------------------------------------------------------
/python-aliyunfc/pic/image-20220205142906239.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205142906239.png
--------------------------------------------------------------------------------
/python-aliyunfc/pic/image-20220205143309699.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205143309699.png
--------------------------------------------------------------------------------
/python-aliyunfc/pic/image-20220205144020332.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-aliyunfc/pic/image-20220205144020332.png
--------------------------------------------------------------------------------
/python-baiduCFC/README.md:
--------------------------------------------------------------------------------
1 | ## 百度智能云·函数计算CFC 部署 Wecom 酱
2 |
3 | 部署步骤:
4 |
5 | 1. 按照[首页](https://github.com/easychen/wecomchan)的说明配置企业微信, 准备好以下信息.
6 | 1. 企业 ID
7 | 2. 应用 secret
8 | 3. 应用 ID
9 | 4. 你自己设置的密码 (sendkey)
10 |
11 | 2. 打开[百度智能云·函数计算CFC](https://console.bce.baidu.com/cfc/), 创建服务.(若没有账户请自行创建)
12 |
13 | 
14 |
15 | 3. 服务名称自选, 点击确定.
16 |
17 | 
18 |
19 | 4. 创建函数, 函数名称自选, 运行环境 Python3.6, 内存规格 128MB, 其余如图所示或默认.
20 |
21 | 
22 |
23 | 
24 |
25 | 
26 |
27 | 
28 |
29 | 5. 进入`代码编辑页`,选择`上传函数.ZIP包`, 上传[这个文件](baidu-code.zip), 上传成功后,使用`在线编辑`修改 `index.py` 中的几个变量为第一步获取的变量, 完成后点击`保存`.
30 |
31 | 需要修改的有第 `7,128,129,130`这四行。
32 |
33 | 
34 |
35 | 如果需要配置多个应用,可把132-142行的注释去掉,并修改为对应的变量。如果只有一个应用则忽略。
36 | 
37 |
38 | 6. 完成!
39 |
40 | #### 使用方法
41 |
42 | 将以下内容以 json 格式 POST 到函数的公网访问地址即可.
43 |
44 | | 字段 | 说明 | 是否必须 |
45 | | ---- | ------------------------------------------------- | --------------- |
46 | | key | 设置的 sendkey | 是 |
47 | | type | text, image, markdown 或 file 其中之一 | 否, 默认为 text |
48 | | msg | 消息主体(需要推送的文本或图片/文件的 Base64 编码) | 是 |
49 | | uid | 发送消息的用户id,格式为 `zhangsan\|lisi\|wangwu` | 否,默认为 `@all` |
50 |
51 | 例:
52 |
53 | ```
54 | {"key":"123", "msg": "Hello, World!"}
55 | ```
56 |
57 | ```
58 | {"key":"123", "msg": "Hello, World!", "uid": "zhangsan"}
59 | ```
60 |
61 | ```
62 | {"key":"123", "type": "markdown", "msg": "**Markdown Here!**"}
63 | ```
64 |
65 | ```
66 | {
67 | "key": "123",
68 | "type": "text",
69 | "msg": "文本中支持超链接"
70 | }
71 | ```
--------------------------------------------------------------------------------
/python-baiduCFC/baidu-code.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/baidu-code.zip
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517004653408.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004653408.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517004717978.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004717978.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517004919062.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517004919062.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517005019692.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005019692.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517005724481.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005724481.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517005727886.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517005727886.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517010037726.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517010037726.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517011904665.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517011904665.png
--------------------------------------------------------------------------------
/python-baiduCFC/pic/image-20220517013849304.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/easychen/wecomchan/e6df2c47d41d8a537751bf50fdead2ba4f61634d/python-baiduCFC/pic/image-20220517013849304.png
--------------------------------------------------------------------------------
/python-huaweiFG/README.md:
--------------------------------------------------------------------------------
1 | 到[华为云函数](https://console.huaweicloud.com/functiongraph)创建python3.9的项目, 名称写WecomPush, 然后复制main.py的内容到华为云函数index.py里, 修改代码中(8, 128-131)对应参数, 之后在触发器里创建APIG网关, 安全认证选None, 仅支持使用params的方式给参数, 不支持通过body, 示例: https://push.example.com/?key=100&msg=hello, 我写的菜, text类型可以推送, 其他的估计不行, 可以pull.
2 |
--------------------------------------------------------------------------------
/python-huaweiFG/main.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | import logging
3 | import json
4 | import requests
5 | import base64
6 |
7 | # 修改为公司ID
8 | WECOM_ID = "修改为自己的公司ID"
9 |
10 |
11 | def send_to_wecom(text, wecom_cid, wecom_aid, wecom_secret, wecom_touid):
12 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
13 | response = requests.get(get_token_url).content
14 | access_token = json.loads(response).get('access_token')
15 | if access_token and len(access_token) > 0:
16 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
17 | data = {
18 | "touser": wecom_touid,
19 | "agentid": wecom_aid,
20 | "msgtype": "text",
21 | "text": {
22 | "content": text
23 | },
24 | "duplicate_check_interval": 600
25 | }
26 | response = requests.post(send_msg_url, data=json.dumps(data)).content
27 | return hook_return(str(response))
28 | else:
29 | return hook_return(None)
30 |
31 |
32 | def send_to_wecom_markdown(text, wecom_cid, wecom_aid, wecom_secret, wecom_touid):
33 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
34 | response = requests.get(get_token_url).content
35 | access_token = json.loads(response).get('access_token')
36 | if access_token and len(access_token) > 0:
37 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
38 | data = {
39 | "touser": wecom_touid,
40 | "agentid": wecom_aid,
41 | "msgtype": "markdown",
42 | "markdown": {
43 | "content": text
44 | },
45 | "duplicate_check_interval": 600
46 | }
47 | response = requests.post(send_msg_url, data=json.dumps(data)).content
48 | return hook_return(str(response))
49 | else:
50 | return hook_return(None)
51 |
52 |
53 | def send_to_wecom_pic(base64_content, wecom_cid, wecom_aid, wecom_secret, wecom_touid):
54 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
55 | response = requests.get(get_token_url).content
56 | access_token = json.loads(response).get('access_token')
57 | if access_token and len(access_token) > 0:
58 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image'
59 | upload_response = requests.post(upload_url, files={
60 | "picture": base64.b64decode(base64_content)
61 | }).json()
62 |
63 | logging.info('upload response: ' + str(upload_response))
64 |
65 | media_id = upload_response['media_id']
66 |
67 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
68 | data = {
69 | "touser": wecom_touid,
70 | "agentid": wecom_aid,
71 | "msgtype": "image",
72 | "image": {
73 | "media_id": media_id
74 | },
75 | "duplicate_check_interval": 600
76 | }
77 | response = requests.post(send_msg_url, data=json.dumps(data)).content
78 | return hook_return(str(response))
79 | else:
80 | return hook_return(None)
81 |
82 |
83 | def send_to_wecom_file(base64_content, file_name, wecom_cid, wecom_aid, wecom_secret, wecom_touid):
84 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}"
85 | response = requests.get(get_token_url).content
86 | access_token = json.loads(response).get('access_token')
87 | if access_token and len(access_token) > 0:
88 | upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=file'
89 | upload_response = requests.post(upload_url + "&debug=1", files={
90 | "media": (file_name, base64.b64decode(base64_content)) # 此处上传中文文件名文件旧版本 urllib 有 bug.
91 | }).json()
92 |
93 | logging.info('upload response: ' + str(upload_response))
94 |
95 | media_id = upload_response['media_id']
96 |
97 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}'
98 | data = {
99 | "touser": wecom_touid,
100 | "agentid": wecom_aid,
101 | "msgtype": "file",
102 | "file": {
103 | "media_id": media_id
104 | },
105 | "duplicate_check_interval": 600
106 | }
107 | response = requests.post(send_msg_url, data=json.dumps(data)).content
108 | return hook_return(str(response))
109 | else:
110 | return hook_return(None)
111 |
112 |
113 | def handler(event, context):
114 | request_body = json.dumps(event.get('queryStringParameters', ''))
115 |
116 | print(request_body)
117 | # get path info
118 | try:
119 | path_info = event.get('path')
120 | except ValueError:
121 | path_info = "ERROR"
122 |
123 | logging.info("request body: " + request_body)
124 | logging.info("path_info: " + str(path_info))
125 |
126 | # path_info 修改为触发器对应的值,上面设定的 / 这里也写 /
127 | # 应用1
128 | if path_info == "/WecomPush":
129 | send_key = '修改为应用1的'
130 | wecom_agentid = "修改为应用1的"
131 | wecom_secret = "修改为应用1的"
132 |
133 | # # 应用2
134 | # elif path_info == "/app2":
135 | # send_key = "修改为应用2的"
136 | # wecom_agentid = "修改为应用2的"
137 | # wecom_secret = "修改为应用2的"
138 | # # 应用3
139 | # elif path_info == "/app3":
140 | # send_key = "修改为应用3的"
141 | # wecom_agentid = "修改为应用3的"
142 | # wecom_secret = "修改为应用3的"
143 | # # 如果有多个应用,模拟上面添加多个elif即可。path_info可自定义,但需要添加和path_info一致的HTTP触发器
144 |
145 | else:
146 | response = '{"code": -6, "msg": "invalid path info"}'
147 | return hook_return(str(response))
148 | # input_json = None
149 | try:
150 | input_json = json.loads(request_body)
151 | if input_json['key'] != send_key:
152 | status = '403 Forbidden'
153 | response_headers = [('Content-type', 'text/json')]
154 | response = '{"code": -2, "msg": "invalid send key"}'
155 | return hook_return(str(response))
156 | except Exception as e:
157 | logging.exception(e)
158 | status = '403 Forbidden'
159 | response_headers = [('Content-type', 'text/json')]
160 | response = '{"code": -1, "msg": "invalid json input"}'
161 | return hook_return(str(response))
162 | # 获取发送的用户
163 | wecom_touid = input_json.get('uid', '@all')
164 |
165 | logging.info("wecom_touid: " + str(wecom_touid))
166 |
167 | code = 0
168 | msg = "ok"
169 | status = '200 OK'
170 |
171 | try:
172 | if 'type' not in input_json or input_json['type'] == 'text':
173 | result = send_to_wecom(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid)
174 | elif input_json['type'] == 'image':
175 | result = send_to_wecom_pic(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid)
176 | elif input_json['type'] == 'markdown':
177 | result = send_to_wecom_markdown(input_json['msg'], WECOM_ID, wecom_agentid, wecom_secret, wecom_touid)
178 | elif input_json['type'] == 'file':
179 | if 'filename' in input_json:
180 | result = send_to_wecom_file(input_json['msg'], input_json['filename'], WECOM_ID, wecom_agentid,
181 | wecom_secret, wecom_touid)
182 | else:
183 | result = send_to_wecom_file(input_json['msg'], "Wepush推送", WECOM_ID, wecom_agentid, wecom_secret,
184 | wecom_touid)
185 | msg = "filename not found. using default."
186 | else:
187 | code = -5
188 | msg = "invalid msg type. type should be text(default), image, markdown or file."
189 | status = "500 Internal Server Error"
190 | result = ""
191 |
192 | logging.info('wechat api response: ' + str(result))
193 | if result is None:
194 | status = "500 Internal Server Error"
195 | code = -4
196 | msg = "wechat api error: wrong config?"
197 | except Exception as e:
198 | status = "500 Internal Server Error"
199 | code = -3
200 | msg = "unexpected error: " + str(e)
201 | logging.exception(e)
202 |
203 | response_headers = [('Content-type', 'text/json')]
204 | response = json.dumps({"code": code, "msg": msg})
205 | return hook_return(str(response))
206 |
207 |
208 | def hook_return(string):
209 | return {
210 | "statusCode": 200,
211 | "isBase64Encoded": False,
212 | "body": string,
213 | "headers": {
214 | "Content-Type": "application/json"
215 | }
216 | }
217 |
--------------------------------------------------------------------------------