wechat 0.14.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +16 -0
- data/README-CN.md +37 -37
- data/README.md +13 -13
- data/bin/wechat +51 -12
- data/lib/action_controller/wechat_responder.rb +13 -6
- data/lib/wechat/api.rb +3 -2
- data/lib/wechat/api_loader.rb +49 -30
- data/lib/wechat/concern/common.rb +1 -1
- data/lib/wechat/concern/draft.rb +42 -0
- data/lib/wechat/controller_api.rb +1 -1
- data/lib/wechat/corp_api.rb +3 -3
- data/lib/wechat/http_client.rb +9 -4
- data/lib/wechat/message.rb +6 -1
- data/lib/wechat/mp_api.rb +3 -3
- data/lib/wechat/network_setting.rb +16 -0
- data/lib/wechat/qcloud/setting.rb +15 -0
- data/lib/wechat/qcloud/token.rb +4 -4
- data/lib/wechat/responder.rb +19 -1
- data.tar.gz.sig +0 -0
- metadata +51 -16
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1907505b47d02aed8ba3a23bc42f90b864928bb4516c6a08af562a7da26babcb
|
4
|
+
data.tar.gz: 615fe878b8b5fe6bfc176eb77c927957cad14b0fb71b65c6f891ddd850655ae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1aafb9ea5a0c1a1281810a0c28f7859de66851192d258939bd37e352dbe7ec5c70251418a41a70f0046a2da04230e8b925fef9bf6b3a7457d98feeb6de93d041
|
7
|
+
data.tar.gz: a7b7acc261ce1051be6c8afa93a8f6eb8da2f5f0fc932fd0240d2a63cebfa257864dad3d63b20d0c75ae4c2e98e74a27318cad6cba0a0f63eae2b144d2d75d42
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.16.0 (released at 2022-06-06)
|
4
|
+
|
5
|
+
* Support wechat draft. #305
|
6
|
+
* Add environment variable for configuring http proxy to ignore IP address changes everytime after app deployment, by @Awlter #312
|
7
|
+
* Soft drop support for Ruby 2.6, because EOL time 12 Apr 2022.
|
8
|
+
|
9
|
+
## v0.15.1 (released at 2022-02-16)
|
10
|
+
|
11
|
+
* fix "Psych::BadAlias (Unknown alias: default)" in ruby 3.1.0 #309, reported by @otorain
|
12
|
+
|
13
|
+
## v0.15.0 (released at 2021-12-21)
|
14
|
+
|
15
|
+
* Add wechat message json format support, by @younthu #306
|
16
|
+
* Support Rails 7 in this version.
|
17
|
+
* Fix wechat command-line 1st attempt bug #307
|
18
|
+
|
3
19
|
## v0.14.0 (released at 2021-09-15)
|
4
20
|
|
5
21
|
* Add beta support for Conversation archive in WeCom, discuss at #303
|
data/README-CN.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://
|
1
|
+
WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://mixtint.semaphoreci.com/badges/wechat/branches/main.svg?style=shields)](https://mixtint.semaphoreci.com/projects/wechat) [![Maintainability](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/maintainability)](https://codeclimate.com/github/Eric-Guo/wechat/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/test_coverage)](https://codeclimate.com/github/Eric-Guo/wechat/test_coverage)
|
2
2
|
======
|
3
3
|
|
4
4
|
[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
@@ -7,11 +7,11 @@ WeChat Gem 帮助开发者方便地在 Rails 环境中集成[微信公众平台]
|
|
7
7
|
|
8
8
|
- 微信公众平台/企业微信[发送消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF) API(命令行和 Web 环境都可以使用)
|
9
9
|
- [接收消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E6%94%B6%E6%B6%88%E6%81%AF%E4%B8%8E%E4%BA%8B%E4%BB%B6)(必须运行 Web 服务器)
|
10
|
-
- [微信JS-SDK](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3) config 接口注入权限验证
|
10
|
+
- [微信 JS-SDK](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3) config 接口注入权限验证
|
11
11
|
- OAuth 2.0 认证机制
|
12
12
|
- 接收消息会话 ( Session ) 记录机制(可选)
|
13
13
|
|
14
|
-
命令行工具 `wechat` 可以调用各种无需 Web 环境的 API,同时也提供了 Rails Controller 的 Responder DSL 。可以帮助开发者方便地在Rails 应用中集成微信的消息处理,包括主动推送的和被动响应的消息。
|
14
|
+
命令行工具 `wechat` 可以调用各种无需 Web 环境的 API,同时也提供了 Rails Controller 的 Responder DSL 。可以帮助开发者方便地在 Rails 应用中集成微信的消息处理,包括主动推送的和被动响应的消息。
|
15
15
|
|
16
16
|
如果您的 App 还需要集成微信 OAuth2.0 除了简便的 `wechat_oauth2` 指令,也可以考虑 [omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2) 以便和 [devise](https://github.com/heartcombo/devise) 集成提供完整的用户认证。
|
17
17
|
|
@@ -28,7 +28,7 @@ WeChat Gem 帮助开发者方便地在 Rails 环境中集成[微信公众平台]
|
|
28
28
|
```
|
29
29
|
gem install "wechat"
|
30
30
|
# 如果使用 Ruby 版本小于 2.6
|
31
|
-
# gem install wechat -v 0.12.
|
31
|
+
# gem install wechat -v 0.12.4
|
32
32
|
```
|
33
33
|
|
34
34
|
或者添加下面这行到 `Gemfile`:
|
@@ -36,16 +36,16 @@ gem install "wechat"
|
|
36
36
|
```
|
37
37
|
gem 'wechat'
|
38
38
|
# 如果使用 Rails 版本小于 6
|
39
|
-
# gem 'wechat', '~> 0.12.
|
39
|
+
# gem 'wechat', '~> 0.12.4'
|
40
40
|
```
|
41
41
|
|
42
|
-
|
42
|
+
运行下面这行代码来安装:
|
43
43
|
|
44
44
|
```console
|
45
45
|
bundle install
|
46
46
|
```
|
47
47
|
|
48
|
-
|
48
|
+
运行下面这行代码来生成必要文件:
|
49
49
|
|
50
50
|
```console
|
51
51
|
rails generate wechat:install
|
@@ -53,7 +53,7 @@ rails generate wechat:install
|
|
53
53
|
|
54
54
|
运行 `rails g wechat:install` 后会自动生成 wechat.yml 配置,还有 wechats_controller 及相关路由配置到当前 Rails 项目。
|
55
55
|
|
56
|
-
启用 Session
|
56
|
+
启用 Session 会话记录:
|
57
57
|
|
58
58
|
```console
|
59
59
|
rails g wechat:session
|
@@ -70,7 +70,7 @@ rails g wechat:redis_store
|
|
70
70
|
|
71
71
|
Redis 存贮相比默认的文件存贮,可以允许 Rails 应用运行在多台服务器中。如果只有一台服务器,仍然推荐使用默认的文件存贮,另外命令行不会读取 Redis 存贮的 Token 或者 Ticket。
|
72
72
|
|
73
|
-
|
73
|
+
启用数据库配置微信账户:
|
74
74
|
|
75
75
|
```console
|
76
76
|
rails g wechat:config
|
@@ -196,7 +196,7 @@ test:
|
|
196
196
|
# secret: "my_secret"
|
197
197
|
```
|
198
198
|
|
199
|
-
支持微信公众平台 / 企业微信多账号的注意点 (
|
199
|
+
支持微信公众平台 / 企业微信多账号的注意点 ( 例如:增加账号 `wx2` ):
|
200
200
|
|
201
201
|
* 配置文件可增加多个微信公众平台 ( 企业微信 ) 配置,用法类似 Rails 中 `config/database.yml` 多数据库配置的处理。 `development`, `test`, `production` 是默认账号的配置段,要想增加账号 `wx2`,你需要增加配置段 `wx2_development`, `wx2_test`, `wx2_production`。
|
202
202
|
|
@@ -219,7 +219,7 @@ test:
|
|
219
219
|
environment | 字串 | 必填。配置对应的运行环境,一般有:`production`、`development`、`test`。比如 `production` 配置仅在生产环境有效。默认为 `development`。
|
220
220
|
account | 字串 | 必填。自定义的微信账户名称。同一 `environment` 下,账户名称不允许重复。
|
221
221
|
enabled | 布尔 | 必填。配置是否生效。默认 `true`。
|
222
|
-
appid | 字串 | 公众号 id
|
222
|
+
appid | 字串 | 公众号 id ,此字段和 `corpid` 两者必填其一。
|
223
223
|
secret | 字串 | 公众号相关配置。当公众号 `appid` 存在时必填。
|
224
224
|
corpid | 字串 | 企业号 id。此字段和 `appid` 两者必填其一。
|
225
225
|
corpsecret | 字串 | 企业号相关配置。当企业号 `corpid` 存在时必填。
|
@@ -229,7 +229,7 @@ encoding_aes_key | 字串 | 当 `encrypt_mode` 为 `true` 时必填。
|
|
229
229
|
token | 字串 | 必填。
|
230
230
|
access_token | 字串 | 必填。存储 `access token` 文件的路径。
|
231
231
|
jsapi_ticket | 字串 | 必填。存储 `jsapi ticket` 文件的路径。
|
232
|
-
skip_verify_ssl |
|
232
|
+
skip_verify_ssl | 布尔 |
|
233
233
|
timeout | 整数 | 默认值是 20。
|
234
234
|
trusted_domain_fullname | 字串 |
|
235
235
|
|
@@ -237,7 +237,7 @@ trusted_domain_fullname | 字串 |
|
|
237
237
|
|
238
238
|
##### 配置优先级
|
239
239
|
|
240
|
-
注意在Rails项目根目录下运行 `wechat` 命令行工具会优先使用 `config/wechat.yml `中的 `default`配置,如果失败则使用 `~\.wechat.yml` 中的配置,以便于在生产环境下管理多个微信账号应用。
|
240
|
+
注意在 Rails 项目根目录下运行 `wechat` 命令行工具会优先使用 `config/wechat.yml `中的 `default`配置,如果失败则使用 `~\.wechat.yml` 中的配置,以便于在生产环境下管理多个微信账号应用。
|
241
241
|
|
242
242
|
如果启用数据库账户配置,数据库中的账户信息在读入 `wechat.yml` 或环境变量之后被载入。当存在同名账户时,数据库中的配置会覆盖前两者。
|
243
243
|
|
@@ -245,9 +245,9 @@ trusted_domain_fullname | 字串 |
|
|
245
245
|
|
246
246
|
微信服务器有时请求会花很长时间,如果不配置默认为 20 秒,可视情况配置。
|
247
247
|
|
248
|
-
##### 配置跳过SSL认证
|
248
|
+
##### 配置跳过 SSL 认证
|
249
249
|
|
250
|
-
Wechat 服务器有报道曾出现 [RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037) 错误,此时可以选择关闭SSL验证。`skip_verify_ssl: true`
|
250
|
+
Wechat 服务器有报道曾出现 [RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037) 错误,此时可以选择关闭 SSL 验证。`skip_verify_ssl: true`
|
251
251
|
|
252
252
|
#### 为每个 Responder 配置不同的 appid 和 secret
|
253
253
|
|
@@ -292,7 +292,7 @@ end
|
|
292
292
|
</body>
|
293
293
|
```
|
294
294
|
|
295
|
-
在开发模式下,由于程序往往通过微信调试工具的服务器端调试工具反向代理被访问,此时需要配置 `trusted_domain_fullname` 以便wechat gem 可以使用正确的域名做 JS-SDK 的权限签名。
|
295
|
+
在开发模式下,由于程序往往通过微信调试工具的服务器端调试工具反向代理被访问,此时需要配置 `trusted_domain_fullname` 以便 wechat gem 可以使用正确的域名做 JS-SDK 的权限签名。
|
296
296
|
|
297
297
|
#### OAuth2.0 验证接口支持
|
298
298
|
|
@@ -333,13 +333,13 @@ end
|
|
333
333
|
|
334
334
|
`wechat_oauth2 `封装了 OAuth2.0 验证接口和 Cookies 处理逻辑,用户仅需提供业务代码块即可。userid 指的是微信企业成员 userid,openid 是关注该公众号的用户 openid。
|
335
335
|
|
336
|
-
|
337
|
-
* 如果使用 `wechat_responder
|
338
|
-
*
|
336
|
+
注意:
|
337
|
+
* 如果使用 `wechat_responder`,请不要在 Controller 里定义 `show` 和 `create` 方法,否则会报错。
|
338
|
+
* 如果遇到 “ redirect_uri 参数错误” 的错误信息,请登录服务号管理后台,查看 “开发者中心/网页服务/网页授权获取用户基本信息” 的授权回调页面域名已正确配置。
|
339
339
|
|
340
340
|
## 关于接口权限
|
341
341
|
|
342
|
-
Wechat Gem
|
342
|
+
Wechat Gem 内部不会检查权限,但因公众号类型不同和微信服务器端通讯时,可能会被拒绝详细权限控制可参考[官方文档](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433401084)。
|
343
343
|
|
344
344
|
## 使用命令行
|
345
345
|
|
@@ -387,7 +387,7 @@ Wechat Public Account commands:
|
|
387
387
|
wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
|
388
388
|
wechat qrcode_create_scene [SCENE_ID_OR_STR, EXPIRE_SECONDS] # 请求临时二维码
|
389
389
|
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过 ticket 下载二维码
|
390
|
-
wechat queryrecoresultfortext [VOICE_ID] # AI
|
390
|
+
wechat queryrecoresultfortext [VOICE_ID] # AI 开放接口 - 获取语音识别结果
|
391
391
|
wechat shorturl [LONG_URL] # 长链接转短链接
|
392
392
|
wechat tag [TAGID] # 获取标签下粉丝列表
|
393
393
|
wechat tag_add_user [TAG_ID, OPEN_IDS] # 批量为用户打标签
|
@@ -397,7 +397,7 @@ Wechat Public Account commands:
|
|
397
397
|
wechat tag_update [TAG_ID, TAGNAME] # 更新标签名字
|
398
398
|
wechat tags # 获取所有标签
|
399
399
|
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
400
|
-
wechat translatecontent [CONTENT] # AI
|
400
|
+
wechat translatecontent [CONTENT] # AI 开放接口 - 微信翻译
|
401
401
|
wechat user [OPEN_ID] # 获取用户基本信息
|
402
402
|
wechat user_batchget [OPEN_ID_LIST] # 批量获取用户基本信息
|
403
403
|
wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
|
@@ -451,7 +451,7 @@ Wechat Enterprise Account commands:
|
|
451
451
|
wechat menu_delete # 删除菜单
|
452
452
|
wechat menu_trymatch [USER_ID] # 测试个性化菜单匹配结果
|
453
453
|
wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息
|
454
|
-
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
454
|
+
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过 ticket 下载二维码
|
455
455
|
wechat tag [TAG_ID] # 获取标签成员
|
456
456
|
wechat tag_add_department [TAG_ID, PARTY_IDS] # 增加标签部门
|
457
457
|
wechat tag_add_user [TAG_ID, USER_IDS] # 增加标签成员
|
@@ -587,7 +587,7 @@ template:
|
|
587
587
|
value: "您好,您已报名成功"
|
588
588
|
color: "#0A0A0A"
|
589
589
|
keynote1:
|
590
|
-
value: "XX活动"
|
590
|
+
value: "XX 活动"
|
591
591
|
color: "#CCCCCC"
|
592
592
|
keynote2:
|
593
593
|
value: "2014 年 9 月 16 日"
|
@@ -668,14 +668,14 @@ class WechatsController < ActionController::Base
|
|
668
668
|
request.reply.text "echo: #{content}" #Just echo
|
669
669
|
end
|
670
670
|
|
671
|
-
# 当请求的文字信息内容为 'help'
|
671
|
+
# 当请求的文字信息内容为 'help' 时,使用这个 responder 处理
|
672
672
|
on :text, with: 'help' do |request|
|
673
673
|
request.reply.text 'help content' #回复帮助信息
|
674
674
|
end
|
675
675
|
|
676
|
-
# 当请求的文字信息内容为'<n>条新闻'
|
677
|
-
on :text, with: /^(\d+)条新闻$/ do |request, count|
|
678
|
-
# 微信最多显示8条新闻,大于8条将只取前8条
|
676
|
+
# 当请求的文字信息内容为'<n>条新闻'时,使用这个 responder 处理,并将 n 作为第二个参数
|
677
|
+
on :text, with: /^(\d+) 条新闻$/ do |request, count|
|
678
|
+
# 微信最多显示 8 条新闻,大于 8 条将只取前 8 条
|
679
679
|
news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: '新闻标题', content: "第#{n}条新闻的内容#{n.hash}" } }
|
680
680
|
request.reply.news(news) do |article, n, index| # 回复"articles"
|
681
681
|
article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
|
@@ -737,7 +737,7 @@ class WechatsController < ActionController::Base
|
|
737
737
|
# request.reply.voice(request[:MediaId])
|
738
738
|
|
739
739
|
voice_id = request[:MediaId]
|
740
|
-
# 开通语音识别后,用户每次发送语音给服务号时,微信会在推送的语音消息XML数据包中,增加一个 Recognition 字段
|
740
|
+
# 开通语音识别后,用户每次发送语音给服务号时,微信会在推送的语音消息 XML 数据包中,增加一个 Recognition 字段
|
741
741
|
recognition = request[:Recognition]
|
742
742
|
request.reply.text "#{voice_id} #{recognition}"
|
743
743
|
end
|
@@ -806,12 +806,12 @@ class WechatsController < ActionController::Base
|
|
806
806
|
request.reply.success # request is XML result hash.
|
807
807
|
end
|
808
808
|
|
809
|
-
# 当无任何 responder
|
809
|
+
# 当无任何 responder 处理用户信息时,使用这个 responder 处理
|
810
810
|
on :fallback, respond: 'fallback message'
|
811
811
|
end
|
812
812
|
```
|
813
813
|
|
814
|
-
在 controller 中使用 `wechat_responder` 引入 Responder DSL
|
814
|
+
在 controller 中使用 `wechat_responder` 引入 Responder DSL,之后可以用
|
815
815
|
|
816
816
|
```
|
817
817
|
on <message_type> do |message|
|
@@ -823,26 +823,26 @@ end
|
|
823
823
|
|
824
824
|
目前支持的 message_type 有如下几种
|
825
825
|
|
826
|
-
- :text
|
826
|
+
- :text 响应文字消息,可以用 `:with` 参数来匹配文本内容 `on(:text, with:'help'){|message, content| ...}`
|
827
827
|
- :image 响应图片消息
|
828
828
|
- :voice 响应语音消息
|
829
829
|
- :shortvideo 响应短视频消息
|
830
830
|
- :video 响应视频消息
|
831
831
|
- :label_location 响应地理位置消息
|
832
832
|
- :link 响应链接消息
|
833
|
-
- :event
|
834
|
-
- :click
|
835
|
-
- :view
|
833
|
+
- :event 响应事件消息,可以用 `:with` 参数来匹配事件类型,同文字消息类似,支持正则表达式匹配
|
834
|
+
- :click 虚拟响应事件消息,微信传入:event,但 gem 内部会单独处理
|
835
|
+
- :view 虚拟响应事件消息,微信传入:event,但 gem 内部会单独处理
|
836
836
|
- :scan 虚拟响应事件消息
|
837
837
|
- :batch_job 虚拟响应事件消息
|
838
838
|
- :location 虚拟响应上报地理位置事件消息
|
839
|
-
- :fallback 默认响应,当收到的消息无法被其他responder响应时,会使用这个responder.
|
839
|
+
- :fallback 默认响应,当收到的消息无法被其他 responder 响应时,会使用这个 responder.
|
840
840
|
|
841
841
|
### 多客服消息转发
|
842
842
|
|
843
843
|
```ruby
|
844
844
|
class WechatsController < ActionController::Base
|
845
|
-
# 当无任何responder处理用户信息时,转发至客服处理。
|
845
|
+
# 当无任何 responder 处理用户信息时,转发至客服处理。
|
846
846
|
on :fallback do |message|
|
847
847
|
message.reply.transfer_customer_service
|
848
848
|
end
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://
|
1
|
+
WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://mixtint.semaphoreci.com/badges/wechat/branches/main.svg?style=shields)](https://mixtint.semaphoreci.com/projects/wechat) [![Maintainability](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/maintainability)](https://codeclimate.com/github/Eric-Guo/wechat/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/test_coverage)](https://codeclimate.com/github/Eric-Guo/wechat/test_coverage)
|
2
2
|
======
|
3
3
|
|
4
4
|
[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
@@ -35,7 +35,7 @@ Use `gem install`
|
|
35
35
|
```
|
36
36
|
gem install "wechat"
|
37
37
|
# If your ruby version < 2.6
|
38
|
-
# gem install wechat -v 0.12.
|
38
|
+
# gem install wechat -v 0.12.4
|
39
39
|
```
|
40
40
|
|
41
41
|
Or add it to your app's `Gemfile`:
|
@@ -43,7 +43,7 @@ Or add it to your app's `Gemfile`:
|
|
43
43
|
```
|
44
44
|
gem 'wechat'
|
45
45
|
# If your rails version < 6.0
|
46
|
-
# gem 'wechat', '~> 0.12.
|
46
|
+
# gem 'wechat', '~> 0.12.4'
|
47
47
|
```
|
48
48
|
|
49
49
|
Run the following command to install it:
|
@@ -381,8 +381,8 @@ Feel safe if you can not read Chinese in the comments, it's kept there in order
|
|
381
381
|
```
|
382
382
|
$ wechat
|
383
383
|
Wechat Public Account commands:
|
384
|
-
wechat addvoicetorecofortext [VOICE_ID] # AI
|
385
|
-
wechat callbackip # 获取微信服务器IP地址
|
384
|
+
wechat addvoicetorecofortext [VOICE_ID] # AI 开放接口 - 提交语音
|
385
|
+
wechat callbackip # 获取微信服务器 IP 地址
|
386
386
|
wechat clear_quota # 接口调用次数清零
|
387
387
|
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
388
388
|
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
@@ -417,8 +417,8 @@ Wechat Public Account commands:
|
|
417
417
|
wechat message_mass_preview [WX_NAME, MPNEWS_MEDIA_ID] # 预览图文消息素材
|
418
418
|
wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
|
419
419
|
wechat qrcode_create_scene [SCENE_ID_OR_STR, EXPIRE_SECONDS] # 请求临时二维码
|
420
|
-
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
421
|
-
wechat queryrecoresultfortext [VOICE_ID] # AI
|
420
|
+
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过 ticket 下载二维码
|
421
|
+
wechat queryrecoresultfortext [VOICE_ID] # AI 开放接口 - 获取语音识别结果
|
422
422
|
wechat shorturl [LONG_URL] # 长链接转短链接
|
423
423
|
wechat tag [TAGID] # 获取标签下粉丝列表
|
424
424
|
wechat tag_add_user [TAG_ID, OPEN_IDS] # 批量为用户打标签
|
@@ -428,7 +428,7 @@ Wechat Public Account commands:
|
|
428
428
|
wechat tag_update [TAG_ID, TAGNAME] # 更新标签名字
|
429
429
|
wechat tags # 获取所有标签
|
430
430
|
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
431
|
-
wechat translatecontent [CONTENT] # AI
|
431
|
+
wechat translatecontent [CONTENT] # AI 开放接口 - 微信翻译
|
432
432
|
wechat user [OPEN_ID] # 获取用户基本信息
|
433
433
|
wechat user_batchget [OPEN_ID_LIST] # 批量获取用户基本信息
|
434
434
|
wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
|
@@ -450,10 +450,10 @@ Wechat Enterprise Account commands:
|
|
450
450
|
wechat batch_replaceparty [BATCH_PARTY_CSV_MEDIA_ID] # 全量覆盖部门
|
451
451
|
wechat batch_replaceuser [BATCH_USER_CSV_MEDIA_ID] # 全量覆盖成员
|
452
452
|
wechat batch_syncuser [SYNC_USER_CSV_MEDIA_ID] # 增量更新成员
|
453
|
-
wechat callbackip # 获取微信服务器IP地址
|
453
|
+
wechat callbackip # 获取微信服务器 IP 地址
|
454
454
|
wechat clear_quota # 接口调用次数清零
|
455
|
-
wechat convert_to_openid [USER_ID] # userid转换成openid
|
456
|
-
wechat convert_to_userid [OPENID] # openid转换成userid
|
455
|
+
wechat convert_to_openid [USER_ID] # userid 转换成 openid
|
456
|
+
wechat convert_to_userid [OPENID] # openid 转换成 userid
|
457
457
|
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
458
458
|
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
459
459
|
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
@@ -483,7 +483,7 @@ Wechat Enterprise Account commands:
|
|
483
483
|
wechat menu_delete # 删除菜单
|
484
484
|
wechat menu_trymatch [USER_ID] # 测试个性化菜单匹配结果
|
485
485
|
wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息
|
486
|
-
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
486
|
+
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过 ticket 下载二维码
|
487
487
|
wechat tag [TAG_ID] # 获取标签成员
|
488
488
|
wechat tag_add_department [TAG_ID, PARTY_IDS] # 增加标签部门
|
489
489
|
wechat tag_add_user [TAG_ID, USER_IDS] # 增加标签成员
|
@@ -592,7 +592,7 @@ Sending custom_news should also be defined as a yaml file, like `articles.yml`
|
|
592
592
|
articles:
|
593
593
|
-
|
594
594
|
title: "习近平在布鲁日欧洲学院演讲"
|
595
|
-
description: "新华网比利时布鲁日4月1日电 国家主席习近平1日在比利时布鲁日欧洲学院发表重要演讲"
|
595
|
+
description: "新华网比利时布鲁日 4 月 1 日电 国家主席习近平 1 日在比利时布鲁日欧洲学院发表重要演讲"
|
596
596
|
url: "http://news.sina.com.cn/c/2014-04-01/232629843387.shtml"
|
597
597
|
pic_url: "http://i3.sinaimg.cn/dy/c/2014-04-01/1396366518_bYays1.jpg"
|
598
598
|
```
|
data/bin/wechat
CHANGED
@@ -50,8 +50,8 @@ class App < Thor
|
|
50
50
|
end
|
51
51
|
|
52
52
|
desc 'department_update [DEPARTMENT_ID, NAME]', '更新部门'
|
53
|
-
method_option :parentid, aliases: '-p', desc: '父亲部门id。根部门id为1', default: nil
|
54
|
-
method_option :order, aliases: '-o', desc: '在父部门中的次序值。order值小的排序靠前。', default: nil
|
53
|
+
method_option :parentid, aliases: '-p', desc: '父亲部门id。根部门id为1', default: nil, check_default_type: false
|
54
|
+
method_option :order, aliases: '-o', desc: '在父部门中的次序值。order值小的排序靠前。', default: nil, check_default_type: false
|
55
55
|
def department_update(departmentid, name)
|
56
56
|
api_opts = options.slice(:parentid, :order)
|
57
57
|
puts wechat_api.department_update(departmentid, name, api_opts[:parentid], api_opts[:order])
|
@@ -97,8 +97,8 @@ class App < Thor
|
|
97
97
|
end
|
98
98
|
|
99
99
|
desc 'user_simplelist [DEPARTMENT_ID]', '获取部门成员'
|
100
|
-
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 1
|
101
|
-
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0
|
100
|
+
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 1, check_default_type: false
|
101
|
+
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0, check_default_type: false
|
102
102
|
def user_simplelist(departmentid = 1)
|
103
103
|
api_opts = options.slice(:fetch_child, :status)
|
104
104
|
|
@@ -111,8 +111,8 @@ class App < Thor
|
|
111
111
|
end
|
112
112
|
|
113
113
|
desc 'user_list [DEPARTMENT_ID]', '获取部门成员详情'
|
114
|
-
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 0
|
115
|
-
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0
|
114
|
+
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 0, check_default_type: false
|
115
|
+
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0, check_default_type: false
|
116
116
|
def user_list(departmentid = 1)
|
117
117
|
api_opts = options.slice(:fetch_child, :status)
|
118
118
|
|
@@ -232,10 +232,45 @@ class App < Thor
|
|
232
232
|
|
233
233
|
desc 'media_uploadnews [MPNEWS_YAML_PATH]', '上传图文消息素材'
|
234
234
|
def media_uploadnews(mpnews_yaml_path)
|
235
|
-
mpnew =
|
235
|
+
mpnew = Wechat::ApiLoader.load_yaml(File.read(mpnews_yaml_path))
|
236
236
|
puts wechat_api.media_uploadnews(Wechat::Message.new(MsgType: 'uploadnews').mpnews(mpnew[:articles]))
|
237
237
|
end
|
238
238
|
|
239
|
+
desc 'draft_add [ARTICLE_YAML_PATH]', '新建草稿'
|
240
|
+
def draft_add(article_yaml_path)
|
241
|
+
yml_hash = Wechat::ApiLoader.load_yaml(File.read(article_yaml_path))
|
242
|
+
puts wechat_api.draft_add(Wechat::Message.new(MsgType: 'draft_news').draft_news(yml_hash[:articles]))
|
243
|
+
end
|
244
|
+
|
245
|
+
desc 'draft_get [MEDIA_ID]', '获取草稿'
|
246
|
+
def draft_get(media_id)
|
247
|
+
puts wechat_api.draft_get(media_id)
|
248
|
+
end
|
249
|
+
|
250
|
+
desc 'draft_delete [MEDIA_ID]', '删除草稿'
|
251
|
+
def draft_delete(media_id)
|
252
|
+
puts wechat_api.draft_delete(media_id)
|
253
|
+
end
|
254
|
+
|
255
|
+
desc 'draft_count', '获取草稿总数'
|
256
|
+
def draft_count
|
257
|
+
puts wechat_api.draft_count
|
258
|
+
end
|
259
|
+
|
260
|
+
desc 'draft_batchget [OFFSET, COUNT]', '获取草稿列表'
|
261
|
+
method_option :no_content, aliases: '-no_content', desc: '不要返回 content 字段', default: true, check_default_type: false
|
262
|
+
def draft_batchget(offset, count)
|
263
|
+
api_opts = options.slice(:no_content)
|
264
|
+
wechat_api.draft_batchget(offset, count, no_content: api_opts[:no_content])
|
265
|
+
end
|
266
|
+
|
267
|
+
desc 'draft_switch', '检查草稿箱和发布功能开关状态'
|
268
|
+
method_option :enable_draft, aliases: '-enable_draft', desc: '立刻开启草稿箱和发布功能', default: false, check_default_type: false
|
269
|
+
def draft_switch
|
270
|
+
api_opts = options.slice(:enable_draft)
|
271
|
+
puts wechat_api.draft_switch(checkonly: api_opts[:enable_draft] || false)
|
272
|
+
end
|
273
|
+
|
239
274
|
desc 'message_mass_delete [MSG_ID]', '删除群发消息'
|
240
275
|
def message_mass_delete(msg_id)
|
241
276
|
puts wechat_api.message_mass_delete(msg_id)
|
@@ -349,13 +384,13 @@ class App < Thor
|
|
349
384
|
|
350
385
|
desc 'menu_create [MENU_YAML_PATH]', '创建菜单'
|
351
386
|
def menu_create(menu_yaml_path)
|
352
|
-
menu =
|
387
|
+
menu = Wechat::ApiLoader.load_yaml(File.read(menu_yaml_path))
|
353
388
|
puts 'Menu created' if wechat_api.menu_create(menu)
|
354
389
|
end
|
355
390
|
|
356
391
|
desc 'menu_addconditional [CONDITIONAL_MENU_YAML_PATH]', '创建个性化菜单'
|
357
392
|
def menu_addconditional(conditional_menu_yaml_path)
|
358
|
-
conditional_menu =
|
393
|
+
conditional_menu = Wechat::ApiLoader.load_yaml(File.read(conditional_menu_yaml_path))
|
359
394
|
add_result = wechat_api.menu_addconditional(conditional_menu)
|
360
395
|
puts "Conditional menu created: #{add_result}" if add_result
|
361
396
|
end
|
@@ -408,7 +443,7 @@ class App < Thor
|
|
408
443
|
|
409
444
|
desc 'material_add_news [MPNEWS_YAML_PATH]', '永久图文素材上传'
|
410
445
|
def material_add_news(mpnews_yaml_path)
|
411
|
-
new =
|
446
|
+
new = Wechat::ApiLoader.load_yaml(File.read(mpnews_yaml_path))
|
412
447
|
puts wechat_api.material_add_news(Wechat::Message.new(MsgType: 'mpnews').mpnews(new['articles']))
|
413
448
|
end
|
414
449
|
|
@@ -479,13 +514,13 @@ class App < Thor
|
|
479
514
|
|
480
515
|
desc 'custom_news [OPENID, NEWS_YAML_PATH]', '发送图文客服消息'
|
481
516
|
def custom_news(openid, news_yaml_path)
|
482
|
-
articles =
|
517
|
+
articles = Wechat::ApiLoader.load_yaml(File.read(news_yaml_path))
|
483
518
|
puts wechat_api.custom_message_send Wechat::Message.to(openid).news(articles['articles'])
|
484
519
|
end
|
485
520
|
|
486
521
|
desc 'template_message [OPENID, TEMPLATE_YAML_PATH]', '模板消息接口'
|
487
522
|
def template_message(openid, template_yaml_path)
|
488
|
-
template =
|
523
|
+
template = Wechat::ApiLoader.load_yaml(File.read(template_yaml_path))
|
489
524
|
puts wechat_api.template_message_send Wechat::Message.to(openid).template(template['template'])
|
490
525
|
end
|
491
526
|
|
@@ -556,6 +591,10 @@ class App < Thor
|
|
556
591
|
def clear_quota
|
557
592
|
puts wechat_api.clear_quota
|
558
593
|
end
|
594
|
+
|
595
|
+
def self.exit_on_failure?
|
596
|
+
true
|
597
|
+
end
|
559
598
|
end
|
560
599
|
|
561
600
|
App.start
|
@@ -38,29 +38,36 @@ module ActionController
|
|
38
38
|
self.trusted_domain_fullname = opts[:trusted_domain_fullname] || cfg.trusted_domain_fullname
|
39
39
|
self.oauth2_cookie_duration = opts[:oauth2_cookie_duration] || cfg.oauth2_cookie_duration.to_i.seconds
|
40
40
|
self.timeout = opts[:timeout] || cfg.timeout
|
41
|
-
self.qcloud_token_lifespan = opts[:qcloud_token_lifespan] || cfg.qcloud_token_lifespan
|
42
41
|
self.skip_verify_ssl = opts.key?(:skip_verify_ssl) ? opts[:skip_verify_ssl] : cfg.skip_verify_ssl
|
43
42
|
|
43
|
+
proxy_url = opts.key?(:proxy_url) ? opts[:proxy_url] : cfg.proxy_url
|
44
|
+
proxy_port = opts.key?(:proxy_port) ? opts[:proxy_port] : cfg.proxy_port
|
45
|
+
proxy_username = opts.key?(:proxy_username) ? opts[:proxy_username] : cfg.proxy_username
|
46
|
+
proxy_password = opts.key?(:proxy_password) ? opts[:proxy_password] : cfg.proxy_password
|
47
|
+
|
44
48
|
return Wechat.api if account == :default && opts.empty?
|
45
49
|
|
46
50
|
access_token = opts[:access_token] || cfg.access_token
|
47
51
|
jsapi_ticket = opts[:jsapi_ticket] || cfg.jsapi_ticket
|
48
52
|
qcloud_env = opts[:qcloud_env] || cfg.qcloud_env
|
49
53
|
qcloud_token = opts[:qcloud_token] || cfg.qcloud_token
|
54
|
+
qcloud_token_lifespan = opts[:qcloud_token_lifespan] || cfg.qcloud_token_lifespan
|
50
55
|
|
51
56
|
api_type = opts[:type] || cfg.type
|
52
57
|
secret = corpid.present? ? opts[:corpsecret] || cfg.corpsecret : opts[:secret] || cfg.secret
|
53
58
|
|
54
|
-
|
59
|
+
network_setting = Wechat::NetworkSetting.new(timeout, skip_verify_ssl, proxy_url, proxy_port, proxy_username, proxy_password)
|
60
|
+
qcloud_setting = Wechat::Qcloud::Setting.new(qcloud_env, qcloud_token, qcloud_token_lifespan)
|
61
|
+
get_wechat_api(api_type, corpid, appid, secret, access_token, agentid, network_setting, jsapi_ticket, qcloud_setting)
|
55
62
|
end
|
56
63
|
|
57
|
-
def get_wechat_api(api_type, corpid, appid, secret, access_token, agentid,
|
64
|
+
def get_wechat_api(api_type, corpid, appid, secret, access_token, agentid, network_setting, jsapi_ticket, qcloud_setting)
|
58
65
|
if api_type && api_type.to_sym == :mp
|
59
|
-
Wechat::MpApi.new(appid, secret, access_token,
|
66
|
+
Wechat::MpApi.new(appid, secret, access_token, network_setting, jsapi_ticket, qcloud_setting)
|
60
67
|
elsif corpid.present?
|
61
|
-
Wechat::CorpApi.new(corpid, secret, access_token, agentid,
|
68
|
+
Wechat::CorpApi.new(corpid, secret, access_token, agentid, network_setting, jsapi_ticket)
|
62
69
|
else
|
63
|
-
Wechat::Api.new(appid, secret, access_token,
|
70
|
+
Wechat::Api.new(appid, secret, access_token, network_setting, jsapi_ticket)
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
data/lib/wechat/api.rb
CHANGED
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
module Wechat
|
4
4
|
class Api < ApiBase
|
5
|
-
def initialize(appid, secret, token_file,
|
5
|
+
def initialize(appid, secret, token_file, network_setting, jsapi_ticket_file)
|
6
6
|
super()
|
7
|
-
@client = HttpClient.new(Wechat::Api::API_BASE,
|
7
|
+
@client = HttpClient.new(Wechat::Api::API_BASE, network_setting)
|
8
8
|
@access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
|
9
9
|
@jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
10
10
|
@qcloud = nil
|
11
11
|
end
|
12
12
|
|
13
13
|
include Concern::Common
|
14
|
+
include Concern::Draft
|
14
15
|
|
15
16
|
def template_message_send(message)
|
16
17
|
post 'message/template/send', message.to_json, content_type: :json
|
data/lib/wechat/api_loader.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
|
3
5
|
module Wechat
|
4
6
|
module ApiLoader
|
5
7
|
def self.with(options)
|
@@ -10,17 +12,19 @@ module Wechat
|
|
10
12
|
js_token_file = options[:js_token_file] || c.jsapi_ticket.presence || '/var/tmp/wechat_jsapi_ticket'
|
11
13
|
type = options[:type] || c.type
|
12
14
|
|
15
|
+
network_setting = Wechat::NetworkSetting.new(c.timeout, c.skip_verify_ssl, c.proxy_url, c.proxy_port, c.proxy_username, c.proxy_password)
|
13
16
|
if c.appid && c.secret && token_file.present?
|
14
17
|
if type == 'mp'
|
15
18
|
qcloud_env = options[:qcloud_env] || c.qcloud_env
|
16
19
|
qcloud_token_file = options[:qcloud_token_file] || c.qcloud_token_file.presence || '/var/tmp/qcloud_access_token'
|
17
20
|
qcloud_token_lifespan = options[:qcloud_token_lifespan] || c.qcloud_token_lifespan
|
18
|
-
Wechat::
|
21
|
+
qcloud_setting = Wechat::Qcloud::Setting.new(qcloud_env, qcloud_token_file, qcloud_token_lifespan)
|
22
|
+
Wechat::MpApi.new(c.appid, c.secret, token_file, network_setting, js_token_file, qcloud_setting)
|
19
23
|
else
|
20
|
-
Wechat::Api.new(c.appid, c.secret, token_file,
|
24
|
+
Wechat::Api.new(c.appid, c.secret, token_file, network_setting, js_token_file)
|
21
25
|
end
|
22
26
|
elsif c.corpid && c.corpsecret && token_file.present?
|
23
|
-
Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid,
|
27
|
+
Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid, network_setting, js_token_file)
|
24
28
|
else
|
25
29
|
raise 'Need create ~/.wechat.yml with wechat appid and secret or running at rails root folder so wechat can read config/wechat.yml'
|
26
30
|
end
|
@@ -38,15 +42,27 @@ module Wechat
|
|
38
42
|
@configs = loading_config!
|
39
43
|
end
|
40
44
|
|
45
|
+
def self.load_yaml(result)
|
46
|
+
YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(result) : YAML.safe_load(result)
|
47
|
+
end
|
48
|
+
|
41
49
|
private_class_method def self.loading_config!
|
42
50
|
configs = config_from_file || config_from_environment
|
43
51
|
configs.merge!(config_from_db)
|
44
52
|
|
45
|
-
configs.
|
53
|
+
configs.transform_keys! do |key|
|
54
|
+
key.to_sym
|
55
|
+
rescue StandardError
|
56
|
+
key
|
57
|
+
end
|
46
58
|
configs.each do |key, cfg|
|
47
59
|
raise "wrong wechat configuration format for #{key}" unless cfg.is_a?(Hash)
|
48
60
|
|
49
|
-
cfg.
|
61
|
+
cfg.transform_keys! do |sub_key|
|
62
|
+
sub_key.to_sym
|
63
|
+
rescue StandardError
|
64
|
+
sub_key
|
65
|
+
end
|
50
66
|
end
|
51
67
|
|
52
68
|
if defined?(::Rails)
|
@@ -60,7 +76,7 @@ module Wechat
|
|
60
76
|
configs.each do |_, cfg|
|
61
77
|
cfg[:timeout] ||= 20
|
62
78
|
cfg[:qcloud_token_lifespan] ||= 7200
|
63
|
-
cfg[:have_session_class]
|
79
|
+
cfg[:have_session_class] ||= class_exists?('WechatSession')
|
64
80
|
cfg[:oauth2_cookie_duration] ||= 3600 # 1 hour
|
65
81
|
end
|
66
82
|
|
@@ -75,27 +91,28 @@ module Wechat
|
|
75
91
|
private_class_method def self.config_from_db
|
76
92
|
return {} unless class_exists?('WechatConfig')
|
77
93
|
|
78
|
-
environment = defined?(::Rails) ? Rails.env.to_s : ENV
|
94
|
+
environment = defined?(::Rails) ? Rails.env.to_s : ENV.fetch('RAILS_ENV', 'development')
|
79
95
|
WechatConfig.get_all_configs(environment)
|
80
96
|
end
|
81
97
|
|
82
98
|
private_class_method def self.config_from_file
|
83
99
|
if defined?(::Rails)
|
84
|
-
config_file = ENV
|
100
|
+
config_file = ENV.fetch('WECHAT_CONF_FILE') { Rails.root.join('config', 'wechat.yml') }
|
85
101
|
resolve_config_file(config_file, Rails.env.to_s)
|
86
102
|
else
|
87
|
-
|
103
|
+
require 'erb'
|
104
|
+
rails_config_file = ENV.fetch('WECHAT_CONF_FILE') { File.join(Dir.getwd, 'config', 'wechat.yml') }
|
88
105
|
application_config_file = File.join(Dir.getwd, 'config', 'application.yml')
|
89
106
|
home_config_file = File.join(Dir.home, '.wechat.yml')
|
90
107
|
if File.exist?(rails_config_file)
|
91
|
-
rails_env = ENV
|
108
|
+
rails_env = ENV.fetch('RAILS_ENV', 'development')
|
92
109
|
if File.exist?(application_config_file) && !defined?(::Figaro)
|
93
110
|
require 'figaro'
|
94
111
|
Figaro::Application.new(path: application_config_file, environment: rails_env).load
|
95
112
|
end
|
96
113
|
config = resolve_config_file(rails_config_file, rails_env)
|
97
114
|
if config.present? && (default = config[:default]) && (default['appid'] || default['corpid'])
|
98
|
-
puts "Using rails project #{ENV
|
115
|
+
puts "Using rails project #{ENV.fetch('WECHAT_CONF_FILE', 'config/wechat.yml')} #{rails_env} setting..."
|
99
116
|
return config
|
100
117
|
end
|
101
118
|
end
|
@@ -107,9 +124,7 @@ module Wechat
|
|
107
124
|
return unless File.exist?(config_file)
|
108
125
|
|
109
126
|
begin
|
110
|
-
|
111
|
-
raw_data = YAML.load(ERB.new(File.read(config_file)).result)
|
112
|
-
# rubocop:enable Security/YAMLLoad
|
127
|
+
raw_data = load_yaml(ERB.new(File.read(config_file)).result)
|
113
128
|
rescue NameError
|
114
129
|
puts "WARNING: If using 'Rails.application.credentials.wechat_secret!' in wechat.yml, you need run in 'rails c' and access via 'Wechat.api' or gem 'figaro' instead."
|
115
130
|
end
|
@@ -132,22 +147,26 @@ module Wechat
|
|
132
147
|
end
|
133
148
|
|
134
149
|
private_class_method def self.config_from_environment
|
135
|
-
value = { appid: ENV
|
136
|
-
secret: ENV
|
137
|
-
corpid: ENV
|
138
|
-
corpsecret: ENV
|
139
|
-
agentid: ENV
|
140
|
-
token: ENV
|
141
|
-
access_token: ENV
|
142
|
-
encrypt_mode: ENV
|
143
|
-
timeout: ENV
|
144
|
-
skip_verify_ssl: ENV
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
150
|
+
value = { appid: ENV.fetch('WECHAT_APPID', nil),
|
151
|
+
secret: ENV.fetch('WECHAT_SECRET', nil),
|
152
|
+
corpid: ENV.fetch('WECHAT_CORPID', nil),
|
153
|
+
corpsecret: ENV.fetch('WECHAT_CORPSECRET', nil),
|
154
|
+
agentid: ENV.fetch('WECHAT_AGENTID', nil),
|
155
|
+
token: ENV.fetch('WECHAT_TOKEN', nil),
|
156
|
+
access_token: ENV.fetch('WECHAT_ACCESS_TOKEN', nil),
|
157
|
+
encrypt_mode: ENV.fetch('WECHAT_ENCRYPT_MODE', nil),
|
158
|
+
timeout: ENV.fetch('WECHAT_TIMEOUT', nil),
|
159
|
+
skip_verify_ssl: ENV.fetch('WECHAT_SKIP_VERIFY_SSL', nil),
|
160
|
+
proxy_url: ENV.fetch('WECHAT_PROXY_URL', nil),
|
161
|
+
proxy_port: ENV.fetch('WECHAT_PROXY_PORT', nil),
|
162
|
+
proxy_username: ENV.fetch('WECHAT_PROXY_USERNAME', nil),
|
163
|
+
proxy_password: ENV.fetch('WECHAT_PROXY_PASSWORD', nil),
|
164
|
+
encoding_aes_key: ENV.fetch('WECHAT_ENCODING_AES_KEY', nil),
|
165
|
+
jsapi_ticket: ENV.fetch('WECHAT_JSAPI_TICKET', nil),
|
166
|
+
qcloud_env: ENV.fetch('WECHAT_QCLOUD_ENV', nil),
|
167
|
+
qcloud_token_file: ENV.fetch('WECHAT_QCLOUD_TOKEN', nil),
|
168
|
+
qcloud_token_lifespan: ENV.fetch('WECHAT_QCLOUD_TOKEN_LIFESPAN', nil),
|
169
|
+
trusted_domain_fullname: ENV.fetch('WECHAT_TRUSTED_DOMAIN_FULLNAME', nil) }
|
151
170
|
{ default: value }
|
152
171
|
end
|
153
172
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
module Concern
|
5
|
+
module Draft
|
6
|
+
def draft_add(mpnews_articles)
|
7
|
+
draft_add_params_hash = { articles: mpnews_articles }
|
8
|
+
post 'draft/add', JSON.generate(draft_add_params_hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
def draft_get(media_id)
|
12
|
+
post 'draft/get', JSON.generate(media_id: media_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def draft_delete(media_id)
|
16
|
+
post 'draft/delete', JSON.generate(media_id: media_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def draft_update(media_id, mpnews_articles, index: 0)
|
20
|
+
draft_update_params_hash = { media_id: media_id,
|
21
|
+
index: index,
|
22
|
+
articles: mpnews_articles }
|
23
|
+
post 'draft/update', JSON.generate(draft_update_params_hash)
|
24
|
+
end
|
25
|
+
|
26
|
+
def draft_count
|
27
|
+
get 'draft/count'
|
28
|
+
end
|
29
|
+
|
30
|
+
def draft_batchget(offset, count, no_content: false)
|
31
|
+
draft_batchget_params_hash = { offset: offset,
|
32
|
+
count: count,
|
33
|
+
no_content: (no_content ? 1 : 0) }
|
34
|
+
post 'draft/batchget', JSON.generate(draft_batchget_params_hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
def draft_switch(checkonly: true)
|
38
|
+
post 'draft/switch', nil, params: { checkonly: (checkonly ? 1 : 0) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -5,7 +5,7 @@ module Wechat
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
-
attr_accessor :wechat_api_client, :wechat_cfg_account, :token, :appid, :corpid, :agentid, :encrypt_mode, :timeout,
|
8
|
+
attr_accessor :wechat_api_client, :wechat_cfg_account, :token, :appid, :corpid, :agentid, :encrypt_mode, :timeout,
|
9
9
|
:skip_verify_ssl, :encoding_aes_key, :trusted_domain_fullname, :oauth2_cookie_duration
|
10
10
|
end
|
11
11
|
|
data/lib/wechat/corp_api.rb
CHANGED
@@ -4,9 +4,9 @@ module Wechat
|
|
4
4
|
class CorpApi < ApiBase
|
5
5
|
attr_reader :agentid
|
6
6
|
|
7
|
-
def initialize(appid, secret, token_file, agentid,
|
7
|
+
def initialize(appid, secret, token_file, agentid, network_setting, jsapi_ticket_file)
|
8
8
|
super()
|
9
|
-
@client = HttpClient.new(QYAPI_BASE,
|
9
|
+
@client = HttpClient.new(QYAPI_BASE, network_setting)
|
10
10
|
@access_token = Token::CorpAccessToken.new(@client, appid, secret, token_file)
|
11
11
|
@agentid = agentid
|
12
12
|
@jsapi_ticket = Ticket::CorpJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
@@ -143,7 +143,7 @@ module Wechat
|
|
143
143
|
end
|
144
144
|
|
145
145
|
def menu_create(menu)
|
146
|
-
# 微信不接受7bit escaped json(eg \uxxxx)
|
146
|
+
# 微信不接受 7bit escaped json(eg \uxxxx),中文必须 UTF-8 编码,这可能是个安全漏洞
|
147
147
|
post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
|
148
148
|
end
|
149
149
|
|
data/lib/wechat/http_client.rb
CHANGED
@@ -6,16 +6,21 @@ module Wechat
|
|
6
6
|
class HttpClient
|
7
7
|
attr_reader :base, :ssl_context, :httprb
|
8
8
|
|
9
|
-
def initialize(base,
|
9
|
+
def initialize(base, network_setting)
|
10
10
|
@base = base
|
11
11
|
@httprb = if HTTP::VERSION.to_i >= 4
|
12
|
-
HTTP.timeout(write: timeout, connect: timeout, read: timeout)
|
12
|
+
HTTP.timeout(write: network_setting.timeout, connect: network_setting.timeout, read: network_setting.timeout)
|
13
13
|
else
|
14
|
-
HTTP.timeout(:global, write: timeout, connect: timeout, read: timeout)
|
14
|
+
HTTP.timeout(:global, write: network_setting.timeout, connect: network_setting.timeout, read: network_setting.timeout)
|
15
15
|
end
|
16
|
+
|
17
|
+
unless network_setting.proxy_url.nil?
|
18
|
+
@httprb = @httprb.via(network_setting.proxy_url, network_setting.proxy_port.to_i, network_setting.proxy_username, network_setting.proxy_password)
|
19
|
+
end
|
20
|
+
|
16
21
|
@ssl_context = OpenSSL::SSL::SSLContext.new
|
17
22
|
@ssl_context.ssl_version = 'TLSv1_2'
|
18
|
-
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE if skip_verify_ssl
|
23
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE if network_setting.skip_verify_ssl
|
19
24
|
end
|
20
25
|
|
21
26
|
def get(path, get_header = {})
|
data/lib/wechat/message.rb
CHANGED
@@ -33,7 +33,6 @@ module Wechat
|
|
33
33
|
class ArticleBuilder
|
34
34
|
attr_reader :items
|
35
35
|
|
36
|
-
delegate :count, to: :items
|
37
36
|
def initialize
|
38
37
|
@items = []
|
39
38
|
end
|
@@ -203,6 +202,10 @@ module Wechat
|
|
203
202
|
update(MsgType: 'template', Template: template_fields)
|
204
203
|
end
|
205
204
|
|
205
|
+
def draft_news(collection)
|
206
|
+
update(MsgType: 'draft_news', Articles: collection)
|
207
|
+
end
|
208
|
+
|
206
209
|
def to_xml
|
207
210
|
ws = message_hash.delete(:WechatSession)
|
208
211
|
xml = message_hash.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
@@ -244,6 +247,8 @@ module Wechat
|
|
244
247
|
json_hash['news'] = { 'articles' => json_hash.delete('articles') }
|
245
248
|
when 'mpnews'
|
246
249
|
json_hash = { 'articles' => json_hash['articles'] }
|
250
|
+
when 'draft_news'
|
251
|
+
json_hash = json_hash['articles']
|
247
252
|
when 'ref_mpnews'
|
248
253
|
json_hash['msgtype'] = 'mpnews'
|
249
254
|
json_hash.delete('articles')
|
data/lib/wechat/mp_api.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
module Wechat
|
4
4
|
class MpApi < ApiBase
|
5
|
-
def initialize(appid, secret, token_file,
|
5
|
+
def initialize(appid, secret, token_file, network_setting, jsapi_ticket_file, qcloud_setting)
|
6
6
|
super()
|
7
|
-
@client = HttpClient.new(Wechat::Api::API_BASE,
|
7
|
+
@client = HttpClient.new(Wechat::Api::API_BASE, network_setting)
|
8
8
|
@access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
|
9
9
|
@jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
10
|
-
@qcloud = Qcloud::Token.new(@client, @access_token,
|
10
|
+
@qcloud = Qcloud::Token.new(@client, @access_token, qcloud_setting)
|
11
11
|
end
|
12
12
|
|
13
13
|
include Concern::Common
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
class NetworkSetting
|
5
|
+
attr_reader :timeout, :skip_verify_ssl, :proxy_url, :proxy_port, :proxy_username, :proxy_password
|
6
|
+
|
7
|
+
def initialize(timeout, skip_verify_ssl, proxy_url, proxy_port, proxy_username, proxy_password)
|
8
|
+
@timeout = timeout
|
9
|
+
@skip_verify_ssl = skip_verify_ssl
|
10
|
+
@proxy_url = proxy_url
|
11
|
+
@proxy_port = proxy_port
|
12
|
+
@proxy_username = proxy_username
|
13
|
+
@proxy_password = proxy_password
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
module Qcloud
|
5
|
+
class Setting
|
6
|
+
attr_reader :qcloud_env, :qcloud_token, :qcloud_token_lifespan
|
7
|
+
|
8
|
+
def initialize(qcloud_env, qcloud_token, qcloud_token_lifespan)
|
9
|
+
@qcloud_env = qcloud_env
|
10
|
+
@qcloud_token = qcloud_token
|
11
|
+
@qcloud_token_lifespan = qcloud_token_lifespan
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/wechat/qcloud/token.rb
CHANGED
@@ -10,12 +10,12 @@ module Wechat
|
|
10
10
|
class Token
|
11
11
|
attr_reader :client, :access_token, :qcloud_env, :qcloud_token_file, :qcloud_token_lifespan, :qcloud_token, :qcloud_token_expired_time
|
12
12
|
|
13
|
-
def initialize(client, access_token,
|
13
|
+
def initialize(client, access_token, qcloud_setting)
|
14
14
|
@client = client
|
15
15
|
@access_token = access_token
|
16
|
-
@qcloud_env = qcloud_env
|
17
|
-
@qcloud_token_file =
|
18
|
-
@qcloud_token_lifespan =
|
16
|
+
@qcloud_env = qcloud_setting.qcloud_env
|
17
|
+
@qcloud_token_file = qcloud_setting.qcloud_token
|
18
|
+
@qcloud_token_lifespan = qcloud_setting.qcloud_token_lifespan
|
19
19
|
@random_generator = Random.new
|
20
20
|
end
|
21
21
|
|
data/lib/wechat/responder.rb
CHANGED
@@ -187,7 +187,7 @@ module Wechat
|
|
187
187
|
end
|
188
188
|
|
189
189
|
def create
|
190
|
-
request_msg = Wechat::Message.from_hash(
|
190
|
+
request_msg = Wechat::Message.from_hash(post_body)
|
191
191
|
response_msg = run_responder(request_msg)
|
192
192
|
|
193
193
|
if response_msg.respond_to? :to_xml
|
@@ -229,6 +229,24 @@ module Wechat
|
|
229
229
|
msg_encrypt)
|
230
230
|
end
|
231
231
|
|
232
|
+
def post_body
|
233
|
+
if request.media_type == 'application/json'
|
234
|
+
data_hash = params
|
235
|
+
|
236
|
+
if @we_encrypt_mode && data['Encrypt'].present?
|
237
|
+
content, @we_app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), @we_encoding_aes_key))
|
238
|
+
data_hash = content
|
239
|
+
end
|
240
|
+
|
241
|
+
data_hash = data_hash.to_unsafe_hash if data_hash.instance_of?(ActionController::Parameters)
|
242
|
+
HashWithIndifferentAccess.new(data_hash).tap do |msg|
|
243
|
+
msg[:Event]&.downcase!
|
244
|
+
end
|
245
|
+
else
|
246
|
+
post_xml
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
232
250
|
def post_xml
|
233
251
|
data = request_content
|
234
252
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wechat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Skinnyworm
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
ZM9IDtdMg8E/4ujwilV8HKmgU77vVN6vSMvxx8zQFSz9a6GbdpB4egPZ++peSk/Q
|
36
36
|
uaIJtOX6M4VC6u7eZfotARKyUy6EcoN2zNqEAQ==
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date:
|
38
|
+
date: 2022-06-06 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: activesupport
|
@@ -44,9 +44,6 @@ dependencies:
|
|
44
44
|
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '6.0'
|
47
|
-
- - "<"
|
48
|
-
- !ruby/object:Gem::Version
|
49
|
-
version: '7'
|
50
47
|
type: :runtime
|
51
48
|
prerelease: false
|
52
49
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -54,9 +51,6 @@ dependencies:
|
|
54
51
|
- - ">="
|
55
52
|
- !ruby/object:Gem::Version
|
56
53
|
version: '6.0'
|
57
|
-
- - "<"
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: '7'
|
60
54
|
- !ruby/object:Gem::Dependency
|
61
55
|
name: http
|
62
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -91,6 +85,20 @@ dependencies:
|
|
91
85
|
- - ">="
|
92
86
|
- !ruby/object:Gem::Version
|
93
87
|
version: 1.6.0
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: psych
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 3.3.2
|
95
|
+
type: :runtime
|
96
|
+
prerelease: false
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.3.2
|
94
102
|
- !ruby/object:Gem::Dependency
|
95
103
|
name: thor
|
96
104
|
requirement: !ruby/object:Gem::Requirement
|
@@ -153,28 +161,42 @@ dependencies:
|
|
153
161
|
requirements:
|
154
162
|
- - ">="
|
155
163
|
- !ruby/object:Gem::Version
|
156
|
-
version:
|
164
|
+
version: 7.0.0
|
157
165
|
type: :development
|
158
166
|
prerelease: false
|
159
167
|
version_requirements: !ruby/object:Gem::Requirement
|
160
168
|
requirements:
|
161
169
|
- - ">="
|
162
170
|
- !ruby/object:Gem::Version
|
163
|
-
version:
|
171
|
+
version: 7.0.0
|
164
172
|
- !ruby/object:Gem::Dependency
|
165
173
|
name: rspec-rails
|
166
174
|
requirement: !ruby/object:Gem::Requirement
|
167
175
|
requirements:
|
168
176
|
- - "~>"
|
169
177
|
- !ruby/object:Gem::Version
|
170
|
-
version: '5.
|
178
|
+
version: '5.1'
|
171
179
|
type: :development
|
172
180
|
prerelease: false
|
173
181
|
version_requirements: !ruby/object:Gem::Requirement
|
174
182
|
requirements:
|
175
183
|
- - "~>"
|
176
184
|
- !ruby/object:Gem::Version
|
177
|
-
version: '5.
|
185
|
+
version: '5.1'
|
186
|
+
- !ruby/object:Gem::Dependency
|
187
|
+
name: rspec-mocks
|
188
|
+
requirement: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - '='
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: 3.10.2
|
193
|
+
type: :development
|
194
|
+
prerelease: false
|
195
|
+
version_requirements: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - '='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 3.10.2
|
178
200
|
- !ruby/object:Gem::Dependency
|
179
201
|
name: sqlite3
|
180
202
|
requirement: !ruby/object:Gem::Requirement
|
@@ -223,6 +245,7 @@ files:
|
|
223
245
|
- lib/wechat/api_loader.rb
|
224
246
|
- lib/wechat/cipher.rb
|
225
247
|
- lib/wechat/concern/common.rb
|
248
|
+
- lib/wechat/concern/draft.rb
|
226
249
|
- lib/wechat/concern/qcloud.rb
|
227
250
|
- lib/wechat/controller_api.rb
|
228
251
|
- lib/wechat/corp_api.rb
|
@@ -230,6 +253,8 @@ files:
|
|
230
253
|
- lib/wechat/http_client.rb
|
231
254
|
- lib/wechat/message.rb
|
232
255
|
- lib/wechat/mp_api.rb
|
256
|
+
- lib/wechat/network_setting.rb
|
257
|
+
- lib/wechat/qcloud/setting.rb
|
233
258
|
- lib/wechat/qcloud/token.rb
|
234
259
|
- lib/wechat/responder.rb
|
235
260
|
- lib/wechat/signature.rb
|
@@ -242,8 +267,18 @@ files:
|
|
242
267
|
homepage: https://github.com/Eric-Guo/wechat
|
243
268
|
licenses:
|
244
269
|
- MIT
|
245
|
-
metadata:
|
246
|
-
|
270
|
+
metadata:
|
271
|
+
bug_tracker_uri: https://github.com/Eric-Guo/wechat/issues
|
272
|
+
changelog_uri: https://github.com/Eric-Guo/wechat/releases
|
273
|
+
documentation_uri: https://github.com/Eric-Guo/wechat/tree/v0.16.0#readme
|
274
|
+
source_code_uri: https://github.com/Eric-Guo/wechat/tree/v0.16.0
|
275
|
+
rubygems_mfa_required: 'true'
|
276
|
+
post_install_message: |-
|
277
|
+
*****WECHAT BREAK CHANGE*****
|
278
|
+
Including correct version of `psych` after upgrade wechat, if not sure, using v3.3.2.
|
279
|
+
Ruby 3.1’s incompatible changes to its YAML module (Psych 4), detail see:
|
280
|
+
https://www.ctrl.blog/entry/ruby-psych4.html
|
281
|
+
*****************************
|
247
282
|
rdoc_options: []
|
248
283
|
require_paths:
|
249
284
|
- lib
|
@@ -256,9 +291,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
256
291
|
requirements:
|
257
292
|
- - ">="
|
258
293
|
- !ruby/object:Gem::Version
|
259
|
-
version:
|
294
|
+
version: 1.8.11
|
260
295
|
requirements: []
|
261
|
-
rubygems_version: 3.
|
296
|
+
rubygems_version: 3.3.15
|
262
297
|
signing_key:
|
263
298
|
specification_version: 4
|
264
299
|
summary: DSL for wechat message handling and API
|
metadata.gz.sig
CHANGED
Binary file
|