├── 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 |
--------------------------------------------------------------------------------