├── README.md ├── http_client.go └── smartqq.go /README.md: -------------------------------------------------------------------------------- 1 | # SmartQQ By Golang 2 | SmartQQ Package By Golang. 3 | ##简介 4 | 1.基于 Smart QQ(Web QQ) 的 Api 封装,你可以用这个 Api 制作属于自己的 QQ 机器人! 5 | 2.提供的接口包括接收QQ好友、QQ群、QQ讨论组消息,并可以主动或被动的发送消息给好友、群、讨论组。
6 | 3.用数组或其他方式保存QClient对象,可以实现批量QQ登录及收发消息。 7 | 4.更多有趣的用途请自行脑洞吧。 8 | ##依赖 9 | 1.因为SmartQQ登录验证及收发消息接口较为复杂,所以使用的是自己封装的Http-Client包(已经集成在本包中),稍后会开源出来。
10 | 2.另外因为qq返回的json有点小复杂,所以使用了bitly的SimpleJson包来解析QQ返回的json字符串,请手动go get github.com/bitly/go-simplejson 11 | ##进度 12 | 1.当前版本只提供了登录验证、收发消息的接口。
13 | 2.未来会逐步实现查询QQ好友列表、聊天记录等接口。 14 | ##不足 15 | 1.SmartQQ不支持收发图片、语音、视屏、及附件。
16 | 2.SmartQQ接口不够稳定,有时候发送消息会返回失败但实际上是发送成功了的,有时候会发送失败但是返回成功...万恶的TX
17 | 3.截止2016年4月7日接口可用。 18 | ##使用方法 19 | 先get source 20 | ```cmd 21 | go get github.com/bitly/go-simplejson 22 | go get github.com/JamesWone/SmartQQ 23 | ``` 24 | 然后,直接见Demo吧! 25 | ##Demo 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "os" 32 | "strings" 33 | 34 | "github.com/JamesWone/SmartQQ" 35 | ) 36 | 37 | //使用自己封装的Http-Client包 38 | var client_turing smartqq.Client = smartqq.Client{ 39 | IsKeepCookie: true, 40 | Timeout: 5, 41 | } 42 | 43 | //调用图灵机器人Api 44 | func getResponseByTuringRobot(request string) string { 45 | resp_turing, err := client_turing.Post("http://www.niurenqushi.com/app/simsimi/ajax.aspx", "txt="+request) 46 | if err != nil { 47 | return "" 48 | } 49 | return resp_turing.Body 50 | } 51 | 52 | func main() { 53 | //初始化一个QClient 54 | client := smartqq.QClient{} 55 | //当二维码图片变动后触发 56 | client.OnQRChange(func(qc *smartqq.QClient, image_bin []byte) { 57 | //将二维码保存至当前目录,打开手机QQ扫描二维码后即可登录成功 58 | fmt.Println("正在保存二维码图片.") 59 | file_image, err := os.OpenFile("v.jpg", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 60 | if err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | defer file_image.Close() 65 | if _, err := file_image.Write(image_bin); err != nil { 66 | fmt.Println(err) 67 | return 68 | } 69 | }) 70 | //当登录成功后触发 71 | client.OnLogined(func(qc *smartqq.QClient) { 72 | fmt.Println("登录成功了!") 73 | }) 74 | //当收到消息后触发 75 | client.OnMessage(func(qc *smartqq.QClient, qm smartqq.QMessage) { 76 | fmt.Println("收到新消息了:") 77 | fmt.Println(qm) 78 | content := qm.Content 79 | if strings.Contains(qm.Content, "@ai") { 80 | content = strings.Replace(qm.Content, "@ai", "", 1) 81 | switch qm.Poll_type { 82 | //QQ好友消息 83 | case "message": 84 | //发送给QQ好友 85 | qc.SendToQQ(qm.From_uin, getResponseByTuringRobot(content)+"\n(by:ai)") 86 | //QQ群消息 87 | case "group_message": 88 | //发送给QQ群 89 | qc.SendToGroup(qm.From_uin, getResponseByTuringRobot(content)+"\n(by:ai)") 90 | //讨论组消息 91 | case "discu_message": 92 | //发送给讨论组 93 | qc.SendToDiscuss(qm.From_uin, getResponseByTuringRobot(content)+"\n(by:ai)") 94 | } 95 | } 96 | }) 97 | fmt.Println("开始登录.") 98 | //开始登录,并自动收发消息 99 | client.Run() 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /http_client.go: -------------------------------------------------------------------------------- 1 | package smartqq 2 | 3 | import ( 4 | "compress/gzip" 5 | "errors" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Client struct { 14 | IsKeepCookie bool 15 | Timeout int 16 | Cookies map[string]string 17 | Header map[string]string 18 | } 19 | 20 | type Response struct { 21 | StatusCode int 22 | Body string 23 | Header *http.Header 24 | Cookies []*http.Cookie 25 | } 26 | 27 | func (client *Client) request(method string, url string, data string) (response Response, err error) { 28 | //初始化client 29 | if client.Cookies == nil { 30 | client.Cookies = map[string]string{} 31 | } 32 | if client.Header == nil { 33 | client.Header = map[string]string{} 34 | } 35 | if client.Timeout == 0 { 36 | client.Timeout = 30 37 | } 38 | //初始化http.Client 39 | var DefaultTransport http.RoundTripper = &http.Transport{ 40 | Dial: func(netw, addr string) (net.Conn, error) { 41 | conn, err := net.DialTimeout(netw, addr, time.Duration(client.Timeout)*time.Second) 42 | if err != nil { 43 | return nil, err 44 | } 45 | conn.SetDeadline(time.Now().Add(time.Duration(client.Timeout) * time.Second)) 46 | return conn, nil 47 | }, 48 | ResponseHeaderTimeout: time.Duration(client.Timeout) * time.Second, 49 | } 50 | 51 | request_get, err := http.NewRequest(method, url, strings.NewReader(data)) 52 | if err != nil { 53 | return response, err 54 | } 55 | 56 | //设置请求的Header 57 | for k, _ := range client.Header { 58 | request_get.Header.Set(k, client.Header[k]) 59 | } 60 | //解析Cookie map数据为字符串 61 | if cookiestr, ok := client.Header["Cookie"]; ok { 62 | cookiestr = strings.Replace(cookiestr, " ", "", 10) 63 | cookiestr = strings.Replace(cookiestr, "\t", "", 10) 64 | cookiestr = strings.Replace(cookiestr, "\n", "", 10) 65 | cookie_item := strings.Split(cookiestr, ";") 66 | for k, _ := range cookie_item { 67 | cookie_item_sp := strings.Split(cookie_item[k], "=") 68 | if len(cookie_item_sp) == 2 { 69 | client.Cookies[cookie_item_sp[0]] = cookie_item_sp[1] 70 | } 71 | } 72 | } 73 | cookie_str := "" 74 | for k, _ := range client.Cookies { 75 | cookie_str += k + "=" + client.Cookies[k] + "; " 76 | } 77 | //设置请求的Cookie 78 | request_get.Header.Set("Cookie", cookie_str) 79 | 80 | //防止因为没有Content-Type,而导致提交POST数据失败 81 | if method == "POST" { 82 | if content_type, ok := client.Header["Content-Type"]; !ok || content_type == "" { 83 | request_get.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") 84 | } 85 | } 86 | 87 | //发送请求 88 | response_get, err := DefaultTransport.RoundTrip(request_get) 89 | 90 | //检查请求错误 91 | if err != nil { 92 | return response, errors.New("Get Error," + err.Error()) 93 | } 94 | 95 | response.StatusCode = response_get.StatusCode 96 | 97 | //释放response.body对象,防止内存泄露 98 | defer response_get.Body.Close() 99 | 100 | //如果IsKeepCookie为true则保存Cookie状态 101 | if client.IsKeepCookie == true { 102 | response_cookie := response_get.Cookies() 103 | for k, _ := range response_cookie { 104 | if response_cookie[k].Value != "" { 105 | client.Cookies[response_cookie[k].Name] = response_cookie[k].Value 106 | } 107 | } 108 | } 109 | 110 | var body_bin []byte 111 | switch response_get.Header.Get("Content-Encoding") { 112 | case "gzip": 113 | reader, _ := gzip.NewReader(response_get.Body) 114 | body_bin, _ = ioutil.ReadAll(reader) 115 | default: 116 | body_bin, _ = ioutil.ReadAll(response_get.Body) 117 | } 118 | 119 | //返回数据 120 | response.Body = string(body_bin) 121 | response.Header = &response_get.Header 122 | response.Cookies = response_get.Cookies() 123 | 124 | return response, nil 125 | } 126 | 127 | func (client *Client) Get(url string) (Response, error) { 128 | return client.request("GET", url, "") 129 | } 130 | 131 | func (client *Client) Post(url string, data string) (Response, error) { 132 | return client.request("POST", url, data) 133 | } 134 | -------------------------------------------------------------------------------- /smartqq.go: -------------------------------------------------------------------------------- 1 | package smartqq 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bitly/go-simplejson" 11 | ) 12 | 13 | type QClient struct { 14 | IsLogin bool 15 | HttpClient *Client 16 | onQRChange func(*QClient, []byte) 17 | onLogined func(*QClient) 18 | onMessage func(*QClient, QMessage) 19 | parameter map[string]string 20 | lastSendTime int64 21 | // sendLock sync.Mutex 22 | } 23 | 24 | type QMessage struct { 25 | Poll_type string 26 | From_uin int 27 | Send_uin int 28 | To_uin int 29 | Msg_id int 30 | Content string 31 | Retcode int 32 | Time int 33 | } 34 | 35 | func (qc *QClient) OnQRChange(fun func(*QClient, []byte)) { 36 | qc.onQRChange = fun 37 | } 38 | 39 | func (qc *QClient) OnLogined(fun func(*QClient)) { 40 | qc.onLogined = fun 41 | } 42 | 43 | func (qc *QClient) OnMessage(fun func(*QClient, QMessage)) { 44 | qc.onMessage = fun 45 | } 46 | 47 | func (qc *QClient) SendToQQ(from_uin int, message string) error { 48 | qc.sendMsg("to", from_uin, message) 49 | return nil 50 | } 51 | 52 | func (qc *QClient) SendToGroup(from_uin int, message string) error { 53 | qc.sendMsg("group_uin", from_uin, message) 54 | return nil 55 | } 56 | 57 | func (qc *QClient) SendToDiscuss(from_uin int, message string) error { 58 | qc.sendMsg("did", from_uin, message) 59 | return nil 60 | } 61 | 62 | func (qc *QClient) pollMessage() { 63 | ierr := 0 64 | client := qc.HttpClient 65 | for { 66 | if ierr > 5 { 67 | return 68 | } 69 | client.Header["Origin"] = "http://d1.web2.qq.com" 70 | client.Header["Referer"] = "http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2" 71 | resp_poll, err := client.Post("http://d1.web2.qq.com/channel/poll2", `r={"ptwebqq":"`+qc.parameter["ptwebqq"]+`","clientid":53999199,"psessionid":"`+qc.parameter["psessionid"]+`","key":""}`) 72 | //Timeout与容错处理,如果连续出错超过5次则返回错误 73 | if err != nil { 74 | if strings.Contains(err.Error(), "timeout") { 75 | ierr = 0 76 | continue 77 | } else { 78 | fmt.Println("Poll Error,", err.Error()) 79 | ierr++ 80 | continue 81 | } 82 | } 83 | recode, err := parseMessage(qc, resp_poll.Body) 84 | if err != nil { 85 | if recode == 103 { 86 | fmt.Println("PollMessage Err,code 103,请登录网页版SmartQQ(http://w.qq.com)然后退出,方可恢复。") 87 | return 88 | } else { 89 | fmt.Println("PollMessage Err,code:", recode, ",err:", err) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func parseMessage(qc *QClient, json_str string) (int, error) { 96 | sj, err := simplejson.NewJson([]byte(json_str)) 97 | if err != nil { 98 | return -1, err 99 | } 100 | retcode, err := sj.Get("retcode").Int() 101 | if err != nil { 102 | return -1, err 103 | } 104 | if retcode != 0 { 105 | errmsg, err := sj.Get("errmsg").String() 106 | if err != nil { 107 | fmt.Println(err) 108 | return retcode, err 109 | } 110 | // fmt.Println("Retcode:", retcode, ",errmsg:", errmsg) 111 | return retcode, errors.New("errmsg:" + errmsg) 112 | } 113 | poll_type, err := sj.Get("result").GetIndex(0).Get("poll_type").String() 114 | if err != nil { 115 | return -1, err 116 | } 117 | sj_value := sj.Get("result").GetIndex(0).Get("value") 118 | sj_content := sj_value.Get("content") 119 | sj_content_arr, err := sj_content.Array() 120 | if err != nil { 121 | return -1, err 122 | } 123 | len_content := len(sj_content_arr) 124 | content := "" 125 | for i := 0; i < len_content; i++ { 126 | if msg_part, err := sj_content.GetIndex(i).String(); err == nil { 127 | content += msg_part 128 | } 129 | } 130 | from_uin, err := sj_value.Get("from_uin").Int() 131 | if err != nil { 132 | return -1, err 133 | } 134 | send_uin := 0 135 | if poll_type == "group_message" || poll_type == "discu_message" { 136 | send_uin, err = sj_value.Get("send_uin").Int() 137 | } 138 | to_uin, err := sj_value.Get("to_uin").Int() 139 | if err != nil { 140 | return -1, err 141 | } 142 | msg_id, err := sj_value.Get("msg_id").Int() 143 | if err != nil { 144 | return -1, err 145 | } 146 | time, err := sj_value.Get("time").Int() 147 | if err != nil { 148 | return -1, err 149 | } 150 | qm := QMessage{ 151 | Poll_type: poll_type, 152 | From_uin: from_uin, 153 | Send_uin: send_uin, 154 | To_uin: to_uin, 155 | Msg_id: msg_id, 156 | Content: content, 157 | Time: time, 158 | } 159 | qc.onMessage(qc, qm) 160 | return retcode, nil 161 | } 162 | 163 | //msg_id加密算法 164 | var msg_num int64 = time.Now().Unix() % 1E4 * 1E4 165 | 166 | func (qc *QClient) sendMsg(sendType string, toUin int, msg string) { 167 | msg_num++ 168 | 169 | //为goroutine加锁,限制两次发送消息的时间必须大于2秒 ps:后来索性去除goroutine了 170 | // qc.sendLock.Lock() 171 | // if time.Now().Unix()-qc.lastSendTime < 3 { 172 | // time.Sleep(2 * time.Second) 173 | // } 174 | // qc.lastSendTime = time.Now().Unix() 175 | // qc.sendLock.Unlock() 176 | 177 | qc.HttpClient.Header["Origin"] = "http://d1.web2.qq.com" 178 | qc.HttpClient.Header["Referer"] = "http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2" 179 | send_data := `{"` + sendType + `":` + fmt.Sprintf("%d", toUin) + `,"content":"[\"` + msg + `\",[\"font\",{\"name\":\"宋体\",\"size\":10,\"style\":[0,0,0],\"color\":\"000000\"}]]","face":528,"clientid":53999199,"msg_id":` + fmt.Sprint(msg_num) + `,"psessionid":"` + qc.parameter["psessionid"] + `"}` 180 | send_url := "" 181 | switch sendType { 182 | case "to": 183 | send_url = "http://d1.web2.qq.com/channel/send_buddy_msg2" 184 | case "group_uin": 185 | send_url = "http://d1.web2.qq.com/channel/send_qun_msg2" 186 | case "did": 187 | send_url = "http://d1.web2.qq.com/channel/send_discu_msg2" 188 | default: 189 | return 190 | } 191 | resp_send, err := qc.HttpClient.Post(send_url, "r="+send_data) 192 | // fmt.Println(send_data) 193 | if err != nil { 194 | fmt.Println(err.Error()) 195 | return 196 | } 197 | result, err := simplejson.NewJson([]byte(resp_send.Body)) 198 | if err == nil { 199 | if retcode, err := result.Get("retcode").Int(); err == nil && retcode == 100001 { 200 | qc.sendMsg(sendType, toUin, msg) 201 | } 202 | } 203 | // fmt.Println(resp_send.Body) 204 | } 205 | 206 | func (qc *QClient) Run() { 207 | client := Client{ 208 | IsKeepCookie: true, 209 | Header: map[string]string{ 210 | "Host": "d1.web2.qq.com", 211 | "Connection": "keep-alive", 212 | "Cache-Control": "max-age=0", 213 | "Accept": "*/*", 214 | "Upgrade-Insecure-Requests": "1", 215 | "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", 216 | "DNT": "1", 217 | "Accept-Encoding": "gzip, deflate", 218 | "Accept-Language": "zh-CN,zh;q=0.8", 219 | }, 220 | Cookies: map[string]string{ 221 | "pgv_info": "ssid=s3883232818", 222 | "pgv_id": "2677229032", 223 | }, 224 | } 225 | 226 | if qc.parameter == nil { 227 | qc.parameter = map[string]string{} 228 | } 229 | 230 | qc.HttpClient = &client 231 | 232 | client.Get("https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=16&mibao_css=m_webqq&appid=501004106&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fw.qq.com%2Fproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20131024001") 233 | resp_image, err := client.Get("https://ssl.ptlogin2.qq.com/ptqrshow?appid=501004106&e=0&l=M&s=5&d=72&v=4&t=0.1") 234 | if err != nil { 235 | fmt.Println(err) 236 | return 237 | } 238 | //回调:OnQRChange() 239 | qc.onQRChange(qc, []byte(resp_image.Body)) 240 | 241 | regexp_image_state := regexp.MustCompile(`ptuiCB\(\'(\d+)\'`) 242 | 243 | validate_login: 244 | for { 245 | resp_image_state, err := client.Get("https://ssl.ptlogin2.qq.com/ptqrlogin?webqq_type=10&remember_uin=1&login2qq=1&aid=501004106&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=0-0-157510&mibao_css=m_webqq&t=1&g=1&js_type=0&js_ver=10143&login_sig=&pt_randsalt=0") 246 | if err != nil { 247 | fmt.Println(err) 248 | return 249 | } 250 | switch code := regexp_image_state.FindAllStringSubmatch(resp_image_state.Body, -1)[0][1]; code { 251 | case "65": 252 | fmt.Println("二维码已失效.") 253 | resp_image, err = client.Get("https://ssl.ptlogin2.qq.com/ptqrshow?appid=501004106&e=0&l=M&s=5&d=72&v=4&t=0.1") 254 | if err != nil { 255 | fmt.Println(err) 256 | return 257 | } 258 | //回调:OnQRChange() 259 | qc.onQRChange(qc, []byte(resp_image.Body)) 260 | case "66": 261 | fmt.Println("二维码未失效.") 262 | case "67": 263 | fmt.Println("二维码正在验证..") 264 | case "0": 265 | fmt.Println("二维码验证成功!") 266 | sig_link := "" 267 | if reg_sig := regexp.MustCompile(`ptuiCB\(\'0\',\'0\',\'([^\']+)\'`).FindAllStringSubmatch(resp_image_state.Body, -1); len(reg_sig) == 1 { 268 | sig_link = reg_sig[0][1] 269 | } else { 270 | fmt.Println("Check Sig Err:") 271 | return 272 | } 273 | resp_check_sig, err := client.Get(sig_link) 274 | if resp_check_sig.StatusCode != 302 { 275 | fmt.Println("Get Err:", err.Error()) 276 | return 277 | } 278 | break validate_login 279 | default: 280 | fmt.Println("未知状态(" + code + ")") 281 | return 282 | } 283 | time.Sleep(time.Second) 284 | } 285 | 286 | //获取ptwebqq 287 | client.Get("http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1") 288 | //获取vfwebqq 289 | client.Header["Referer"] = "http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1" 290 | resp_vfwebqq, err := client.Get("http://s.web2.qq.com/api/getvfwebqq?ptwebqq=" + client.Cookies["ptwebqq"] + "&clientid=53999199&psessionid=&t=0.1") 291 | if err != nil { 292 | fmt.Println(err) 293 | return 294 | } 295 | sp_vfweb, err := simplejson.NewJson([]byte(resp_vfwebqq.Body)) 296 | if vf_code_int, _ := sp_vfweb.Get("retcode").Int(); vf_code_int != 0 { 297 | return 298 | } 299 | vfwebqq, err := sp_vfweb.Get("result").Get("vfwebqq").String() 300 | if err != nil { 301 | return 302 | } 303 | qc.parameter["vfwebqq"] = vfwebqq 304 | 305 | //获取psessionid 306 | client.Header["Referer"] = "http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2" 307 | resp_psession, err := client.Post("http://d1.web2.qq.com/channel/login2", `r={"ptwebqq":"`+client.Cookies["ptwebqq"]+`","clientid":53999199,"psessionid":"","status":"online"}`) 308 | if err != nil { 309 | fmt.Println(err) 310 | return 311 | } 312 | if psessionid := regexp.MustCompile(`\"psessionid\"\:\"([^\"]+)\"`).FindAllStringSubmatch(resp_psession.Body, -1); len(psessionid) == 1 { 313 | qc.parameter["psessionid"] = psessionid[0][1] 314 | } else { 315 | return 316 | } 317 | 318 | if uin := regexp.MustCompile(`\"uin\"\:([\d]+),\"`).FindAllStringSubmatch(resp_psession.Body, -1); len(uin) == 1 { 319 | qc.parameter["uin"] = uin[0][1] 320 | } else { 321 | return 322 | } 323 | 324 | //回调:qc.OnLogined() 325 | if qc.onLogined != nil { 326 | qc.onLogined(qc) 327 | } 328 | //开始轮训新消息 329 | if qc.onMessage != nil { 330 | qc.pollMessage() 331 | } 332 | 333 | } 334 | --------------------------------------------------------------------------------