├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── qqbot.rb └── qqbot │ ├── api.rb │ ├── bot.rb │ ├── client.rb │ ├── cookie.rb │ ├── error.rb │ ├── model.rb │ └── version.rb ├── qqbot.gemspec └── spec ├── qqbot_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | lib/qqbot/qrcode.png 12 | 13 | qqbot-0.1.0.gem 14 | 15 | *.gem 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.3 4 | before_install: gem install bundler -v 1.11.2 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at xie_enlong@foxmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://ruby.taobao.org' 2 | 3 | # Specify your gem's dependencies in qqbot.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ScienJus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QQBot 2 | 3 | 基于 Smart QQ(Web QQ)的 QQ 机器人 4 | 5 | Java 版本:[ScienJus/smartqq][5] 6 | 7 | Api分析: 8 | 9 | [Web QQ协议分析(一):前言][6] 10 | 11 | [Web QQ协议分析(二):登录][7] 12 | 13 | [Web QQ协议分析(三):收发消息][8] 14 | 15 | [Web QQ协议分析(四):好友相关][9] 16 | 17 | [Web QQ协议分析(五):群和讨论组相关][10] 18 | 19 | [Web QQ协议分析(六):其他][11] 20 | 21 | ### 使用方法 22 | 23 | 安装这个 Gem : 24 | 25 | ``` 26 | gem install qqbot 27 | ``` 28 | 29 | `require 'qqbot'`并编写自己的业务逻辑,例如: 30 | 31 | ``` 32 | require 'qqbot' 33 | 34 | qqbot = QQBot.new 35 | 36 | # 在这里需要扫描二维码登录 37 | 38 | # 打印出好友列表 39 | qqbot.get_friend_list_with_category.each do |category| 40 | puts category.name 41 | category.friends.each do |friend| 42 | puts "———— #{friend.nickname}" 43 | end 44 | end 45 | ``` 46 | 47 | ### 示例代码 48 | 49 | [在控制台打印接收到的所有消息][1] 50 | 51 | 效果: 52 | 53 | ![Console][2] 54 | 55 | [通过 Tuling123 的 Api 实现自动回复功能][3] 56 | 57 | 效果: 58 | 59 | ![Tuling][4] 60 | 61 | 62 | [1]: https://gist.github.com/ScienJus/f1ba1e5b1611cca662cc 63 | [2]: http://www.scienjus.com/wp-content/uploads/2015/12/console.png 64 | [3]: https://gist.github.com/ScienJus/26a341fda25d009acea1 65 | [4]: http://www.scienjus.com/wp-content/uploads/2015/12/tuling.jpg 66 | [5]: https://github.com/ScienJus/smartqq 67 | [6]: http://www.scienjus.com/webqq-analysis-1/ 68 | [7]: http://www.scienjus.com/webqq-analysis-2/ 69 | [8]: http://www.scienjus.com/webqq-analysis-3/ 70 | [9]: http://www.scienjus.com/webqq-analysis-4/ 71 | [10]: http://www.scienjus.com/webqq-analysis-5/ 72 | [11]: http://www.scienjus.com/webqq-analysis-6/ 73 | 74 | ### Api 列表 75 | 76 | - [x] 登录 77 | - [x] 拉取消息 78 | - [x] 获取群列表 79 | - [x] 获取好友列表 80 | - [x] 获取讨论组列表 81 | - [x] 发送私聊消息 82 | - [x] 发送群消息 83 | - [x] 发送讨论组消息 84 | - [x] 发送临时消息 85 | - [x] 好友详细信息 86 | - [x] 群详细信息 87 | - [x] 讨论组详细信息 88 | - [x] 获取在线好友 89 | - [x] 获取最近会话 90 | - [x] 获取登录用户信息 91 | - [x] 查询用户 QQ 号 92 | - [ ] 退出登录 93 | 94 | 95 | ### 拓展功能列表 96 | 97 | - [x] 登录时保存二维码到本地 98 | - [x] 登录时通过网页查看二维码 99 | - [ ] 登录时发送二维码到指定邮箱 100 | - [ ] 登录时将二维码打印到控制台 101 | - [x] 掉线时尝试重新登录 102 | - [ ] 登录失效后邮件推送信息 103 | 104 | 105 | ### 反馈 106 | 107 | 有问题或是建议可以提 Issues ,或是发邮件联系我,我的邮箱:`i@scienjus.com` 108 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "qqbot" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/qqbot.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'qqbot/version' 3 | 4 | module QQBot 5 | 6 | class Logger < Logger 7 | 8 | def info(*args) 9 | super && false if info? 10 | end 11 | 12 | def debug(*args) 13 | super && false if debug? 14 | end 15 | end 16 | 17 | LOGGER = QQBot::Logger.new(STDOUT) 18 | LOGGER.datetime_format = '%Y-%m-%d %H:%M:%S' 19 | LOGGER.level = Logger::INFO 20 | 21 | CLIENT_ID = 53999199 22 | 23 | autoload :Cookie, 'qqbot/cookie' 24 | autoload :Client, 'qqbot/client' 25 | autoload :Api, 'qqbot/api' 26 | autoload :Bot, 'qqbot/bot' 27 | autoload :Group, 'qqbot/model' 28 | autoload :Friend, 'qqbot/model' 29 | autoload :Category, 'qqbot/model' 30 | autoload :Discuss, 'qqbot/model' 31 | autoload :Message, 'qqbot/model' 32 | autoload :Font, 'qqbot/model' 33 | autoload :UserInfo, 'qqbot/model' 34 | autoload :Birthday, 'qqbot/model' 35 | autoload :Recent, 'qqbot/model' 36 | autoload :Online, 'qqbot/model' 37 | autoload :GroupInfo, 'qqbot/model' 38 | autoload :GroupMember, 'qqbot/model' 39 | autoload :DiscussInfo, 'qqbot/model' 40 | autoload :DiscussMember, 'qqbot/model' 41 | autoload :Error, 'qqbot/error' 42 | 43 | def self.new 44 | QQBot::Bot.new 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/qqbot/api.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module QQBot 4 | class Api 5 | 6 | def initialize 7 | @client = QQBot::Client.new 8 | @msg_id = 1_000_000 9 | end 10 | 11 | def auth_options=(options = {}) 12 | @options = options 13 | end 14 | 15 | def get_qrcode 16 | uri = URI('https://ssl.ptlogin2.qq.com/ptqrshow'); 17 | uri.query = 18 | URI.encode_www_form( 19 | appid: 501004106, 20 | e: 0, 21 | l: :M, 22 | s: 5, 23 | d: 72, 24 | v: 4, 25 | t: 0.1, 26 | ) 27 | @client.get(uri) 28 | end 29 | 30 | # webqq 对应js源码 https://imgcache.qq.com/ptlogin/ver/10203/js/mq_comm.js 31 | def verify_qrcode 32 | uri = URI('https://ssl.ptlogin2.qq.com/ptqrlogin'); 33 | uri.query = 34 | URI.encode_www_form( 35 | ptqrtoken: get_ptqrtoken, 36 | webqq_type: 10, 37 | remember_uin: 1, 38 | login2qq: 1, 39 | aid: 501004106, 40 | u1: 'http://Fw.qq.com/proxy.html?login2qq=1&webqq_type=10', 41 | ptredirect: 0, 42 | ptlang: 2052, 43 | daid: 164, 44 | from_ui: 1, 45 | pttype: 1, 46 | dumy: '', 47 | fp: 'loginerroralert', 48 | # action: '0-0-157510', 49 | action: '0-0-12038', 50 | mibao_css: 'm_webqq', 51 | t: 1, 52 | g: 1, 53 | js_type: 0, 54 | # js_ver: 10143, 55 | js_ver: 10203, 56 | login_sig: '', 57 | pt_randsalt: 2, 58 | ) 59 | @client.get(uri) 60 | end 61 | 62 | def get_ptqrtoken 63 | t = @client.cookie['qrsig'] 64 | e = 0 65 | i = 0 66 | n = t.length 67 | 68 | while n > i do 69 | e += (e << 5) + t[i].ord 70 | i += 1 71 | end 72 | 73 | return 2147483647 & e 74 | end 75 | 76 | def get_ptwebqq(url) 77 | uri = URI(url); 78 | code, body = @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 79 | return code, @client.get_cookie('ptwebqq') 80 | end 81 | 82 | def get_vfwebqq(ptwebqq) 83 | uri = URI('http://s.web2.qq.com/api/getvfwebqq'); 84 | uri.query = 85 | URI.encode_www_form( 86 | ptwebqq: ptwebqq, 87 | clientid: QQBot::CLIENT_ID, 88 | psessionid: '', 89 | t: 0.1, 90 | ) 91 | @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 92 | end 93 | 94 | def get_psessionid_and_uin(ptwebqq) 95 | uri = URI('http://d1.web2.qq.com/channel/login2'); 96 | r = JSON.generate( 97 | ptwebqq: ptwebqq, 98 | clientid: QQBot::CLIENT_ID, 99 | psessionid: '', 100 | status: 'online' 101 | ) 102 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 103 | end 104 | 105 | def self.hash(uin, ptwebqq) 106 | n = Array.new(4, 0) 107 | ptwebqq.chars.each_index { |i| n[i % 4] ^= ptwebqq[i].ord } 108 | u = ['EC', 'OK'] 109 | v = Array.new(4) 110 | v[0] = uin >> 24 & 255 ^ u[0][0].ord; 111 | v[1] = uin >> 16 & 255 ^ u[0][1].ord; 112 | v[2] = uin >> 8 & 255 ^ u[1][0].ord; 113 | v[3] = uin & 255 ^ u[1][1].ord; 114 | u = Array.new(8) 115 | (0...8).each { |i| u[i] = i.odd? ? v[i >> 1] : n[i >> 1] } 116 | n = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'] 117 | v = '' 118 | u.each do |i| 119 | v << n[(i >> 4) & 15] 120 | v << n[i & 15] 121 | end 122 | v 123 | end 124 | 125 | def poll 126 | uri = URI('http://d1.web2.qq.com/channel/poll2') 127 | r = JSON.generate( 128 | ptwebqq: @options[:ptwebqq], 129 | clientid: QQBot::CLIENT_ID, 130 | psessionid: @options[:psessionid], 131 | key: '' 132 | ) 133 | begin 134 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 135 | rescue 136 | retry 137 | end 138 | end 139 | 140 | def get_group_list 141 | uri = URI('http://s.web2.qq.com/api/get_group_name_list_mask2') 142 | r = JSON.generate( 143 | vfwebqq: @options[:vfwebqq], 144 | hash: hash 145 | ) 146 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 147 | end 148 | 149 | def hash 150 | self.class.hash(@options[:uin], @options[:ptwebqq]) 151 | end 152 | 153 | def get_friend_list 154 | uri = URI('http://s.web2.qq.com/api/get_user_friends2') 155 | r = JSON.generate( 156 | vfwebqq: @options[:vfwebqq], 157 | hash: hash 158 | ) 159 | @client.post(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1', r: r) 160 | end 161 | 162 | def get_discuss_list 163 | uri = URI('http://s.web2.qq.com/api/get_discus_list') 164 | uri.query = 165 | URI.encode_www_form( 166 | clientid: QQBot::CLIENT_ID, 167 | psessionid: @options[:psessionid], 168 | vfwebqq: @options[:vfwebqq], 169 | t: 0.1 170 | ) 171 | @client.get(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2') 172 | end 173 | 174 | def send_to_friend(friend_id, content) 175 | uri = URI('http://d1.web2.qq.com/channel/send_buddy_msg2') 176 | r = JSON.generate( 177 | to: friend_id, 178 | content: self.class.build_message(content), 179 | face: 522, 180 | clientid: QQBot::CLIENT_ID, 181 | msg_id: msg_id, 182 | psessionid: @options[:psessionid] 183 | ) 184 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 185 | end 186 | 187 | def send_to_group(group_id, content) 188 | uri = URI('http://d1.web2.qq.com/channel/send_qun_msg2') 189 | r = JSON.generate( 190 | group_uin: group_id, 191 | content: self.class.build_message(content), 192 | face: 522, 193 | clientid: QQBot::CLIENT_ID, 194 | msg_id: msg_id, 195 | psessionid: @options[:psessionid] 196 | ) 197 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 198 | end 199 | 200 | def send_to_discuss(discuss_id, content) 201 | uri = URI('http://d1.web2.qq.com/channel/send_discu_msg2') 202 | r = JSON.generate( 203 | did: discuss_id, 204 | content: self.class.build_message(content), 205 | face: 522, 206 | clientid: QQBot::CLIENT_ID, 207 | msg_id: msg_id, 208 | psessionid: @options[:psessionid] 209 | ) 210 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 211 | end 212 | 213 | def send_to_sess(sess_id, content) 214 | uri = URI('http://d1.web2.qq.com/channel/send_sess_msg2') 215 | r = JSON.generate( 216 | to: sess_id, 217 | content: self.class.build_message(content), 218 | face: 522, 219 | clientid: QQBot::CLIENT_ID, 220 | msg_id: msg_id, 221 | psessionid: @options[:psessionid] 222 | ) 223 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 224 | end 225 | 226 | def get_account_info 227 | uri = URI('http://s.web2.qq.com/api/get_self_info2') 228 | uri.query = 229 | URI.encode_www_form( 230 | t: 0.1 231 | ) 232 | 233 | @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 234 | end 235 | 236 | def get_recent_list 237 | uri = URI('http://d1.web2.qq.com/channel/get_recent_list2') 238 | r = JSON.generate( 239 | vfwebqq: @options[:vfwebqq], 240 | clientid: QQBot::CLIENT_ID, 241 | psessionid: '' 242 | ) 243 | @client.post(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', r: r) 244 | end 245 | 246 | def get_qq_by_id(id) 247 | uri = URI('http://s.web2.qq.com/api/get_friend_uin2') 248 | uri.query = 249 | URI.encode_www_form( 250 | tuin: id, 251 | type: 1, 252 | vfwebqq: @options[:vfwebqq], 253 | t: 0.1 254 | ) 255 | @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 256 | end 257 | 258 | def get_online_friends 259 | uri = URI('http://d1.web2.qq.com/channel/get_online_buddies2') 260 | uri.query = 261 | URI.encode_www_form( 262 | vfwebqq: @options[:vfwebqq], 263 | clientid: QQBot::CLIENT_ID, 264 | psessionid: @options[:psessionid], 265 | t: 0.1 266 | ) 267 | @client.get(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2') 268 | end 269 | 270 | def get_group_info(group_code) 271 | uri = URI('http://s.web2.qq.com/api/get_group_info_ext2') 272 | uri.query = 273 | URI.encode_www_form( 274 | gcode: group_code, 275 | vfwebqq: @options[:vfwebqq], 276 | t: 0.1 277 | ) 278 | @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 279 | end 280 | 281 | def get_discuss_info(discuss_id) 282 | uri = URI('http://d1.web2.qq.com/channel/get_discu_info') 283 | uri.query = 284 | URI.encode_www_form( 285 | did: discuss_id, 286 | vfwebqq: @options[:vfwebqq], 287 | clientid: QQBot::CLIENT_ID, 288 | psessionid: @options[:psessionid], 289 | t: 0.1 290 | ) 291 | @client.get(uri, 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2') 292 | end 293 | 294 | def get_friend_info(friend_id) 295 | uri = URI('http://s.web2.qq.com/api/get_friend_info2') 296 | uri.query = 297 | URI.encode_www_form( 298 | tuin: friend_id, 299 | vfwebqq: @options[:vfwebqq], 300 | clientid: QQBot::CLIENT_ID, 301 | psessionid: @options[:psessionid], 302 | t: 0.1 303 | ) 304 | @client.get(uri, 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1') 305 | end 306 | 307 | def hash 308 | self.class.hash(@options[:uin], @options[:ptwebqq]) 309 | end 310 | 311 | def msg_id 312 | @msg_id += 1 313 | end 314 | 315 | def self.build_message(content) 316 | JSON.generate( 317 | [ 318 | content.force_encoding("UTF-8"), 319 | [ 320 | 'font', 321 | { 322 | name: '宋体', 323 | size: 10, 324 | style: [0, 0, 0], 325 | color: '000000' 326 | } 327 | ] 328 | ] 329 | ) 330 | end 331 | end 332 | end 333 | -------------------------------------------------------------------------------- /lib/qqbot/bot.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module QQBot 4 | class Bot 5 | def initialize 6 | @api = QQBot::Api.new 7 | @api.auth_options = login 8 | end 9 | 10 | def self.check_response_json(code, body) 11 | if code == '200' 12 | json = JSON.parse body 13 | retcode = json['retcode'] 14 | if retcode == 0 15 | return json['result'] 16 | elsif retcode == 100012 17 | QQBot::LOGGER.info '因为掉线此次请求失败,尝试重新登录' 18 | @api.auth_options = relogin 19 | else 20 | QQBot::LOGGER.info "请求失败,JSON返回码 #{retcode}" 21 | end 22 | else 23 | QQBot::LOGGER.info "请求失败,HTTP返回码 #{code}" 24 | end 25 | end 26 | 27 | def self.check_send_msg_response(code, body) 28 | if code == '200' 29 | json = JSON.parse body 30 | if json['errCode'] == 0 31 | QQBot::LOGGER.info '发送成功' 32 | return true 33 | else 34 | QQBot::LOGGER.info "发送失败,JSON返回码 #{json['retcode']}" 35 | end 36 | else 37 | QQBot::LOGGER.info "请求失败,HTTP返回码 #{code}" 38 | end 39 | end 40 | 41 | def get_qrcode 42 | code, body = @api.get_qrcode 43 | 44 | if code == '200' 45 | file_name = File.expand_path('qrcode.png', File.dirname(__FILE__)); 46 | 47 | File.open(file_name, 'wb') do |file| 48 | file.write body 49 | file.close 50 | end 51 | 52 | QQBot::LOGGER.info "二维码已经保存在#{file_name}中" 53 | 54 | unless @pid 55 | QQBot::LOGGER.info '开启web服务进程' 56 | @pid = spawn("ruby -run -e httpd #{file_name} -p 9090") 57 | end 58 | 59 | QQBot::LOGGER.info '也可以通过访问 http://localhost:9090 查看二维码' 60 | return true 61 | else 62 | QQBot::LOGGER.info "请求失败,返回码#{code}" 63 | end 64 | end 65 | 66 | def verify_qrcode 67 | url = '' 68 | 69 | until url.start_with? 'http' do 70 | sleep 5 71 | code, body = @api.verify_qrcode 72 | 73 | if code == '200' 74 | result = body.force_encoding("UTF-8") 75 | if result.include? '二维码已失效' 76 | QQBot::LOGGER.info '二维码已失效,准备重新获取' 77 | return unless get_qrcode 78 | elsif result.include? 'http' 79 | QQBot::LOGGER.info '认证成功' 80 | return URI.extract(result).first 81 | end 82 | else 83 | QQBot::LOGGER.info "请求失败,返回码#{code}" 84 | end 85 | end 86 | end 87 | 88 | def close_qrcode_server 89 | if @pid 90 | QQBot::LOGGER.info '关闭web服务进程' 91 | Process.kill('KILL', @pid) 92 | @pid = nil 93 | end 94 | end 95 | 96 | def get_ptwebqq(url) 97 | code, ptwebqq = @api.get_ptwebqq url 98 | 99 | if code == '302' 100 | ptwebqq 101 | else 102 | QQBot::LOGGER.info "请求失败,返回码#{code}" 103 | end 104 | end 105 | 106 | def get_vfwebqq(ptwebqq) 107 | code, body = @api.get_vfwebqq(ptwebqq) 108 | 109 | result = self.class.check_response_json(code, body) 110 | result['vfwebqq'] if result 111 | end 112 | 113 | def get_psessionid_and_uin(ptwebqq) 114 | code, body = @api.get_psessionid_and_uin ptwebqq 115 | 116 | result = self.class.check_response_json(code, body) 117 | if result 118 | return result['psessionid'], result['uin'] 119 | end 120 | end 121 | 122 | def login 123 | QQBot::LOGGER.info '开始获取二维码' 124 | raise QQBot::Error::LoginFailed unless get_qrcode 125 | 126 | QQBot::LOGGER.info '等待扫描二维码' 127 | url = verify_qrcode 128 | raise QQBot::Error::LoginFailed unless url 129 | 130 | close_qrcode_server 131 | 132 | QQBot::LOGGER.info '开始获取ptwebqq' 133 | @ptwebqq = get_ptwebqq url 134 | raise QQBot::Error::LoginFailed unless @ptwebqq 135 | 136 | QQBot::LOGGER.info '开始获取vfwebqq' 137 | vfwebqq = get_vfwebqq @ptwebqq 138 | raise QQBot::Error::LoginFailed unless vfwebqq 139 | 140 | QQBot::LOGGER.info '开始获取psessionid和uin' 141 | psessionid, uin = get_psessionid_and_uin @ptwebqq 142 | raise QQBot::Error::LoginFailed unless uin && psessionid 143 | 144 | { 145 | ptwebqq: @ptwebqq, 146 | vfwebqq: vfwebqq, 147 | psessionid: psessionid, 148 | uin: uin 149 | } 150 | end 151 | 152 | def relogin 153 | QQBot::LOGGER.info '开始获取vfwebqq' 154 | vfwebqq = get_vfwebqq @ptwebqq 155 | raise QQBot::Error::LoginFailed unless vfwebqq 156 | 157 | QQBot::LOGGER.info '开始获取psessionid和uin' 158 | psessionid, uin = get_psessionid_and_uin @ptwebqq 159 | raise QQBot::Error::LoginFailed unless uin && psessionid 160 | 161 | { 162 | ptwebqq: @ptwebqq, 163 | vfwebqq: vfwebqq, 164 | psessionid: psessionid, 165 | uin: uin 166 | } 167 | end 168 | 169 | def poll 170 | loop do 171 | code, body = @api.poll 172 | result = self.class.check_response_json(code, body) 173 | 174 | if result 175 | result.each do |item| 176 | message = QQBot::Message.new 177 | value = item['value'] 178 | message.type, message.from_id, message.send_id = 179 | case item['poll_type'] 180 | when 'message' then [0, value['from_uin'], value['from_uin']] 181 | when 'group_message' then [1, value['from_uin'], value['send_uin']] 182 | when 'discu_message' then [2, value['from_uin'], value['send_uin']] 183 | else 3 184 | end 185 | message.time = value['time'] 186 | message.content = value['content'][1] 187 | 188 | font = QQBot::Font.new 189 | font_json = value['content'][0][1] 190 | font.color = font_json['color'] 191 | font.name = font_json['name'] 192 | font.size = font_json['size'] 193 | font.style = font_json['style'] 194 | message.font = font 195 | 196 | yield message if block_given? 197 | end 198 | end 199 | sleep 1 200 | end 201 | end 202 | 203 | def get_group_list 204 | code, body = @api.get_group_list 205 | result = self.class.check_response_json(code, body) 206 | 207 | if result 208 | group_map = {} 209 | 210 | gnamelist = result['gnamelist'] 211 | gnamelist.each do |item| 212 | group = QQBot::Group.new 213 | group.name = item['name'] 214 | group.id = item['gid'] 215 | group.code = item['code'] 216 | group_map[group.id] = group 217 | end 218 | 219 | gmarklist = result['gmarklist'] 220 | gmarklist.each do |item| 221 | group_map[item['uin']].markname = item['markname'] 222 | end 223 | 224 | group_map.values 225 | end 226 | end 227 | 228 | def get_friend_list_with_category 229 | code, body = @api.get_friend_list 230 | result = self.class.check_response_json(code, body) 231 | 232 | if result 233 | friend_list = self.class.build_friend_list result 234 | 235 | categories = result['categories'] 236 | has_default_category = false 237 | category_list = categories.collect do |item| 238 | category = QQBot::Category.new 239 | category.name = item['name'] 240 | category.sort = item['sort'] 241 | category.id = item['index'] 242 | category.friends = friend_list.select { |friend| friend.category_id == category.id } 243 | has_default_category ||= (category.id == 0) 244 | category 245 | end 246 | 247 | unless has_default_category 248 | category = QQBot::Category.new 249 | category.name = '我的好友(默认)' 250 | category.sort = 1 251 | category.id = 0 252 | category.friends = friend_list.select { |friend| friend.category_id == category.id } 253 | category_list << category 254 | end 255 | 256 | category_list 257 | end 258 | end 259 | 260 | def get_friend_list 261 | code, body = @api.get_friend_list 262 | result = self.class.check_response_json(code, body) 263 | 264 | if result 265 | self.class.build_friend_list(result) 266 | end 267 | end 268 | 269 | def self.build_friend_list(json) 270 | friend_map = {} 271 | 272 | friends = json['friends'] 273 | friends.each do |item| 274 | friend = QQBot::Friend.new 275 | friend.id = item['uin'] 276 | friend.category_id = item['categories'] 277 | friend_map[friend.id] = friend 278 | end 279 | 280 | marknames = json['marknames'] 281 | marknames.each do |item| 282 | friend_map[item['uin']].markname = item['markname'] 283 | end 284 | 285 | vipinfo = json['vipinfo'] 286 | vipinfo.each do |item| 287 | friend = friend_map[item['u']] 288 | friend.is_vip = item['is_vip'] 289 | friend.vip_level = item['vip_level'] 290 | end 291 | 292 | info = json['info'] 293 | info.each do |item| 294 | friend_map[item['uin']].nickname = item['nick'] 295 | end 296 | 297 | friend_map.values 298 | end 299 | 300 | def get_discuss_list 301 | code, body = @api.get_discuss_list 302 | result = self.class.check_response_json(code, body) 303 | 304 | if result 305 | dnamelist = result['dnamelist'] 306 | dnamelist.collect do |item| 307 | discuss = QQBot::Discuss.new 308 | discuss.name = item['name'] 309 | discuss.id = item['did'] 310 | discuss 311 | end 312 | end 313 | end 314 | 315 | def send_to_friend(friend_id, content) 316 | code, body = @api.send_to_friend(friend_id, content) 317 | self.class.check_send_msg_response(code, body) 318 | end 319 | 320 | def send_to_group(group_id, content) 321 | code, body = @api.send_to_group(group_id, content) 322 | self.class.check_send_msg_response(code, body) 323 | end 324 | 325 | def send_to_discuss(discuss_id, content) 326 | code, body = @api.send_to_discuss(discuss_id, content) 327 | self.class.check_send_msg_response(code, body) 328 | end 329 | 330 | def send_to_sess(sess_id, content) 331 | code, body = @api.send_to_sess(sess_id, content) 332 | self.class.check_send_msg_response(code, body) 333 | end 334 | 335 | def get_account_info 336 | code, body = @api.get_account_info 337 | result = self.class.check_response_json(code, body) 338 | 339 | if result 340 | # TODO 341 | build_user_info(result) 342 | end 343 | end 344 | 345 | def get_friend_info(friend_id) 346 | code, body = @api.get_friend_info(friend_id) 347 | result = self.class.check_response_json(code, body) 348 | 349 | if result 350 | # TODO 351 | build_user_info(result) 352 | end 353 | end 354 | 355 | def build_user_info(result) 356 | user_info = QQBot::UserInfo.new 357 | user_info.phone = result['phone'] 358 | user_info.occupation = result['occupation'] 359 | user_info.college = result['college'] 360 | user_info.id = result['uin'] 361 | user_info.blood = result['blood'] 362 | user_info.slogan = result['lnick'] 363 | user_info.homepage = result['homepage'] 364 | user_info.vip_info = result['vip_info'] 365 | user_info.city = result['city'] 366 | user_info.country = result['country'] 367 | user_info.province = result['province'] 368 | user_info.personal = result['personal'] 369 | user_info.shengxiao = result['shengxiao'] 370 | user_info.nickname = result['nick'] 371 | user_info.email = result['email'] 372 | user_info.account = result['account'] 373 | user_info.gender = result['gender'] 374 | user_info.mobile = result['mobile'] 375 | birthday = QQBot::Birthday.new 376 | birthday.year = result['birthday']['year'] 377 | birthday.month = result['birthday']['month'] 378 | birthday.day = result['birthday']['day'] 379 | user_info.birthday = birthday 380 | 381 | user_info 382 | end 383 | 384 | def get_recent_list 385 | code, body = @api.get_recent_list 386 | result = self.class.check_response_json(code, body) 387 | 388 | if result 389 | result.collect do |item| 390 | recent = QQBot::Recent.new 391 | recent.id = item['uin'] 392 | recent.type = item['type'] 393 | recent 394 | end 395 | end 396 | end 397 | 398 | def get_qq_by_id(id) 399 | code, body = @api.get_qq_by_id(id) 400 | result = self.class.check_response_json(code, body) 401 | 402 | if result 403 | result['account'] 404 | end 405 | end 406 | 407 | def get_online_friends 408 | code, body = @api.get_online_friends 409 | result = self.class.check_response_json(code, body) 410 | 411 | if result 412 | result.collect do |item| 413 | online = QQBot::Online.new 414 | online.id = item['uin'] 415 | online.client_type = item['client_type'] 416 | online 417 | end 418 | end 419 | end 420 | 421 | def get_group_info(group_code) 422 | code, body = @api.get_group_info(group_code) 423 | result = self.class.check_response_json(code, body) 424 | 425 | if result 426 | group_info = QQBot::GroupInfo.new 427 | ginfo = result['ginfo'] 428 | group_info.id = ginfo['gid'] 429 | group_info.create_time = ginfo['createtime'] 430 | group_info.memo = ginfo['memo'] 431 | group_info.name = ginfo['name'] 432 | group_info.owner_id = ginfo['owner'] 433 | group_info.markname = ginfo['markname'] 434 | 435 | member_map = {} 436 | 437 | minfo = result['minfo'] 438 | minfo.each do |item| 439 | member = QQBot::GroupMember.new 440 | member.id = item['uin'] 441 | member.nickname = item['nick'] 442 | member.gender = item['gender'] 443 | member.country = item['country'] 444 | member.city = item['city'] 445 | member_map[member.id] = member 446 | end 447 | 448 | cards = result['cards'] 449 | cards.each { |item| member_map[item['muin']].markname = item['card'] } if cards 450 | 451 | vipinfo = result['vipinfo'] 452 | vipinfo.each do |item| 453 | member = member_map[item['u']] 454 | member.is_vip = item['is_vip'] 455 | member.vip_level = item['vip_level'] 456 | end 457 | 458 | stats = result['stats'] 459 | stats.each do |item| 460 | member = member_map[item['uin']] 461 | member.client_type = item['client_type'] 462 | member.status = item['stat'] 463 | end 464 | 465 | group_info.members = member_map.values 466 | 467 | group_info 468 | end 469 | end 470 | 471 | def get_discuss_info(discuss_id) 472 | code, body = @api.get_discuss_info(discuss_id) 473 | result = self.class.check_response_json(code, body) 474 | 475 | if result 476 | discuss_info = QQBot::DiscussInfo.new 477 | info = result['info'] 478 | discuss_info.id = info['did'] 479 | discuss_info.name = info['discu_name'] 480 | 481 | member_map = {} 482 | 483 | mem_info = result['mem_info'] 484 | mem_info.each do |item| 485 | member = QQBot::DiscussMember.new 486 | member.id = item['uin'] 487 | member.nickname = item['nick'] 488 | member_map[member.id] = member 489 | end 490 | 491 | mem_status = result['mem_status'] 492 | mem_status.each do |item| 493 | member = member_map[item['uin']] 494 | member.client_type = item['client_type'] 495 | member.status = item['status'] 496 | end 497 | 498 | discuss_info.members = member_map.values 499 | 500 | discuss_info 501 | end 502 | end 503 | end 504 | end 505 | -------------------------------------------------------------------------------- /lib/qqbot/client.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'openssl' 3 | 4 | module QQBot 5 | class Client 6 | 7 | @@user_agent = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36' 8 | 9 | attr_accessor :cookie 10 | 11 | def self.origin(uri) 12 | "#{uri.scheme}://#{uri.host}" 13 | end 14 | 15 | def initialize 16 | @cookie = QQBot::Cookie.new 17 | end 18 | 19 | def get(uri, referer = '') 20 | QQBot::LOGGER.debug { "get #{uri.to_s}" } 21 | 22 | Net::HTTP.start(uri.host, uri.port, 23 | use_ssl: uri.scheme == 'https', 24 | verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| 25 | req = Net::HTTP::Get.new(uri) 26 | req.initialize_http_header( 27 | 'User-Agent' => @@user_agent, 28 | 'Cookie' => @cookie.to_s, 29 | 'Referer' => referer 30 | ) 31 | res = http.request(req) 32 | @cookie.put(res.get_fields('set-cookie')) 33 | QQBot::LOGGER.debug { "code: #{res.code}, body: #{res.body}" } 34 | return res.code, res.body 35 | end 36 | end 37 | 38 | def post(uri, referer = '', form_data = {}) 39 | QQBot::LOGGER.debug { "post uri: #{uri.to_s} data: #{form_data.to_s}" } 40 | Net::HTTP.start(uri.host, uri.port, 41 | use_ssl: uri.scheme == 'https', 42 | verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| 43 | req = Net::HTTP::Post.new(uri) 44 | req.set_form_data(form_data) 45 | req.initialize_http_header( 46 | 'User-Agent' => @@user_agent, 47 | 'Cookie' => @cookie.to_s, 48 | 'Referer' => referer, 49 | 'Origin' => self.class.origin(uri) 50 | ) 51 | res = http.request(req) 52 | @cookie.put(res.get_fields('set-cookie')) 53 | QQBot::LOGGER.debug { "response code: #{res.code}, body: #{res.body}" } 54 | return res.code, res.body 55 | end 56 | end 57 | 58 | def get_cookie(key) 59 | @cookie[key] 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/qqbot/cookie.rb: -------------------------------------------------------------------------------- 1 | module QQBot 2 | class Cookie 3 | 4 | def initialize 5 | @cookies = {} 6 | end 7 | 8 | def put set_cookie_array 9 | return if set_cookie_array.nil? 10 | 11 | set_cookie_array.each do |set_cookie| 12 | set_cookie.split('; ').each do |cookie| 13 | k, v = cookie.split('=') 14 | @cookies[k] = v unless v.nil? 15 | end 16 | end 17 | end 18 | 19 | def [] key 20 | @cookies[key] || '' 21 | end 22 | 23 | def to_s 24 | @cookies.map{ |k, v| "#{k}=#{v}" }.join('; ') 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/qqbot/error.rb: -------------------------------------------------------------------------------- 1 | module QQBot 2 | class Error < StandardError; end 3 | class Error::LoginFailed < Error; end 4 | end 5 | -------------------------------------------------------------------------------- /lib/qqbot/model.rb: -------------------------------------------------------------------------------- 1 | module QQBot 2 | class Group 3 | attr_accessor :name, :code, :id, :markname 4 | end 5 | 6 | class Friend 7 | attr_accessor :nickname, :category_id, :id, :is_vip, :vip_level, :markname 8 | end 9 | 10 | class Category 11 | attr_accessor :name, :sort, :id, :friends 12 | end 13 | 14 | class Discuss 15 | attr_accessor :name, :id 16 | end 17 | 18 | class Message 19 | attr_accessor :type, :from_id, :send_id, :time, :content, :font 20 | end 21 | 22 | class Font 23 | attr_accessor :color, :name, :size, :style 24 | end 25 | 26 | class UserInfo 27 | attr_accessor :phone, :occupation, :college, :id, :blood, :slogan, :homepage, :vip_info, :city, :country, :province, :personal, :shengxiao, :nickname, :email, :account, :gender, :mobile, :birthday 28 | end 29 | 30 | class Birthday 31 | attr_accessor :year, :month, :day 32 | end 33 | 34 | class Recent 35 | attr_accessor :id, :type 36 | end 37 | 38 | class Online 39 | attr_accessor :id, :client_type 40 | end 41 | 42 | class GroupMember 43 | attr_accessor :id, :nickname, :gender, :country, :city, :markname, :is_vip, :vip_level, :client_type, :status 44 | end 45 | 46 | class GroupInfo 47 | attr_accessor :id, :create_time, :memo, :name, :owner_id, :markname, :members 48 | end 49 | 50 | class DiscussInfo 51 | attr_accessor :id, :name, :members 52 | end 53 | 54 | class DiscussMember 55 | attr_accessor :id, :nickname, :client_type, :status 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/qqbot/version.rb: -------------------------------------------------------------------------------- 1 | module QQBot 2 | VERSION = "0.1.6" 3 | end 4 | -------------------------------------------------------------------------------- /qqbot.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'qqbot/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "qqbot" 8 | spec.version = QQBot::VERSION 9 | spec.authors = ["ScienJus"] 10 | spec.email = ["i@scienjus.com"] 11 | 12 | spec.summary = %q{a qq robot based on smart qq api.} 13 | spec.description = %q{a qq robot based on smart qq api.} 14 | spec.homepage = "https://github.com/ScienJus/qqbot" 15 | spec.license = "MIT" 16 | 17 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 18 | # delete this section to allow pushing this gem to any host. 19 | if spec.respond_to?(:metadata) 20 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 21 | else 22 | raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 23 | end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_development_dependency "bundler", "~> 1.11" 31 | spec.add_development_dependency "rake", "~> 10.0" 32 | spec.add_development_dependency "rspec", "~> 3.0" 33 | end 34 | -------------------------------------------------------------------------------- /spec/qqbot_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Qqbot do 4 | it 'has a version number' do 5 | expect(Qqbot::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'qqbot' 3 | --------------------------------------------------------------------------------