wechat 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3e0f3e13da7012b66a0db4454a02588e18b197b0
4
- data.tar.gz: 8f20da9e9e385f3a47f07fdd74857b6be7065ac8
3
+ metadata.gz: 7a76622259e3d0a5ea26bab1dfb80463a0bc6587
4
+ data.tar.gz: 1e2fbf216e23989e7ff3151be9942e552b427199
5
5
  SHA512:
6
- metadata.gz: c248645bc11c2b0973cb2a05a0db3ba0611de7173c2077632884be5097706b7906b7ae48c81e3d5094d628b7850278dc9f64d4851ee976fb8ac4e1ce0fbc70ff
7
- data.tar.gz: fc0758f38f48240b64a74b475802259b98df2606952b2c6fbd7addb89b8dee177243789e5acd83adf76c8e8795864905610821ff6b71e6311a3badd157dd8586
6
+ metadata.gz: 8b5a068b1a5f3846335d693121e47c227bfe8a272f2d44e056d3354b0e873e696ae1588e32f7518aef1fb5e2be8ddafa8d9b271a7690724d526ebf6bde0c4bba
7
+ data.tar.gz: e9ee9effb1a9373b914e023f27b1b82afd5ca688382fa5237fca88b2908a2a582fab995ff40aa4b6fc1dc5748dfe13858bc2c2c65d7dd9a3eeefeb1314904b22
Binary file
data.tar.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- [w�C�Nn��8��&�&���' ����,�XTe��ڂ)Rr� $��/8�
2
- �`��
1
+ |0�����g&�vUkY���������NF�� �,����V67&����,��$�'�fo�����ߔ�pXY�-k�n�.J�!�G+�rv��HDQP*
2
+ G�}a>�$��S��4�՚e��Iք�A�3�Ph�� i2M��n3}.��1g&NK��:ö�? ��U'[���Af�:mJ}i�<��v��c��^x��`�;�����5L���~v+>{.ǡ�:�uWb'[X,���_��Gj��z�W��B��'
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.8.5 (released at 3/14/2017)
4
+
5
+ * Support mass send API #176
6
+ * Support new media_hq API
7
+ * Support new createwxaqrcode API for miniapp
8
+ * Fix wechat_responder not proper injected in rails 5 API #165
9
+ * parse response support XML return, by @zhangbin #167
10
+ * WeChat only allow 8 article per one news, by @kikyous #175
11
+ * Store token at cookies, by @jstdoit #174
12
+
3
13
  ## v0.8.4 (released at 1/12/2017)
4
14
 
5
15
  # Support Ruby 2.4.0
@@ -289,7 +289,7 @@ wechat gems 内部不会检查权限。但因公众号类型不同,和微信
289
289
 
290
290
  ```
291
291
  $ wechat
292
- Wechat commands:
292
+ Wechat Public Account commands:
293
293
  wechat callbackip # 获取微信服务器IP地址
294
294
  wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
295
295
  wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
@@ -308,14 +308,19 @@ Wechat commands:
308
308
  wechat material_delete [MEDIA_ID] # 删除永久素材
309
309
  wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
310
310
  wechat media [MEDIA_ID, PATH] # 媒体下载
311
+ wechat media_hq [MEDIA_ID, PATH] # 高清音频下载
311
312
  wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
312
313
  wechat media_uploadimg [IMAGE_PATH] # 上传图文消息内的图片
314
+ wechat media_uploadnews [MPNEWS_YAML_PATH] # 上传图文消息素材
313
315
  wechat menu # 当前菜单
314
316
  wechat menu_addconditional [CONDITIONAL_MENU_YAML_PATH] # 创建个性化菜单
315
317
  wechat menu_create [MENU_YAML_PATH] # 创建菜单
316
318
  wechat menu_delconditional [MENU_ID] # 删除个性化菜单
317
319
  wechat menu_delete # 删除菜单
318
320
  wechat menu_trymatch [USER_ID] # 测试个性化菜单匹配结果
321
+ wechat message_mass_delete [MSG_ID] # 删除群发消息
322
+ wechat message_mass_get [MSG_ID] # 查询群发消息发送状态
323
+ wechat message_mass_preview [WX_NAME, MPNEWS_MEDIA_ID] # 预览图文消息素材
319
324
  wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
320
325
  wechat qrcode_create_scene [SCENE_ID, EXPIRE_SECONDS] # 请求临时二维码
321
326
  wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
@@ -339,7 +344,7 @@ Wechat commands:
339
344
  #### 企业号命令行
340
345
  ```
341
346
  $ wechat
342
- Wechat commands:
347
+ Wechat Enterprise Account commands:
343
348
  wechat agent [AGENT_ID] # 获取企业号应用详情
344
349
  wechat agent_list # 获取应用概况列表
345
350
  wechat batch_job_result [JOB_ID] # 获取异步任务结果
@@ -580,7 +585,7 @@ class WechatsController < ActionController::Base
580
585
 
581
586
  # 当请求的文字信息内容为'<n>条新闻'时, 使用这个responder处理, 并将n作为第二个参数
582
587
  on :text, with: /^(\d+)条新闻$/ do |request, count|
583
- # 微信最多显示10条新闻,大于10条将只取前10
588
+ # 微信最多显示8条新闻,大于8条将只取前8
584
589
  news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: '新闻标题', content: "第#{n}条新闻的内容#{n.hash}" } }
585
590
  request.reply.news(news) do |article, n, index| # 回复"articles"
586
591
  article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
@@ -687,6 +692,12 @@ class WechatsController < ActionController::Base
687
692
  request.reply.text "job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
688
693
  end
689
694
 
695
+ # 事件推送群发结果
696
+ on :event, with: 'masssendjobfinish' do |request|
697
+ # https://mp.weixin.qq.com/wiki?action=doc&id=mp1481187827_i0l21&t=0.03571905015619936#8
698
+ request.reply.success # request is XML result hash.
699
+ end
700
+
690
701
  # 当无任何responder处理用户信息时,使用这个responder处理
691
702
  on :fallback, respond: 'fallback message'
692
703
  end
data/README.md CHANGED
@@ -304,7 +304,7 @@ Feel safe if you can not read Chinese in the comments, it's kept there in order
304
304
 
305
305
  ```
306
306
  $ wechat
307
- Wechat commands:
307
+ Wechat Public Account commands:
308
308
  wechat callbackip # 获取微信服务器IP地址
309
309
  wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
310
310
  wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
@@ -323,14 +323,19 @@ Wechat commands:
323
323
  wechat material_delete [MEDIA_ID] # 删除永久素材
324
324
  wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
325
325
  wechat media [MEDIA_ID, PATH] # 媒体下载
326
+ wechat media_hq [MEDIA_ID, PATH] # 高清音频下载
326
327
  wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
327
328
  wechat media_uploadimg [IMAGE_PATH] # 上传图文消息内的图片
329
+ wechat media_uploadnews [MPNEWS_YAML_PATH] # 上传图文消息素材
328
330
  wechat menu # 当前菜单
329
331
  wechat menu_addconditional [CONDITIONAL_MENU_YAML_PATH] # 创建个性化菜单
330
332
  wechat menu_create [MENU_YAML_PATH] # 创建菜单
331
333
  wechat menu_delconditional [MENU_ID] # 删除个性化菜单
332
334
  wechat menu_delete # 删除菜单
333
335
  wechat menu_trymatch [USER_ID] # 测试个性化菜单匹配结果
336
+ wechat message_mass_delete [MSG_ID] # 删除群发消息
337
+ wechat message_mass_get [MSG_ID] # 查询群发消息发送状态
338
+ wechat message_mass_preview [WX_NAME, MPNEWS_MEDIA_ID] # 预览图文消息素材
334
339
  wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
335
340
  wechat qrcode_create_scene [SCENE_ID, EXPIRE_SECONDS] # 请求临时二维码
336
341
  wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
@@ -354,7 +359,7 @@ Wechat commands:
354
359
  #### Enterprise account command line
355
360
  ```
356
361
  $ wechat
357
- Wechat commands:
362
+ Wechat Enterprise Account commands:
358
363
  wechat agent [AGENT_ID] # 获取企业号应用详情
359
364
  wechat agent_list # 获取应用概况列表
360
365
  wechat batch_job_result [JOB_ID] # 获取异步任务结果
@@ -594,7 +599,7 @@ class WechatsController < ActionController::Base
594
599
 
595
600
  # When receive '<n>news', will match and will get count as <n> as parameter
596
601
  on :text, with: /^(\d+) news$/ do |request, count|
597
- # Wechat article can only contain max 10 items, large than 10 will be dropped.
602
+ # Wechat article can only contain max 8 items, large than 8 will be dropped.
598
603
  news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: 'News title', content: "No. #{n} news content" } }
599
604
  request.reply.news(news) do |article, n, index| # article is return object
600
605
  article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
@@ -700,6 +705,12 @@ class WechatsController < ActionController::Base
700
705
  request.reply.text "replace_party job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
701
706
  end
702
707
 
708
+ # mass sent job finish result notification
709
+ on :event, with: 'masssendjobfinish' do |request|
710
+ # https://mp.weixin.qq.com/wiki?action=doc&id=mp1481187827_i0l21&t=0.03571905015619936#8
711
+ request.reply.success # request is XML result hash.
712
+ end
713
+
703
714
  # If no match above will fallback to below
704
715
  on :fallback, respond: 'fallback message'
705
716
  end
data/bin/wechat CHANGED
@@ -16,7 +16,6 @@ require 'wechat/api_loader'
16
16
  require 'cgi'
17
17
 
18
18
  class App < Thor
19
- package_name 'Wechat'
20
19
  class_option :account, aliases: '-a', default: :default, desc: 'Name of Wechat account configuration.'
21
20
 
22
21
  attr_reader :wechat_api_client
@@ -42,6 +41,7 @@ class App < Thor
42
41
  in_corp_api_cmd = Wechat::ApiLoader.with(options).is_a?(Wechat::CorpApi)
43
42
 
44
43
  if in_corp_api_cmd
44
+ package_name 'Wechat Enterprise Account'
45
45
  desc 'department_create [NAME, PARENT_ID]', '创建部门'
46
46
  method_option :parentid, aliases: '-p', desc: '父亲部门id。根部门id为1'
47
47
  def department_create(name)
@@ -198,6 +198,28 @@ class App < Thor
198
198
  puts wechat_api.message_send openid, text_message
199
199
  end
200
200
  else
201
+ package_name 'Wechat Public Account'
202
+ desc 'media_uploadnews [MPNEWS_YAML_PATH]', '上传图文消息素材'
203
+ def media_uploadnews(mpnews_yaml_path)
204
+ mpnew = YAML.load(File.read(mpnews_yaml_path))
205
+ puts wechat_api.media_uploadnews(Wechat::Message.new(MsgType: 'uploadnews').mpnews(mpnew[:articles]))
206
+ end
207
+
208
+ desc 'message_mass_delete [MSG_ID]', '删除群发消息'
209
+ def message_mass_delete(msg_id)
210
+ puts wechat_api.message_mass_delete(msg_id)
211
+ end
212
+
213
+ desc 'message_mass_preview [WX_NAME, MPNEWS_MEDIA_ID]', '预览图文消息素材'
214
+ def message_mass_preview(wx_name, mpnews_media_id)
215
+ puts wechat_api.message_mass_preview(Wechat::Message.to(towxname: wx_name).ref_mpnews(mpnews_media_id))
216
+ end
217
+
218
+ desc 'message_mass_get [MSG_ID]', '查询群发消息发送状态'
219
+ def message_mass_get(msg_id)
220
+ puts wechat_api.message_mass_get(msg_id)
221
+ end
222
+
201
223
  desc 'group_create [GROUP_NAME]', '创建分组'
202
224
  def group_create(group_name)
203
225
  puts wechat_api.group_create(group_name)
@@ -309,6 +331,13 @@ class App < Thor
309
331
  puts 'File downloaded'
310
332
  end
311
333
 
334
+ desc 'media_hq [MEDIA_ID, PATH]', '高清音频媒体下载'
335
+ def media_hq(media_id, path)
336
+ tmp_file = wechat_api.media_hq(media_id)
337
+ FileUtils.mv(tmp_file.path, path)
338
+ puts 'File downloaded'
339
+ end
340
+
312
341
  desc 'media_create [MEDIA_TYPE, PATH]', '媒体上传'
313
342
  def media_create(type, path)
314
343
  puts wechat_api.media_create(type, path)
@@ -52,7 +52,8 @@ module ActionController
52
52
  class << Base
53
53
  include WechatResponder
54
54
  end
55
- elsif defined? API
55
+ end
56
+ if defined? API
56
57
  class << API
57
58
  include WechatResponder
58
59
  end
@@ -75,6 +75,26 @@ module Wechat
75
75
  post 'shorturl', JSON.generate(action: 'long2short', long_url: long_url)
76
76
  end
77
77
 
78
+ def message_mass_sendall(message)
79
+ post 'message/mass/sendall', message.to_json
80
+ end
81
+
82
+ def message_mass_delete(msg_id)
83
+ post 'message/mass/delete', JSON.generate(msg_id: msg_id)
84
+ end
85
+
86
+ def message_mass_preview(message)
87
+ post 'message/mass/preview', message.to_json
88
+ end
89
+
90
+ def message_mass_get(msg_id)
91
+ post 'message/mass/get', JSON.generate(msg_id: msg_id)
92
+ end
93
+
94
+ def wxa_create_qrcode(path, width = 430)
95
+ post 'wxaapp/createwxaqrcode', JSON.generate(path: path, width: width)
96
+ end
97
+
78
98
  def menu
79
99
  get 'menu/get'
80
100
  end
@@ -16,6 +16,10 @@ module Wechat
16
16
  get 'media/get', params: { media_id: media_id }, as: :file
17
17
  end
18
18
 
19
+ def media_hq(media_id)
20
+ get 'media/get/jssdk', params: { media_id: media_id }, as: :file
21
+ end
22
+
19
23
  def media_create(type, file)
20
24
  post_file 'media/upload', file, params: { type: type }
21
25
  end
@@ -24,6 +28,10 @@ module Wechat
24
28
  post_file 'media/uploadimg', file
25
29
  end
26
30
 
31
+ def media_uploadnews(mpnews_message)
32
+ post 'media/uploadnews', mpnews_message.to_json
33
+ end
34
+
27
35
  protected
28
36
 
29
37
  def get(path, headers = {})
@@ -36,12 +36,14 @@ module Wechat
36
36
  def wechat_public_oauth2(oauth2_params)
37
37
  openid = cookies.signed_or_encrypted[:we_openid]
38
38
  unionid = cookies.signed_or_encrypted[:we_unionid]
39
+ we_token = cookies.signed_or_encrypted[:we_access_token]
39
40
  if openid.present?
40
- yield openid, { 'openid' => openid, 'unionid' => unionid }
41
+ yield openid, { 'openid' => openid, 'unionid' => unionid, 'access_token' => we_token}
41
42
  elsif params[:code].present? && params[:state] == oauth2_params[:state]
42
43
  access_info = wechat.web_access_token(params[:code])
43
44
  cookies.signed_or_encrypted[:we_openid] = { value: access_info['openid'], expires: self.class.oauth2_cookie_duration.from_now }
44
45
  cookies.signed_or_encrypted[:we_unionid] = { value: access_info['unionid'], expires: self.class.oauth2_cookie_duration.from_now }
46
+ cookies.signed_or_encrypted[:we_access_token] = { value: access_info['access_token'], expires: self.class.oauth2_cookie_duration.from_now }
45
47
  yield access_info['openid'], access_info
46
48
  else
47
49
  redirect_to generate_oauth2_url(oauth2_params)
@@ -42,7 +42,7 @@ module Wechat
42
42
  def request(path, header = {}, &_block)
43
43
  url_base = header.delete(:base) || base
44
44
  as = header.delete(:as)
45
- header['Accept'] = 'application/json'
45
+ header['Accept'] ||= 'application/json'
46
46
  response = yield("#{url_base}#{path}", header)
47
47
 
48
48
  raise "Request not OK, response status #{response.status}" if response.status != 200
@@ -5,8 +5,22 @@ module Wechat
5
5
  new(message_hash)
6
6
  end
7
7
 
8
- def to(to_user)
9
- new(ToUserName: to_user, CreateTime: Time.now.to_i)
8
+ def to(to_users = '', towxname: nil, send_ignore_reprint: 0)
9
+ if towxname.present?
10
+ new(ToWxName: towxname, CreateTime: Time.now.to_i)
11
+ elsif send_ignore_reprint == 1
12
+ new(ToUserName: to_users, CreateTime: Time.now.to_i, send_ignore_reprint: send_ignore_reprint)
13
+ else
14
+ new(ToUserName: to_users, CreateTime: Time.now.to_i)
15
+ end
16
+ end
17
+
18
+ def to_mass(tag_id: nil, send_ignore_reprint: 0)
19
+ if tag_id
20
+ new(filter: { is_to_all: false, tag_id: tag_id }, send_ignore_reprint: send_ignore_reprint)
21
+ else
22
+ new(filter: { is_to_all: true }, send_ignore_reprint: send_ignore_reprint)
23
+ end
10
24
  end
11
25
  end
12
26
 
@@ -16,12 +30,21 @@ module Wechat
16
30
  def initialize
17
31
  @items = []
18
32
  end
33
+ end
19
34
 
35
+ class NewsArticleBuilder < ArticleBuilder
20
36
  def item(title: 'title', description: nil, pic_url: nil, url: nil)
21
37
  items << { Title: title, Description: description, PicUrl: pic_url, Url: url }.reject { |_k, v| v.nil? }
22
38
  end
23
39
  end
24
40
 
41
+ class MpNewsArticleBuilder < ArticleBuilder
42
+ def item(thumb_media_id:, title:, content:, author: nil, content_source_url: nil, digest: nil, show_cover_pic: '0')
43
+ items << { Thumb_Media_ID: thumb_media_id, Author: author, Title: title, ContentSourceUrl: content_source_url,
44
+ Content: content, Digest: digest, ShowCoverPic: show_cover_pic }.reject { |_k, v| v.nil? }
45
+ end
46
+ end
47
+
25
48
  attr_reader :message_hash
26
49
 
27
50
  def initialize(message_hash)
@@ -117,8 +140,8 @@ module Wechat
117
140
 
118
141
  def news(collection, &_block)
119
142
  if block_given?
120
- article = ArticleBuilder.new
121
- collection.take(10).each_with_index { |item, index| yield(article, item, index) }
143
+ article = NewsArticleBuilder.new
144
+ collection.take(8).each_with_index { |item, index| yield(article, item, index) }
122
145
  items = article.items
123
146
  else
124
147
  items = collection.collect do |item|
@@ -130,6 +153,24 @@ module Wechat
130
153
  Articles: items.collect { |item| camelize_hash_keys(item) })
131
154
  end
132
155
 
156
+ def mpnews(collection, &_block)
157
+ if block_given?
158
+ article = MpNewsArticleBuilder.new
159
+ collection.take(8).each_with_index { |item, index| yield(article, item, index) }
160
+ items = article.items
161
+ else
162
+ items = collection.collect do |item|
163
+ camelize_hash_keys(item.symbolize_keys.slice(:thumb_media_id, :title, :content, :author, :content_source_url, :digest, :show_cover_pic).reject { |_k, v| v.nil? })
164
+ end
165
+ end
166
+
167
+ update(MsgType: 'mpnews', Articles: items.collect { |item| camelize_hash_keys(item) })
168
+ end
169
+
170
+ def ref_mpnews(media_id)
171
+ update(MsgType: 'ref_mpnews', MpNews: { MediaId: media_id })
172
+ end
173
+
133
174
  def template(opts = {})
134
175
  template_fields = opts.symbolize_keys.slice(:template_id, :topcolor, :url, :data)
135
176
  update(MsgType: 'template', Template: template_fields)
@@ -144,12 +185,16 @@ module Wechat
144
185
 
145
186
  TO_JSON_KEY_MAP = {
146
187
  'ToUserName' => 'touser',
188
+ 'ToWxName' => 'towxname',
147
189
  'MediaId' => 'media_id',
190
+ 'MpNews' => 'mpnews',
148
191
  'ThumbMediaId' => 'thumb_media_id',
149
- 'TemplateId' => 'template_id'
192
+ 'TemplateId' => 'template_id',
193
+ 'ContentSourceUrl' => 'content_source_url',
194
+ 'ShowCoverPic' => 'show_cover_pic'
150
195
  }.freeze
151
196
 
152
- TO_JSON_ALLOWED = %w(touser msgtype content image voice video file music news articles template agentid).freeze
197
+ TO_JSON_ALLOWED = %w(touser msgtype content image voice video file music news articles template agentid filter send_ignore_reprint mpnews towxname).freeze
153
198
 
154
199
  def to_json
155
200
  keep_camel_case_key = message_hash[:MsgType] == 'template'
@@ -164,6 +209,11 @@ module Wechat
164
209
  json_hash['text'] = { 'content' => json_hash.delete('content') }
165
210
  when 'news'
166
211
  json_hash['news'] = { 'articles' => json_hash.delete('articles') }
212
+ when 'mpnews'
213
+ json_hash = { 'articles' => json_hash['articles'] }
214
+ when 'ref_mpnews'
215
+ json_hash['msgtype'] = 'mpnews'
216
+ json_hash.delete('articles')
167
217
  when 'template'
168
218
  json_hash = { 'touser' => json_hash['touser'] }.merge!(json_hash['template'])
169
219
  end
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.8.4
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skinnyworm
@@ -31,7 +31,7 @@ cert_chain:
31
31
  R5k6Ma92sW8jupX4cqbSu9rntdVQkNRpoHIrfU0MZT0cKsg/D1zMteylxrO3KMsz
32
32
  SPQRv+nrI1J0zevFqb8010heoR8SDyUA0Mm3+Q==
33
33
  -----END CERTIFICATE-----
34
- date: 2017-01-12 00:00:00.000000000 Z
34
+ date: 2017-03-14 00:00:00.000000000 Z
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: activesupport
@@ -206,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
206
  version: '0'
207
207
  requirements: []
208
208
  rubyforge_project:
209
- rubygems_version: 2.6.8
209
+ rubygems_version: 2.6.10
210
210
  signing_key:
211
211
  specification_version: 4
212
212
  summary: DSL for wechat message handling and API
metadata.gz.sig CHANGED
Binary file