├── README.md ├── authenticator.go ├── constants.go ├── contrib └── get_statuses.go ├── examples ├── auth.go ├── goroutines.go ├── sample.jpg └── weibo.go ├── license.txt ├── structs.go └── weibo.go /README.md: -------------------------------------------------------------------------------- 1 | gobo 2 | ==== 3 | 4 | 新浪微博Go语言SDK,支持所有微博API功能 5 | 6 | # 安装/更新 7 | 8 | ``` 9 | go get -u github.com/huichen/gobo 10 | ``` 11 | 12 | # 使用 13 | 14 | 抓取@人民日报的最近10条微博: 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "github.com/huichen/gobo" 23 | ) 24 | 25 | var ( 26 | weibo = gobo.Weibo{} 27 | access_token = flag.String("access_token", "", "用户的访问令牌") 28 | ) 29 | 30 | func main() { 31 | // 解析命令行参数 32 | flag.Parse() 33 | 34 | // 调用API 35 | var statuses gobo.Statuses 36 | params := gobo.Params{"screen_name": "人民日报", "count": 10} 37 | err := weibo.Call("statuses/user_timeline", "get", *access_token, params, &statuses) 38 | 39 | // 处理返回结果 40 | if err != nil { 41 | fmt.Println(err) 42 | return 43 | } 44 | for _, status := range statuses.Statuses { 45 | fmt.Println(status.Text) 46 | } 47 | } 48 | ``` 49 | 50 | 用命令行参数-access_token传入访问令牌,令牌可以通过API测试工具或者gobo.Authenticator得到。 51 | 52 | 更多API调用的例子见 examples/weibo.go。 53 | -------------------------------------------------------------------------------- /authenticator.go: -------------------------------------------------------------------------------- 1 | package gobo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | // Authenticator结构体实现了微博应用授权功能 12 | type Authenticator struct { 13 | redirectUri string 14 | clientId string 15 | clientSecret string 16 | initialized bool 17 | httpClient *http.Client 18 | } 19 | 20 | // 初始化结构体 21 | // 22 | // 在调用其它函数之前必须首先初始化。 23 | func (auth *Authenticator) Init(redirectUri string, clientId string, clientSecret string) error { 24 | // 检查结构体是否已经初始化 25 | if auth.initialized { 26 | return &ErrorString{"Authenticator结构体已经初始化"} 27 | } 28 | 29 | auth.redirectUri = redirectUri 30 | auth.clientId = clientId 31 | auth.clientSecret = clientSecret 32 | auth.httpClient = new(http.Client) 33 | auth.initialized = true 34 | return nil 35 | } 36 | 37 | // 得到授权URI 38 | func (auth *Authenticator) Authorize() (string, error) { 39 | // 检查结构体是否初始化 40 | if !auth.initialized { 41 | return "", &ErrorString{"Authenticator结构体尚未初始化"} 42 | } 43 | 44 | return fmt.Sprintf("%s/oauth2/authorize?redirect_uri=%s&response_type=code&client_id=%s", ApiDomain, auth.redirectUri, auth.clientId), nil 45 | } 46 | 47 | // 从授权码得到访问令牌 48 | func (auth *Authenticator) AccessToken(code string) (AccessToken, error) { 49 | // 检查结构体是否初始化 50 | token := AccessToken{} 51 | if !auth.initialized { 52 | return token, &ErrorString{"Authenticator结构体尚未初始化"} 53 | } 54 | 55 | // 生成请求URI 56 | queries := url.Values{} 57 | queries.Add("client_id", auth.clientId) 58 | queries.Add("client_secret", auth.clientSecret) 59 | queries.Add("redirect_uri", auth.redirectUri) 60 | queries.Add("grant_type", "authorization_code") 61 | queries.Add("code", code) 62 | 63 | // 发送请求 64 | err := auth.sendPostHttpRequest("oauth2/access_token", queries, &token) 65 | return token, err 66 | } 67 | 68 | // 得到访问令牌对应的信息 69 | func (auth *Authenticator) GetTokenInfo(token string) (AccessTokenInfo, error) { 70 | // 检查结构体是否初始化 71 | info := AccessTokenInfo{} 72 | if !auth.initialized { 73 | return info, &ErrorString{"Authenticator结构体尚未初始化"} 74 | } 75 | 76 | // 生成请求URI 77 | queries := url.Values{} 78 | queries.Add("access_token", token) 79 | 80 | // 发送请求 81 | err := auth.sendPostHttpRequest("oauth2/get_token_info", queries, &info) 82 | return info, err 83 | } 84 | 85 | // 解除访问令牌的授权 86 | func (auth *Authenticator) Revokeoauth2(token string) error { 87 | // 检查结构体是否初始化 88 | if !auth.initialized { 89 | return &ErrorString{"Authenticator结构体尚未初始化"} 90 | } 91 | 92 | // 生成请求URI 93 | queries := url.Values{} 94 | queries.Add("access_token", token) 95 | 96 | // 发送请求 97 | type Result struct { 98 | Result string 99 | } 100 | var result Result 101 | err := auth.sendPostHttpRequest("oauth2/revokeoauth2", queries, &result) 102 | return err 103 | } 104 | 105 | func (auth *Authenticator) sendPostHttpRequest(apiName string, queries url.Values, response interface{}) error { 106 | // 生成请求URI 107 | requestUri := fmt.Sprintf("%s/%s", ApiDomain, apiName) 108 | 109 | // 发送POST Form请求 110 | resp, err := auth.httpClient.PostForm(requestUri, queries) 111 | if err != nil { 112 | return err 113 | } 114 | defer resp.Body.Close() 115 | 116 | // 解析返回内容 117 | bytes, _ := ioutil.ReadAll(resp.Body) 118 | if resp.StatusCode == 200 { 119 | err := json.Unmarshal(bytes, &response) 120 | if err != nil { 121 | return err 122 | } 123 | } else { 124 | var weiboErr WeiboError 125 | err := json.Unmarshal(bytes, &weiboErr) 126 | if err != nil { 127 | return err 128 | } 129 | return weiboErr 130 | } 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package gobo 2 | 3 | // 微博API相关的常数 4 | const ( 5 | ApiDomain string = "https://api.weibo.com" 6 | ApiVersion string = "2" 7 | UploadAPIName string = "statuses/upload" 8 | ApiNamePostfix string = ".json" 9 | ) 10 | -------------------------------------------------------------------------------- /contrib/get_statuses.go: -------------------------------------------------------------------------------- 1 | package contrib 2 | 3 | import ( 4 | "github.com/huichen/gobo" 5 | "math" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | const ( 11 | STATUSES_PER_PAGE = 100 12 | MAX_THREADS = 20 13 | ) 14 | 15 | // 并行抓取指定用户的微博 16 | // 17 | // 输入参数: 18 | // weibo gobo.Weibo结构体指针 19 | // access_token 用户的访问令牌 20 | // userName 微博用户名 21 | // userId 微博用户ID,注意仅当userName为空字符串时使用此值 22 | // numStatuses 需要抓取的总微博数,注意由于新浪的限制,最多只能抓取最近2000条微博,当此参数大于2000时取2000 23 | // timeout 超时退出,单位为毫秒,当值为0时不设超时 24 | // 25 | // 返回按照ID逆序排序的微博 26 | func GetStatuses(weibo *gobo.Weibo, access_token string, userName string, userId int64, numStatuses int, timeout int) ([]*gobo.Status, error) { 27 | // 检查输入参数的有效性 28 | if userName == "" && userId == 0 { 29 | return nil, &gobo.ErrorString{"userName和userId不可以都是无效值"} 30 | } 31 | 32 | // 计算需要启动的进程数 33 | if numStatuses <= 0 { 34 | return nil, &gobo.ErrorString{"抓取微博数必须大于零"} 35 | } 36 | numThreads := int(math.Ceil(float64(numStatuses) / STATUSES_PER_PAGE)) 37 | if numThreads > MAX_THREADS { 38 | numThreads = MAX_THREADS 39 | } 40 | 41 | // output通道中收集所有线程抓取的微博 42 | output := make(chan *gobo.Status, STATUSES_PER_PAGE*numThreads) 43 | 44 | // done通道中收集线程抓取微博的数目,并负责通知主线程是否全部子线程已经完成 45 | done := make(chan int, numThreads) 46 | 47 | // 启动子线程 48 | for i := 0; i < numThreads; i++ { 49 | // 开辟numThreads个新线程负责分页抓取微博 50 | go func(page int) { 51 | var posts gobo.Statuses 52 | var params gobo.Params 53 | if userName != "" { 54 | params = gobo.Params{"screen_name": userName, "count": STATUSES_PER_PAGE, "page": page} 55 | } else { 56 | params = gobo.Params{"uid": userId, "count": STATUSES_PER_PAGE, "page": page} 57 | } 58 | err := weibo.Call("statuses/user_timeline", "get", access_token, params, &posts) 59 | if err != nil { 60 | done <- 0 61 | return 62 | } 63 | for _, p := range posts.Statuses { 64 | select { 65 | case output <- p: 66 | default: 67 | } 68 | } 69 | done <- len(posts.Statuses) 70 | }(i + 1) 71 | } 72 | 73 | // 循环监听线程通道 74 | numCompletedThreads := 0 75 | numReceivedStatuses := 0 76 | numTotalStatuses := 0 77 | statuses := make([]*gobo.Status, 0, numThreads*STATUSES_PER_PAGE) // 长度为零但预留足够容量 78 | isTimeout := false 79 | t0 := time.Now() 80 | for { 81 | // 非阻塞监听output和done通道 82 | select { 83 | case status := <-output: 84 | statuses = append(statuses, status) 85 | numReceivedStatuses++ 86 | case numThreadStatuses := <-done: 87 | numCompletedThreads++ 88 | numTotalStatuses = numTotalStatuses + numThreadStatuses 89 | case <-time.After(time.Second): // 让子线程飞一会儿 90 | } 91 | 92 | // 超时退出 93 | if timeout > 0 { 94 | t1 := time.Now() 95 | if t1.Sub(t0).Nanoseconds() > int64(timeout)*1000000 { 96 | isTimeout = true 97 | break 98 | } 99 | } 100 | 101 | // 当所有线程完成并且从output通道收集齐全部微博时退出循环 102 | if numCompletedThreads == numThreads && numTotalStatuses == numReceivedStatuses { 103 | break 104 | } 105 | } 106 | 107 | if isTimeout { 108 | return nil, &gobo.ErrorString{"抓取超时"} 109 | } 110 | 111 | // 将所有的微博按照id顺序排序 112 | sort.Sort(StatusSlice(statuses)) 113 | 114 | // 删除掉重复的微博 115 | sortedStatuses := make([]*gobo.Status, 0, len(statuses)) 116 | numStatusesToReturn := 0 117 | for i := 0; i < len(statuses); i++ { 118 | // 跳过重复微博 119 | if i > 0 && statuses[i].Id == statuses[i-1].Id { 120 | continue 121 | } 122 | 123 | sortedStatuses = append(sortedStatuses, statuses[i]) 124 | numStatusesToReturn++ 125 | 126 | // 最多返回numStatuses条微博 127 | if numStatusesToReturn == numStatuses { 128 | break 129 | } 130 | } 131 | return sortedStatuses, nil 132 | } 133 | 134 | // 为了方便将微博排序定义下列结构体和成员函数 135 | 136 | type StatusSlice []*gobo.Status 137 | 138 | func (ss StatusSlice) Len() int { 139 | return len(ss) 140 | } 141 | func (ss StatusSlice) Swap(i, j int) { 142 | ss[i], ss[j] = ss[j], ss[i] 143 | } 144 | func (ss StatusSlice) Less(i, j int) bool { 145 | return ss[i].Id > ss[j].Id 146 | } 147 | -------------------------------------------------------------------------------- /examples/auth.go: -------------------------------------------------------------------------------- 1 | // 例子程序:微博应用授权 2 | // 展示功能包括得到授权URI,通过授权码得到访问令牌,获得令牌对应的信息和解除访问令牌授权等功能。 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "flag" 8 | "fmt" 9 | "github.com/huichen/gobo" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | redirect_uri = flag.String("redirect_uri", "", "应用的重定向地址") 16 | client_id = flag.String("client_id", "", "应用的client id") 17 | client_secret = flag.String("client_secret", "", "应用的client secret") 18 | auth = gobo.Authenticator{} 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | 24 | // 初始化 25 | err := auth.Init(*redirect_uri, *client_id, *client_secret) 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | 31 | // 得到重定向地址 32 | uri, err := auth.Authorize() 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | fmt.Printf("请在浏览器中打开下面地址\n%s\n", uri) 38 | 39 | // 从终端读取用户输入的认证码 40 | fmt.Print("请输入浏览器返回的授权码:") 41 | reader := bufio.NewReader(os.Stdin) 42 | input, _ := reader.ReadString('\n') 43 | code := strings.TrimSuffix(string([]byte(input)), "\n") 44 | 45 | // 从授权码得到token 46 | token, err := auth.AccessToken(code) 47 | if err != nil { 48 | fmt.Println(err) 49 | return 50 | } 51 | fmt.Printf("访问令牌 = %#v\n", token) 52 | 53 | // 从token得到相关信息 54 | info, err := auth.GetTokenInfo(token.Access_Token) 55 | if err != nil { 56 | fmt.Println(err) 57 | return 58 | } 59 | fmt.Printf("访问令牌信息 = %#v\n", info) 60 | 61 | // 解除token授权 62 | revokeErr := auth.Revokeoauth2(token.Access_Token) 63 | if revokeErr != nil { 64 | fmt.Println(revokeErr) 65 | return 66 | } 67 | fmt.Println("解除授权成功") 68 | } 69 | -------------------------------------------------------------------------------- /examples/goroutines.go: -------------------------------------------------------------------------------- 1 | // 例子程序:利用goroutines并行抓取微博 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "github.com/huichen/gobo" 8 | "github.com/huichen/gobo/contrib" 9 | "time" 10 | ) 11 | 12 | var ( 13 | access_token = flag.String("access_token", "", "用户的访问令牌") 14 | weibo = gobo.Weibo{} 15 | timeout = flag.Int("timeout", 0, "超时,单位毫秒") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | fmt.Println("==== 测试并行调用 statuses/user_timeline ====") 21 | 22 | // 记录初始时间 23 | t0 := time.Now() 24 | 25 | // 抓微博 26 | statuses, err := contrib.GetStatuses(&weibo, *access_token, 27 | "人民日报", // 微博用户名 28 | 0, // 微博用户ID,仅当用户名为空字符串时使用 29 | 211, // 抓取微博数 30 | *timeout) // 不设超时 31 | if err != nil { 32 | fmt.Println(err) 33 | return 34 | } 35 | fmt.Printf("抓取的总微博数 %d\n", len(statuses)) 36 | 37 | // 记录终止时间 38 | t1 := time.Now() 39 | fmt.Printf("并行抓取花费时间 %v\n", t1.Sub(t0)) 40 | 41 | // 打印最后五条微博内容 42 | for i, status := range statuses { 43 | if i == 5 { 44 | break 45 | } 46 | fmt.Println(status.Text) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huichen/gobo/a483eda6582e80e8cae69727294f3377f72029cd/examples/sample.jpg -------------------------------------------------------------------------------- /examples/weibo.go: -------------------------------------------------------------------------------- 1 | // 例子程序:调用微博API 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "github.com/huichen/gobo" 8 | "math/rand" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | var ( 16 | access_token = flag.String("access_token", "", "用户的访问令牌") 17 | image = flag.String("image", "", "上传图片的位置") 18 | random = rand.New(rand.NewSource(time.Now().UnixNano())) 19 | weibo = gobo.Weibo{} 20 | ) 21 | 22 | func showUser() { 23 | fmt.Println("==== 测试 users/show ====") 24 | var user gobo.User 25 | params := gobo.Params{"screen_name": "人民日报"} 26 | err := weibo.Call("users/show", "get", *access_token, params, &user) 27 | if err != nil { 28 | fmt.Println(err) 29 | } else { 30 | fmt.Printf("%#v\n", user) 31 | } 32 | } 33 | 34 | func getFriendsStatuses() { 35 | fmt.Println("==== 测试 statuses/friends_timeline ====") 36 | var statuses gobo.Statuses 37 | params := gobo.Params{"count": 10} 38 | err := weibo.Call("statuses/friends_timeline", "get", *access_token, params, &statuses) 39 | if err != nil { 40 | fmt.Println(err) 41 | } else { 42 | for _, status := range statuses.Statuses { 43 | fmt.Println(status.Text) 44 | } 45 | } 46 | } 47 | 48 | func getUserStatus() { 49 | fmt.Println("==== 测试 statuses/user_timeline ====") 50 | var statuses gobo.Statuses 51 | params := gobo.Params{"screen_name": "人民日报", "count": 1} 52 | err := weibo.Call("statuses/user_timeline", "get", *access_token, params, &statuses) 53 | if err != nil { 54 | fmt.Println(err) 55 | } else if len(statuses.Statuses) > 0 { 56 | fmt.Printf("%#v\n", statuses.Statuses[0]) 57 | } 58 | } 59 | 60 | func updateStatus() { 61 | fmt.Println("==== 测试 statuses/update ====") 62 | var status gobo.Status 63 | params := gobo.Params{"status": "测试" + strconv.Itoa(rand.Int())} 64 | err := weibo.Call("statuses/update", "status", *access_token, params, &status) 65 | if err != nil { 66 | fmt.Println(err) 67 | } else { 68 | fmt.Printf("%#v\n", status) 69 | } 70 | } 71 | 72 | func uploadStatus() { 73 | fmt.Println("==== 测试 statuses/upload ====") 74 | var status gobo.Status 75 | params := gobo.Params{"status": "测试" + strconv.Itoa(rand.Int())} 76 | img, err := os.Open(*image) 77 | if err != nil { 78 | fmt.Println(err) 79 | } 80 | err = weibo.Upload(*access_token, params, img, filepath.Ext(*image), &status) 81 | if err != nil { 82 | fmt.Println(err) 83 | } else { 84 | fmt.Printf("%#v\n", status) 85 | } 86 | } 87 | 88 | func main() { 89 | flag.Parse() 90 | showUser() 91 | getFriendsStatuses() 92 | getUserStatus() 93 | //updateStatus() 94 | //uploadStatus() 95 | } 96 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Hui Chen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package gobo 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // 微博API返回对象数据结构 8 | // 9 | // 结构体根据下面的文档定义 10 | // http://open.weibo.com/wiki/常见返回对象数据结构 11 | // 12 | // JSON字段名和golang结构体字段名有这样的一一对应关系 13 | // 14 | // JSON字段: golang结构体字段: 15 | // name_of_a_field Name_Of_A_Field 16 | 17 | type Status struct { 18 | Created_At string 19 | Id int64 20 | Mid string 21 | Text string 22 | Idstr string 23 | Source string 24 | Favorited bool 25 | Trucated bool 26 | In_Reply_To_Status_Id string 27 | In_Reply_To_User_Id string 28 | In_Reply_To_Screen_Name string 29 | Thumbnail_Pic string 30 | Bmiddle_Pic string 31 | Original_Pic string 32 | Geo *Geo 33 | User *User 34 | Retweeted_Status *Status 35 | Reposts_Count int 36 | Comments_Count int 37 | Attitudes_Count int 38 | Mlevel int 39 | Visible *Visible 40 | Pic_Urls []*Pic_Url 41 | } 42 | 43 | type Comment struct { 44 | Created_At string 45 | Id int64 46 | Text string 47 | Source string 48 | User *User 49 | Mid string 50 | Idstr string 51 | Status string 52 | Reply_Comment *Comment 53 | } 54 | 55 | type User struct { 56 | Id int64 57 | Idstr string 58 | Screen_Name string 59 | Name string 60 | Province string 61 | City string 62 | Location string 63 | Description string 64 | Url string 65 | Profile_Image_Url string 66 | Profile_Url string 67 | Domain string 68 | Weihao string 69 | Gender string 70 | Followers_Count int 71 | Friends_Count int 72 | Statuses_Count int 73 | Favourites_Count int 74 | Created_At string 75 | Following bool 76 | Allow_All_Act_Msg bool 77 | Geo_Enabled bool 78 | Verified bool 79 | Verified_Type int 80 | Remark string 81 | Status *Status 82 | Allow_All_Comment bool 83 | Avatar_Large string 84 | Verified_Reason string 85 | Follow_Me bool 86 | Online_Status int 87 | Bi_Followers_Count int 88 | Lang string 89 | } 90 | 91 | type Privacy struct { 92 | Comment int 93 | Geo int 94 | Message int 95 | Realname int 96 | Badge int 97 | Mobile int 98 | Webim int 99 | } 100 | 101 | type Remind struct { 102 | Status int 103 | Follower int 104 | Cmt int 105 | Dm int 106 | Mention_Status int 107 | Mention_Cmt int 108 | Group int 109 | Private_Group int 110 | Notice int 111 | Invite int 112 | Badge int 113 | Photo int 114 | } 115 | 116 | type Url_Short struct { 117 | Url_Short string 118 | Url_Long string 119 | Type int 120 | Result bool 121 | } 122 | 123 | type Geo struct { 124 | Longitude string 125 | Latitude string 126 | City string 127 | Province string 128 | City_Name string 129 | Province_Name string 130 | Address string 131 | Pinyin string 132 | More string 133 | } 134 | 135 | // 其他的常用结构体 136 | 137 | type ErrorString struct { 138 | S string 139 | } 140 | 141 | func (e *ErrorString) Error() string { 142 | return "Gobo错误:" + e.S 143 | } 144 | 145 | type WeiboError struct { 146 | Err string `json:"Error"` 147 | Error_Code int64 148 | Request string 149 | } 150 | 151 | func (e WeiboError) Error() string { 152 | return fmt.Sprintf("微博API访问错误 %d [%s] %s", e.Error_Code, e.Request, e.Err) 153 | } 154 | 155 | type AccessToken struct { 156 | Access_Token string 157 | Remind_In string 158 | Expires_In int 159 | Uid string 160 | } 161 | 162 | type AccessTokenInfo struct { 163 | Uid int64 164 | Appkey string 165 | Scope string 166 | Created_At int 167 | Expire_In int 168 | } 169 | 170 | type Statuses struct { 171 | Statuses []*Status 172 | } 173 | 174 | type Visible struct { 175 | Type int 176 | List_Id int 177 | } 178 | 179 | type Pic_Url struct { 180 | Thumbnail_Pic string 181 | } 182 | -------------------------------------------------------------------------------- /weibo.go: -------------------------------------------------------------------------------- 1 | package gobo 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | // Params类型用来表达微博API的JSON输入参数。注意: 15 | // 1. Params不应当包含访问令牌(access_token),因为它已经是Call和Upload函数的参数 16 | // 2. 在Upload函数中,Params参数不应当包含pic参数,上传的图片内容和类型应当通过reader和imageFormat指定 17 | type Params map[string]interface{} 18 | 19 | // Weibo结构体定义了微博API调用功能 20 | type Weibo struct { 21 | httpClient http.Client 22 | } 23 | 24 | // 调用微博API 25 | // 26 | // 该函数可用来调用除了statuses/upload(见Upload函数)和微博授权(见Authenticator结构体)外的所有微博API。 27 | // 28 | // 输入参数 29 | // method API方法名,比如 "/statuses/user_timeline" 又如 "comments/show" 30 | // httpMethod HTTP请求方式,只能是"get"或者"post"之一,否则出错 31 | // token 用户授权的访问令牌 32 | // params JSON输入参数,见Params结构体的注释 33 | // response API服务器的JSON输出将被还原成该结构体 34 | // 35 | // 当出现异常时输出非nil错误 36 | func (weibo *Weibo) Call(method string, httpMethod string, token string, params Params, response interface{}) error { 37 | apiUri := fmt.Sprintf("%s/%s/%s%s", ApiDomain, ApiVersion, method, ApiNamePostfix) 38 | if httpMethod == "get" { 39 | return weibo.sendGetHttpRequest(apiUri, token, params, response) 40 | } else if httpMethod == "post" { 41 | return weibo.sendPostHttpRequest(apiUri, token, params, nil, "", response) 42 | } 43 | return &ErrorString{"HTTP方法只能是\"get\"或者\"post\""} 44 | } 45 | 46 | // 调用/statuses/upload发带图片微博 47 | // 48 | // 输入参数 49 | // token 用户授权的访问令牌 50 | // params JSON输入参数,见Params结构体的注释 51 | // reader 包含图片的二进制流 52 | // imageFormat 图片的格式,比如 "jpg" 又如 "png" 53 | // response API服务器的JSON输出将被还原成该结构体 54 | // 55 | // 当出现异常时输出非nil错误 56 | func (weibo *Weibo) Upload(token string, params Params, reader io.Reader, imageFormat string, response interface{}) error { 57 | apiUri := fmt.Sprintf("%s/%s/%s%s", ApiDomain, ApiVersion, UploadAPIName, ApiNamePostfix) 58 | return weibo.sendPostHttpRequest(apiUri, token, params, reader, imageFormat, response) 59 | } 60 | 61 | // 向微博API服务器发送GET请求 62 | func (weibo *Weibo) sendGetHttpRequest(uri string, token string, params Params, response interface{}) error { 63 | // 生成请求URI 64 | var uriBuffer bytes.Buffer 65 | uriBuffer.WriteString(fmt.Sprintf("%s?access_token=%s", uri, token)) 66 | for k, v := range params { 67 | value := fmt.Sprint(v) 68 | if k != "" && value != "" { 69 | uriBuffer.WriteString(fmt.Sprintf("&%s=%s", k, value)) 70 | } 71 | } 72 | requestUri := uriBuffer.String() 73 | 74 | // 发送GET请求 75 | resp, err := weibo.httpClient.Get(requestUri) 76 | if err != nil { 77 | return err 78 | } 79 | defer resp.Body.Close() 80 | 81 | // 解析API服务器返回内容 82 | bytes, _ := ioutil.ReadAll(resp.Body) 83 | if resp.StatusCode == 200 { 84 | err := json.Unmarshal(bytes, &response) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } else { 90 | var weiboErr WeiboError 91 | err := json.Unmarshal(bytes, &weiboErr) 92 | if err != nil { 93 | return err 94 | } 95 | return weiboErr 96 | } 97 | return nil 98 | } 99 | 100 | // 向微博API服务器发送POST请求 101 | // 102 | // 输入参数的含义请见Upload函数注释。当reader == nil时使用query string模式,否则使用multipart。 103 | func (weibo *Weibo) sendPostHttpRequest(uri string, token string, params Params, reader io.Reader, imageFormat string, response interface{}) error { 104 | // 生成POST请求URI 105 | requestUri := fmt.Sprintf("%s?access_token=%s", uri, token) 106 | 107 | // 生成POST内容 108 | var bodyBuffer bytes.Buffer 109 | var writer *multipart.Writer 110 | if reader == nil { 111 | // reader为nil时无文件上传,因此POST body为简单的query string模式 112 | pb := url.Values{} 113 | pb.Add("access_token", token) 114 | 115 | for k, v := range params { 116 | value := fmt.Sprint(v) 117 | if k != "" && value != "" { 118 | pb.Add(k, value) 119 | } 120 | } 121 | bodyBuffer = *bytes.NewBufferString(pb.Encode()) 122 | } else { 123 | // 否则POST body使用multipart模式 124 | writer = multipart.NewWriter(&bodyBuffer) 125 | imagePartWriter, _ := writer.CreateFormFile("pic", "image."+imageFormat) 126 | io.Copy(imagePartWriter, reader) 127 | for k, v := range params { 128 | value := fmt.Sprint(v) 129 | if k != "" && value != "" { 130 | writer.WriteField(k, value) 131 | } 132 | } 133 | writer.Close() 134 | } 135 | 136 | // 生成POST请求 137 | req, err := http.NewRequest("POST", requestUri, &bodyBuffer) 138 | if err != nil { 139 | return err 140 | } 141 | if reader == nil { 142 | // reader为nil时使用一般的内容类型 143 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 144 | } else { 145 | // 否则使用带boundary的multipart类型 146 | req.Header.Set("Content-Type", writer.FormDataContentType()) 147 | } 148 | 149 | // 发送请求 150 | resp, err := weibo.httpClient.Do(req) 151 | if err != nil { 152 | return err 153 | } 154 | defer resp.Body.Close() 155 | 156 | // 解析API服务器返回内容 157 | bytes, _ := ioutil.ReadAll(resp.Body) 158 | if resp.StatusCode == 200 { 159 | err := json.Unmarshal(bytes, &response) 160 | if err != nil { 161 | return err 162 | } 163 | return nil 164 | } else { 165 | var weiboErr WeiboError 166 | err := json.Unmarshal(bytes, &weiboErr) 167 | if err != nil { 168 | return err 169 | } 170 | return weiboErr 171 | } 172 | return nil 173 | } 174 | --------------------------------------------------------------------------------