Class: WeChat::Bot::Client
- Inherits:
-
Object
- Object
- WeChat::Bot::Client
- Defined in:
- lib/wechat/bot/client.rb
Overview
微信 API 类
Instance Method Summary collapse
- #_send ⇒ Object
-
#add_friend(username, status = 2, verify_content = '') ⇒ Object
添加好友.
-
#add_group_member(username, *users) ⇒ Object
群组添加.
-
#alive? ⇒ Boolean
获取是否在线(存活).
-
#contacts ⇒ Hash
获取所有联系人列表.
-
#create_group(*users) ⇒ Hash<Object, Object>
创建群组.
-
#delete_group_member(username, *users) ⇒ Object
删除群组成员.
-
#download_image(message_id) ⇒ TempFile
下载图片.
-
#initialize(bot) ⇒ Client
constructor
A new instance of Client.
-
#invite_group_member(username, *users) ⇒ Object
群组邀请.
-
#logged? ⇒ Boolean
获取登录状态.
-
#login ⇒ Object
微信登录.
-
#login_loading ⇒ Object
微信登录后初始化工作.
-
#login_status(uuid) ⇒ Array
处理微信登录.
-
#logout ⇒ void
登出.
-
#qr_uuid ⇒ String
获取生成二维码的唯一识别 ID.
-
#send(type, username, content) ⇒ Hash<Object,Object>
消息发送.
-
#send_emoticon(username, emoticon_id) ⇒ Hash<Object,Object>
发送表情.
-
#send_image(username, **opts) ⇒ Boolean
发送图片.
-
#send_text(username, text) ⇒ Hash<Object,Object>
发送消息.
-
#set_group_name(username, name) ⇒ Object
修改群组名称.
-
#show_qr_code(uuid) ⇒ Object
获取二维码图片.
-
#start_runloop_thread ⇒ Object
Runloop 监听.
-
#store_login_data(redirect_url) ⇒ Object
保存登录返回的数据信息.
-
#sync_check ⇒ Hash
检查微信状态.
-
#sync_messages ⇒ void
获取微信消息数据.
-
#update_group(username, fun, update_key, update_value) ⇒ Object
更新群组.
-
#update_notice_status ⇒ Object
更新通知状态(关闭手机提醒通知).
-
#upload_image(username, file) ⇒ Object
FIXME: 上传图片出问题,未能解决.
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
#_send ⇒ Object
329 |
# File 'lib/wechat/bot/client.rb', line 329 alias_method :_send, :send |
#add_friend(username, status = 2, verify_content = '') ⇒ Object
添加好友
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 |
# File 'lib/wechat/bot/client.rb', line 576 def add_friend(username, status = 2, verify_content='') url = api_url('webwxverifyuser', {r: , 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
群组添加
569 570 571 |
# File 'lib/wechat/bot/client.rb', line 569 def add_group_member(username, *users) update_group(username, 'addmember', 'AddMemberList', users.join(",")) end |
#alive? ⇒ Boolean
获取是否在线(存活)
621 622 623 |
# File 'lib/wechat/bot/client.rb', line 621 def alive? @is_alive end |
#contacts ⇒ Hash
获取所有联系人列表
好友、群组、订阅号、公众号和特殊号
316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/wechat/bot/client.rb', line 316 def contacts url = api_url('webwxgetcontact', { "r" => , "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>
创建群组
527 528 529 530 531 532 533 534 535 536 537 |
# File 'lib/wechat/bot/client.rb', line 527 def create_group(*users) url = api_url('webwxcreatechatroom', r: , 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
删除群组成员
559 560 561 |
# File 'lib/wechat/bot/client.rb', line 559 def delete_group_member(username, *users) update_group(username, 'delmember', 'DelMemberList', users.join(",")) end |
#download_image(message_id) ⇒ TempFile
下载图片
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
# File 'lib/wechat/bot/client.rb', line 502 def download_image() url = api_url('webwxgetmsgimg') params = { "msgid" => , "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
群组邀请
564 565 566 |
# File 'lib/wechat/bot/client.rb', line 564 def invite_group_member(username, *users) update_group(username, 'invitemember', 'InviteMemberList', users.join(",")) end |
#logged? ⇒ Boolean
获取登录状态
614 615 616 |
# File 'lib/wechat/bot/client.rb', line 614 def logged? @is_logged end |
#login ⇒ Object
微信登录
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 56 57 |
# File 'lib/wechat/bot/client.rb', line 16 def login 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 = login_status(uuid) case status when :logged @is_logged = true store_login_data(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 "等待加载登录后所需资源 ..." login_loading update_notice_status @bot.logger.info "用户 [#{@bot.profile.nickname}] 登录成功!" start_runloop_thread end |
#login_loading ⇒ Object
微信登录后初始化工作
掉线后 300 秒可以重新使用此 api 登录获取的联系人和群ID保持不变
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/wechat/bot/client.rb', line 199 def login_loading url = api_url('webwxinit', r: ) 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
处理微信登录
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/wechat/bot/client.rb', line 145 def login_status(uuid) = () params = { "loginicon" => "true", "uuid" => uuid, "tip" => 0, "r" => .to_i / 1579, "_" => , } 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 |
#logout ⇒ void
This method returns an undefined value.
登出
597 598 599 600 601 602 603 604 605 606 607 608 609 |
# File 'lib/wechat/bot/client.rb', line 597 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_uuid ⇒ String
获取生成二维码的唯一识别 ID
100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/wechat/bot/client.rb', line 100 def qr_uuid params = { "appid" => @bot.config.app_id, "fun" => "new", "lang" => "zh_CN", "_" => , } @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>
消息发送
340 341 342 343 344 345 346 347 348 349 |
# File 'lib/wechat/bot/client.rb', line 340 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>
发送表情
支持微信表情和自定义表情
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/wechat/bot/client.rb', line 473 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" => , "ClientMsgId" => , }, }) 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
发送图片
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 462 463 |
# File 'lib/wechat/bot/client.rb', line 433 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" => , "ClientMsgId" => , }.merge(conf) }) r = @session.post(url, json: params) r.parse(:json) end |
#send_text(username, text) ⇒ Hash<Object,Object>
发送消息
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/wechat/bot/client.rb', line 356 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" => , "ClientMsgId" => , }, }) r = @session.post(url, json: params) r.parse(:json) end |
#set_group_name(username, name) ⇒ Object
修改群组名称
554 555 556 |
# File 'lib/wechat/bot/client.rb', line 554 def set_group_name(username, name) update_group(username, 'modtopic', 'NewTopic', name) end |
#show_qr_code(uuid) ⇒ Object
获取二维码图片
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/wechat/bot/client.rb', line 116 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_thread ⇒ Object
Runloop 监听
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 94 95 |
# File 'lib/wechat/bot/client.rb', line 60 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" 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 会改变
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/wechat/bot/client.rb', line 171 def store_login_data(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_check ⇒ Hash
检查微信状态
状态会包含是否有新消息、用户状态变化等
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/wechat/bot/client.rb', line 246 def sync_check url = "#{store(:push_url)}/synccheck" params = { "r" => , "skey" => store(:skey), "sid" => store(:sid), "uin" => store(:uin), "deviceid" => store(:device_id), "synckey" => params_sync_key, "_" => , } 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_messages ⇒ void
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 308 309 |
# File 'lib/wechat/bot/client.rb', line 271 def 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" => "-#{}" }) 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.new(msg, @bot) events = [:message] events.push(:text) if .kind == Message::Kind::Text events.push(:group) if msg["ToUserName"].include?("@@") events.each do |event, *args| @bot.handlers.dispatch(event, , args) end end end data end |
#update_group(username, fun, update_key, update_value) ⇒ Object
更新群组
543 544 545 546 547 548 549 550 551 |
# File 'lib/wechat/bot/client.rb', line 543 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_status ⇒ Object
更新通知状态(关闭手机提醒通知)
需要解密参数 Code 的值的作用,目前都用的是 3
219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/wechat/bot/client.rb', line 219 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" => }) r = @session.post(url, json: params) r end |
#upload_image(username, file) ⇒ Object
FIXME: 上传图片出问题,未能解决
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 424 425 |
# File 'lib/wechat/bot/client.rb', line 375 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' => , 'TotalLen' => file.size, 'StartPos' => 0, 'DataLen' => file.size, 'MediaType' => 4, 'FromUserName' => @bot.profile.username, 'ToUserName' => username, 'FileMd5' => md5 }) ), 'webwx_data_ticket' => @session.('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 |