Class: WeChat::Bot::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/wechat/bot/client.rb

Overview

微信 API 类

Instance Method Summary collapse

Constructor Details

#initialize(bot) ⇒ Client

Returns a new instance of Client.



10
11
12
13
# File 'lib/wechat/bot/client.rb', line 10

def initialize(bot)
  @bot = bot
  clone!
end

Instance Method Details

#_sendObject



327
# File 'lib/wechat/bot/client.rb', line 327

alias_method :_send, :send

#add_friend(username, status = 2, verify_content = '') ⇒ Object

添加好友

Parameters:

  • status: (Integer)

    2-添加 3-接受



574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/wechat/bot/client.rb', line 574

def add_friend(username, status = 2, verify_content='')
  url = api_url('webwxverifyuser', {r: timestamp, pass_ticket: store(:pass_ticket)})
  params = params_base_request.merge({
    "Opcode" => status, # 3
    "VerifyUserListSize" => 1,
    "VerifyUserList" => [{
      "Value" => username,
      "VerifyUserTicket" => ''}],
    "VerifyContent" => verify_content,
    "SceneListCount" => 1,
    "SceneList" => [33],
    "skey" => store(:skey)
  })
  r = @session.post(url, json: params)
  r.parse(:json)
end

#add_group_member(username, *users) ⇒ Object

群组添加



567
568
569
# File 'lib/wechat/bot/client.rb', line 567

def add_group_member(username, *users)
  update_group(username, 'addmember', 'AddMemberList', users.join(","))
end

#alive?Boolean

获取是否在线(存活)

Returns:

  • (Boolean)


619
620
621
# File 'lib/wechat/bot/client.rb', line 619

def alive?
  @is_alive
end

#contactsHash

获取所有联系人列表

好友、群组、订阅号、公众号和特殊号

Returns:

  • (Hash)

    联系人列表



314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/wechat/bot/client.rb', line 314

def contacts
  url = api_url('webwxgetcontact', {
    "r" => timestamp,
    "pass_ticket" => store(:pass_ticket),
    "skey" => store(:skey)
  })

  r = @session.post(url, json: {})
  data = r.parse(:json)

  @bot.contact_list.batch_sync(data["MemberList"])
end

#create_group(*users) ⇒ Hash<Object, Object>

创建群组

Parameters:

Returns:

  • (Hash<Object, Object>)


525
526
527
528
529
530
531
532
533
534
535
# File 'lib/wechat/bot/client.rb', line 525

def create_group(*users)
  url = api_url('webwxcreatechatroom', r: timestamp, pass_ticket: store(:pass_ticket))
  params = params_base_request.merge({
    "Topic" => "",
    "MemberCount" => users.size,
    "MemberList" => users.map { |u| { "UserName" => u } }
  })

  r = @session.post(url, json: params)
  r.parse(:json)
end

#delete_group_member(username, *users) ⇒ Object

删除群组成员



557
558
559
# File 'lib/wechat/bot/client.rb', line 557

def delete_group_member(username, *users)
  update_group(username, 'delmember', 'DelMemberList', users.join(","))
end

#download_image(message_id) ⇒ TempFile

下载图片

Parameters:

Returns:

  • (TempFile)


500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/wechat/bot/client.rb', line 500

def download_image(message_id)
  url = api_url('webwxgetmsgimg')
  params = {
    "msgid" => message_id,
    "skey" => store(:skey)
  }

  r = @session.get(url, params: params)
  # body = r.body

  # FIXME: 不知道什么原因,下载的是空字节
  # 返回的 headers 是 {"Connection"=>"close", "Content-Length"=>"0"}
  temp_file = Tempfile.new(["emoticon", ".gif"])
  while data = r.readpartial
    temp_file.write data
  end
  temp_file.close

  temp_file
end

#invite_group_member(username, *users) ⇒ Object

群组邀请



562
563
564
# File 'lib/wechat/bot/client.rb', line 562

def invite_group_member(username, *users)
  update_group(username, 'invitemember', 'InviteMemberList', users.join(","))
end

#logged?Boolean

获取登录状态

Returns:

  • (Boolean)


612
613
614
# File 'lib/wechat/bot/client.rb', line 612

def logged?
  @is_logged
end

#loginObject

微信登录



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/wechat/bot/client.rb', line 16

def 
  return @bot.logger.info("你已经登录") if logged?

  check_count = 0
  until logged?
    check_count += 1
    @bot.logger.debug "尝试登录 (#{check_count})..."

    uuid = qr_uuid
    until uuid
      @bot.logger.info "重新尝试获取登录二维码 ..."
      sleep 1
    end

    show_qr_code(uuid)

    until logged?
      status, status_data = (uuid)
      case status
      when :logged
        @is_logged = true
        (status_data["redirect_uri"])
        break
      when :scaned
        @bot.logger.info "请在手机微信确认登录 ..."
      when :timeout
        @bot.logger.info "扫描超时,重新获取登录二维码 ..."
        break
      end
    end

    break if logged?
  end

  @bot.logger.info "等待加载登录后所需资源 ..."
  
  update_notice_status

  @bot.logger.info "用户 [#{@bot.profile.nickname}] 登录成功!"
end

#login_loadingObject

微信登录后初始化工作

掉线后 300 秒可以重新使用此 api 登录获取的联系人和群ID保持不变



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/wechat/bot/client.rb', line 197

def 
  url = api_url('webwxinit', r: timestamp)
  r = @session.post(url, json: params_base_request)
  data = r.parse(:json)

  store(
    sync_key: data["SyncKey"],
    invite_start_count: data["InviteStartCount"].to_i,
  )

  # 保存当前用户信息和最近聊天列表
  @bot.profile.parse(data["User"])
  @bot.contact_list.batch_sync(data["ContactList"])

  r
end

#login_status(uuid) ⇒ Array

处理微信登录

Returns:

  • (Array)


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/wechat/bot/client.rb', line 143

def (uuid)
  timestamp = timestamp()
  params = {
    "loginicon" => "true",
    "uuid" => uuid,
    "tip" => 0,
    "r" => timestamp.to_i / 1579,
    "_" => timestamp,
  }

  r = @session.get(File.join(@bot.config.auth_url, "cgi-bin/mmwebwx-bin/login"), params: params)
  data = r.parse(:js)
  status = case data["code"]
           when 200 then :logged
           when 201 then :scaned
           when 408 then :waiting
           else          :timeout
           end

  [status, data]
end

#logoutvoid

This method returns an undefined value.

登出



595
596
597
598
599
600
601
602
603
604
605
606
607
# File 'lib/wechat/bot/client.rb', line 595

def logout
  url = api_url('webwxlogout')
  params = {
    "redirect" => 1,
    "type"  => 1,
    "skey"  => store(:skey)
  }

  @session.get(url, params: params)

  @bot.logger.info "用户 [#{@bot.profile.nickname}] 登出成功!"
  clone!
end

#qr_uuidString

获取生成二维码的唯一识别 ID

Returns:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/wechat/bot/client.rb', line 98

def qr_uuid
  params = {
    "appid" => @bot.config.app_id,
    "fun" => "new",
    "lang" => "zh_CN",
    "_" => timestamp,
  }

  @bot.logger.info "获取登录唯一标识 ..."
  r = @session.get(File.join(@bot.config.auth_url, "jslogin") , params: params)
  data = r.parse(:js)

  return data["uuid"] if data["code"] == 200
end

#send(type, username, content) ⇒ Hash<Object,Object>

消息发送

Parameters:

  • type (Symbol)

    消息类型,未知类型默认走 :text

    • :text 文本

    • :emoticon 表情

    • :image 图片

  • username (String)
  • content (String)

Returns:

  • (Hash<Object,Object>)

    发送结果状态



338
339
340
341
342
343
344
345
346
347
# File 'lib/wechat/bot/client.rb', line 338

def send(type, username, content)
  case type
  when :emoticon
    send_emoticon(username, content)
  when :image
    send_image(username, content: content)
  else
    send_text(username, content)
  end
end

#send_emoticon(username, emoticon_id) ⇒ Hash<Object,Object>

发送表情

支持微信表情和自定义表情

Parameters:

Returns:

  • (Hash<Object,Object>)

    发送结果状态



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/wechat/bot/client.rb', line 471

def send_emoticon(username, emoticon_id)
  url = api_url('webwxsendemoticon', {
    'fun' => 'sys',
    'pass_ticket' => store(:pass_ticket),
    'lang' => 'zh_CN'
  })
  params = params_base_request.merge({
    "Scene" => 0,
    "Msg" => {
      "Type" => 47,
      'EmojiFlag' => 2,
      "FromUserName" => @bot.profile.username,
      "ToUserName" => username,
      "LocalID" => timestamp,
      "ClientMsgId" => timestamp,
    },
  })

  emoticon_key = emoticon_id.include?("@") ? "MediaId" : "EMoticonMd5"
  params["Msg"][emoticon_key] = emoticon_id

  r = @session.post(url, json: params)
  r.parse(:json)
end

#send_image(username, **opts) ⇒ Boolean

发送图片

Parameters:

  • username (String)

    目标 UserName

  • 图片名或图片文件 (String, File)
  • 非文本消息的参数(可选) (Hash)

Returns:

  • (Boolean)

    发送结果状态



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/wechat/bot/client.rb', line 431

def send_image(username, **opts)
  # if media_id.nil?
  #   media_id = upload_file(image)
  # end
  if opts[:media_id]
    conf = {"MediaId" => opts[:media_id], "Content" => ""}
  elsif opts[:image]
    media_id = upload_image(username, opts[:image])
    conf = {"MediaId" => media_id, "Content" => ""}
  elsif opts[:content]
    conf = {"MediaId" => "", "Content" => opts[:content]}
  else
    raise RuntimeException, "发送图片参数错误,须提供media_id或content"
  end

  url = "#{store(:index_url)}/webwxsendmsgimg?fun=async&f=json"

  params = params_base_request.merge({
    "Scene" => 0,
    "Msg" => {
      "Type" => 3,
      "FromUserName" => @bot.profile.username,
      "ToUserName" => username,
      "LocalID" => timestamp,
      "ClientMsgId" => timestamp,
    }.merge(conf)
  })

  r = @session.post(url, json: params)
  r.parse(:json)
end

#send_text(username, text) ⇒ Hash<Object,Object>

发送消息

Parameters:

  • username (String)

    目标UserName

  • text (String)

    消息内容

Returns:

  • (Hash<Object,Object>)

    发送结果状态



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/wechat/bot/client.rb', line 354

def send_text(username, text)
  url = api_url('webwxsendmsg')
  params = params_base_request.merge({
    "Scene" => 0,
    "Msg" => {
      "Type" => 1,
      "FromUserName" => @bot.profile.username,
      "ToUserName" => username,
      "Content" => text,
      "LocalID" => timestamp,
      "ClientMsgId" => timestamp,
    },
  })

  r = @session.post(url, json: params)
  r.parse(:json)
end

#set_group_name(username, name) ⇒ Object

修改群组名称



552
553
554
# File 'lib/wechat/bot/client.rb', line 552

def set_group_name(username, name)
  update_group(username, 'modtopic', 'NewTopic', name)
end

#show_qr_code(uuid) ⇒ Object

获取二维码图片



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/wechat/bot/client.rb', line 114

def show_qr_code(uuid)
  @bot.logger.info "获取登录用扫描二维码 ... "
  url = File.join(@bot.config.auth_url, "l", uuid)
  qrcode = RQRCode::QRCode.new(url)

  # image = qrcode.as_png(
  #   resize_gte_to: false,
  #   resize_exactly_to: false,
  #   fill: "white",
  #   color: "black",
  #   size: 120,
  #   border_modules: 4,
  #   module_px_size: 6,
  # )
  # IO.write(QR_FILENAME, image.to_s)

  svg = qrcode.as_ansi(
    light: "\033[47m",
    dark: "\033[40m",
    fill_character: "  ",
    quiet_zone_size: 2
  )

  puts svg
end

#start_runloop_threadObject

Runloop 监听



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/wechat/bot/client.rb', line 58

def start_runloop_thread
  @is_alive = true
  retry_count = 0

  Thread.new do
    while alive?
      begin
        status = sync_check
        if status[:retcode] == "0"
          if status[:selector].nil?
            @is_alive = false
          elsif status[:selector] != "0"
            sync_messages
          end
        elsif status[:retcode] == "1100"
          @bot.logger.info("账户在手机上进行登出操作")
          @is_alive = false
          break
        elsif [ "1101", "1102" ].include?(status[:retcode])
          @bot.logger.info("账户在手机上进行登出或在其他地方进行登录操作操作")
          @is_alive = false
          break
        end

        retry_count = 0
      rescue Exception => e
        retry_count += 1
        @bot.logger.fatal(e)
      end

      sleep 1
    end

    logout
  end
end

#store_login_data(redirect_url) ⇒ Object

保存登录返回的数据信息

redirect_uri 有效时间是从扫码成功后算起大概是 300 秒, 在此期间可以重新登录,但获取的联系人和群 ID 会改变

Raises:

  • (RuntimeError)


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/wechat/bot/client.rb', line 169

def (redirect_url)
  host = URI.parse(redirect_url).host
  r = @session.get(redirect_url)
  data = r.parse(:xml)

  store(
    skey: data["error"]["skey"],
    sid: data["error"]["wxsid"],
    uin: data["error"]["wxuin"],
    device_id: "e#{rand.to_s[2..17]}",
    pass_ticket: data["error"]["pass_ticket"],
  )

  @bot.config.servers.each do |server|
    if host == server[:index]
      update_servers(server)
      break
    end
  end

  raise RuntimeError, "没有匹配到对于的微信服务器: #{host}" unless store(:index_url)

  r
end

#sync_checkHash

检查微信状态

状态会包含是否有新消息、用户状态变化等

Returns:

  • (Hash)

    状态数据数组

    • :retcode

      • 0 成功

      • 1100 用户登出

      • 1101 用户在其他地方登录

    • :selector

      • 0 无消息

      • 2 新消息

      • 6 未知消息类型

      • 7 需要调用 #sync_messages



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/wechat/bot/client.rb', line 244

def sync_check
  url = "#{store(:push_url)}/synccheck"
  params = {
    "r" => timestamp,
    "skey" => store(:skey),
    "sid" => store(:sid),
    "uin" => store(:uin),
    "deviceid" => store(:device_id),
    "synckey" => params_sync_key,
    "_" => timestamp,
  }

  r = @session.get(url, params: params, timeout: [10, 60])
  data = r.parse(:js)["synccheck"]

  # raise RuntimeException "微信数据同步异常,原始返回内容:#{r.to_s}" if data.nil?

  @bot.logger.debug "HeartBeat: retcode/selector #{data.nil? ? "exception" :  [data[:retcode], data[:selector]].join('/')}"
  data
end

#sync_messagesvoid

This method returns an undefined value.

获取微信消息数据

根据 #sync_check 接口返回有数据时需要调用该接口



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/wechat/bot/client.rb', line 269

def sync_messages
  url = api_url('webwxsync', {
    "sid" => store(:sid),
    "skey" => store(:skey),
    "pass_ticket" => store(:pass_ticket)
  })
  params = params_base_request.merge({
    "SyncKey" => store(:sync_key),
    "rr" => "-#{timestamp}"
  })

  r = @session.post(url, json: params, timeout: [10, 60])
  data = r.parse(:json)

  @bot.logger.debug "Message: A/M/D/CM #{data["AddMsgCount"]}/#{data["ModContactCount"]}/#{data["DelContactCount"]}/#{data["ModChatRoomMemberCount"]}"

  store(:sync_key, data["SyncCheckKey"])

  # 更新已存在的群聊信息、增加新的群聊信息
  @bot.contact_list.batch_sync(data["ModContactList"]) if data["ModContactCount"] > 0

  if data["AddMsgCount"] > 0
    data["AddMsgList"].each do |msg|
      next if msg["FromUserName"] == @bot.profile.username

      message = Message.new(msg, @bot)

      events = [:message]
      events.push(:text) if message.kind == Message::Kind::Text
      events.push(:group) if msg["ToUserName"].include?("@@")

      events.each do |event, *args|
        @bot.handlers.dispatch(event, message, args)
      end
    end
  end

  data
end

#update_group(username, fun, update_key, update_value) ⇒ Object

更新群组



541
542
543
544
545
546
547
548
549
# File 'lib/wechat/bot/client.rb', line 541

def update_group(username, fun, update_key, update_value)
  url = api_url('webwxupdatechatroom', {fun: fun, pass_ticket: store(:pass_ticket)})
  params = params_base_request.merge({
    "ChatRoomName" => username,
    update_key => update_value
    })
  r = @session.post(url, json: params)
  r.parse(:json)
end

#update_notice_statusObject

更新通知状态(关闭手机提醒通知)

需要解密参数 Code 的值的作用,目前都用的是 3



217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/wechat/bot/client.rb', line 217

def update_notice_status
  url = api_url('webwxstatusnotify', lang: 'zh_CN', pass_ticket: store(:pass_ticket))
  params = params_base_request.merge({
    "Code"  => 3,
    "FromUserName" => @bot.profile.username,
    "ToUserName" => @bot.profile.username,
    "ClientMsgId" => timestamp
  })

  r = @session.post(url, json: params)
  r
end

#upload_image(username, file) ⇒ Object

FIXME: 上传图片出问题,未能解决



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/wechat/bot/client.rb', line 373

def upload_image(username, file)
  url = "#{store(:file_url)}/webwxuploadmedia?f=json"
  
  filename = File.basename(file.path)
  content_type = {'png'=>'image/png', 'jpg'=>'image/jpeg', 'jpeg'=>'image/jpeg'}[filename.split('.').last.downcase] || 'application/octet-stream'
  md5 = Digest::MD5.file(file.path).hexdigest

  headers = {
    'Host' => 'file.wx.qq.com',
    'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0',
    'Accept' => '*/*',
    'Accept-Language' => 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
    'Accept-Encoding' => 'gzip, deflate, br',
    'Referer' => 'https://wx.qq.com/',
    'Origin' => 'https://wx.qq.com',
    'Connection' => 'Keep-Alive'
  }

  @media_cnt = 1 + (@media_cnt || -1)

  params = {
    'id' => "WU_FILE_#{@media_cnt}",
    'name' => filename,
    'type' => content_type,
    'lastModifiedDate' => 'Tue Sep 09 2014 17:47:23 GMT+0800 (CST)',
    'size' => file.size,
    'mediatype' => 'pic', # pic/video/doc
    'uploadmediarequest' => JSON.generate(
      params_base_request.merge({
        'UploadType' => 2,
        'ClientMediaId' => timestamp,
        'TotalLen' => file.size,
        'StartPos' => 0,
        'DataLen' => file.size,
        'MediaType' => 4,
        'FromUserName' => @bot.profile.username,
        'ToUserName' => username,
        'FileMd5' => md5
        })
      ),
    'webwx_data_ticket' => @session.cookie_of('webwx_data_ticket'),
    'pass_ticket' => store(:pass_ticket),
    'filename' => ::HTTP::FormData::File.new(file, content_type: content_type)
    }

  r = @session.post(url, form: params, headers: headers)

  # @bot.logger.info "Response: #{r.inspect}"

  r.parse(:json)
end