├── .gitignore ├── LICENSE ├── README.md ├── examples ├── basic.go ├── go_faq.go ├── pagination.go ├── questions_asked_by_new_users.go └── trends.go └── stackongo ├── access_tokens.go ├── access_tokens_test.go ├── answers.go ├── answers_test.go ├── auth.go ├── auth_test.go ├── badges.go ├── badges_test.go ├── collections.go ├── comments.go ├── comments_test.go ├── errors.go ├── errors_test.go ├── events.go ├── events_test.go ├── filters.go ├── filters_test.go ├── inbox.go ├── inbox_test.go ├── info.go ├── info_test.go ├── network_users.go ├── network_users_test.go ├── params.go ├── params_test.go ├── posts.go ├── posts_test.go ├── privileges.go ├── privileges_test.go ├── question_timelines.go ├── question_timelines_test.go ├── questions.go ├── questions_test.go ├── reputations.go ├── reputations_test.go ├── resource_types.go ├── revisions.go ├── revisions_test.go ├── session.go ├── session_test.go ├── sites.go ├── sites_test.go ├── suggested_edits.go ├── suggested_edits_test.go ├── tag_scores.go ├── tag_scores_test.go ├── tag_synonyms.go ├── tag_synonyms_test.go ├── tag_wikis.go ├── tag_wikis_test.go ├── tags.go ├── tags_test.go ├── top_tags.go ├── top_tags_test.go ├── user_timelines.go ├── user_timelines_test.go ├── users.go └── users_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | 6.out 3 | *.6 4 | *.a 5 | learning 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Lakshan Perera 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Stack on Go 2 | 3 | This is a wrapper written in Golang for [Stack Exchange API 2.0](https://api.stackexchange.com). 4 | 5 | ### Installation 6 | 7 | Let's have a look how to get started with *Stack on Go*. 8 | 9 | *Stack on Go* fully supports Go1. 10 | 11 | To install the package, run: 12 | 13 | ```bash 14 | go get github.com/laktek/Stack-on-Go 15 | ``` 16 | 17 | ### Basic Usage 18 | 19 | Once installed, you can use *Stack on Go* by importing it in your source. 20 | 21 | ```go 22 | import "github.com/laktek/Stack-on-Go/stackongo" 23 | ``` 24 | 25 | By default, package will be named as `stackongo`. If you want, you can give an alternate name at the import. 26 | 27 | Stack Exchange API contains global and site specific methods. Global methods can be directly called like this: 28 | 29 | ```go 30 | sites, err := stackongo.AllSites(params) 31 | ``` 32 | 33 | Before calling site specific methods, you need to create a new session. A site identifier should be passed as a string (usually, it's the domain of the site). 34 | 35 | ```go 36 | session := stackongo.NewSession("stackoverflow") 37 | ``` 38 | 39 | Then call the methods in scope of the created session. 40 | 41 | ```go 42 | info, err := session.Info() 43 | ``` 44 | 45 | Most methods accept a map of parameters. There's a special `Params` type that you can use to create a parameter map. 46 | 47 | ```go 48 | //set the params 49 | params := make(stackongo.Params) 50 | params.Add("filter", "total") 51 | params.AddVectorized("tagged", []string("go", "ruby", "java")) 52 | 53 | questions, err := session.AllQuestions(params) 54 | ``` 55 | 56 | If you prefer, you can pass your parameters directly in a `map[string]string` literal: 57 | 58 | ```go 59 | questions, err := session.AllQuestions(map[string]string{"filter": "total", "tagged": "go;ruby;java"}) 60 | ``` 61 | 62 | Most methods returns a `struct` containing a collection of items and meta information (more details available in [StackExchange docs](https://api.stackexchange.com/docs/wrapper) ). You can traverse through the results to create an output: 63 | 64 | ```go 65 | for _, question := range questions.Items { 66 | fmt.Printf("%v\n", question.Title) 67 | fmt.Printf("Asked By: %v on %v\n", question.Owner.Display_name, time.SecondsToUTC(question.Creation_date)) 68 | fmt.Printf("Link: %v\n\n", question.Link) 69 | } 70 | ``` 71 | 72 | You can use the returned meta information to make run-time decisions. For example, you can check whether there are more results and load them progressively. 73 | 74 | ```go 75 | if questions.Has_more { 76 | params.Page(page + 1) 77 | questions, err = session.AllQuestions(params) 78 | } 79 | ``` 80 | 81 | ### Authentication 82 | 83 | Stack Exchange follows the OAuth 2.0 workflow for user authentication. *Stack on Go* includes two helper functions tailored for authentication offered by the Stack Exchange API. 84 | 85 | `AuthURL` returns you a URL to redirect the user for authentication and `ObtainAcessToken` should be called from the handler of redirected URI to obtain the access token. 86 | 87 | Check the following code sample, which explains the authentication flow: 88 | 89 | ```go 90 | func init() { 91 | http.HandleFunc("/", authorize) 92 | http.HandleFunc("/profile", profile) 93 | } 94 | 95 | func authorize(w http.ResponseWriter, r *http.Request) { 96 | auth_url := stackongo.AuthURL(client_id, "http://myapp.com/profile", map[string]string{"scope": "read_inbox"}) 97 | 98 | header := w.Header() 99 | header.Add("Location", auth_url) 100 | w.WriteHeader(302) 101 | } 102 | 103 | func profile(w http.ResponseWriter, r *http.Request) { 104 | code := r.URL.Query().Get("code") 105 | access_token, err := stackongo.ObtainAccessToken(client_id, client_secret, code, "http://myapp.com/profile") 106 | 107 | if err != nil { 108 | fmt.Fprintf(w, "%v", err.String()) 109 | } else { 110 | //get authenticated user 111 | session := stackongo.NewSession("stackoverflow") 112 | user, err := session.AuthenticatedUser(map[string]string{}, map[string]string{"key": client_key, "access_token": access_token["access_token"]}) 113 | 114 | // do more with the authenticated user 115 | } 116 | 117 | } 118 | ``` 119 | 120 | ### Using with AppEngine 121 | 122 | If you plan to deploy your app on [Google AppEngine](http://code.google.com/appengine/docs/go/), remember to do a one slight modification in your code. Since AppEngine has a special package to fetch external URLs you have to set it as the transport method for *Stack on Go*. 123 | 124 | Here's how to do it: 125 | 126 | ```go 127 | 128 | import ( 129 | "github.com/laktek/Stack-on-Go/stackongo" 130 | "appengine/urlfetch" 131 | ) 132 | 133 | func main(){ 134 | c := appengine.NewContext(r) 135 | ut := &urlfetch.Transport{Context: c} 136 | 137 | stackongo.SetTransport(ut) //set urlfetch as the transport 138 | 139 | session := stackongo.NewSession("stackoverflow") 140 | info, err := session.Info() 141 | } 142 | ``` 143 | 144 | ### Tests and Documentation 145 | 146 | Methods are organized into files by their return types. You can see how to use a method by checking the relavant test file. There's 100% test coverage for all methods. 147 | 148 | *Stack on Go* implements all methods defined in the Stack Exchange API 2.0. You can use its [documentation](https://api.stackexchange.com/docs) to learn more about parameters available parameters to a method, available filters and fields you can expect in a response. 149 | 150 | ### Examples 151 | 152 | Please check the `/examples` directory to learn various ways you can use *Stack on Go*. 153 | 154 | ### Issues & Suggestions 155 | 156 | Please report any bugs or feature requests here: 157 | http://github.com/laktek/Stack-on-Go/issues/ 158 | 159 | -------------------------------------------------------------------------------- /examples/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "github.com/laktek/Stack-on-Go/stackongo" 5 | 6 | func main() { 7 | fmt.Printf("Here are some facts about StackOverflow:\n") 8 | 9 | session := stackongo.NewSession("stackoverflow") 10 | info, err := session.Info() 11 | 12 | if err != nil { 13 | fmt.Printf(err.Error()) 14 | } 15 | 16 | fmt.Printf("Total Questions: %v\n", info.Total_questions) 17 | fmt.Printf("Total Answers: %v\n", info.Total_answers) 18 | fmt.Printf("Total Users: %v\n", info.Total_users) 19 | 20 | fmt.Printf("Questions per minute: %v\n", info.Questions_per_minute) 21 | fmt.Printf("Answers per minute: %v\n", info.Answers_per_minute) 22 | fmt.Printf("New active users: %v\n", info.New_active_users) 23 | } 24 | -------------------------------------------------------------------------------- /examples/go_faq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "time" 5 | import "github.com/laktek/Stack-on-Go/stackongo" 6 | 7 | func main() { 8 | fmt.Printf("Frequently Asked Questions about Go:\n") 9 | 10 | session := stackongo.NewSession("stackoverflow") 11 | questions, err := session.FAQForTags([]string{"Go"}, map[string]string{}) 12 | 13 | if err != nil { 14 | fmt.Printf(err.Error()) 15 | } 16 | 17 | for _, question := range questions.Items { 18 | fmt.Printf("%v\n", question.Title) 19 | fmt.Printf("Asked By: %v on %v\n", question.Owner.Display_name, time.Unix(question.Creation_date, 0).UTC()) 20 | fmt.Printf("Link: %v\n\n", question.Link) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/pagination.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "time" 5 | import "github.com/laktek/Stack-on-Go/stackongo" 6 | 7 | func main() { 8 | show_pages_upto := 5 9 | 10 | fmt.Printf("Questions tagged with Web:\n") 11 | 12 | session := stackongo.NewSession("stackoverflow") 13 | 14 | params := make(stackongo.Params) 15 | params.Sort("votes") 16 | params.Add("tagged", "web") 17 | params.Pagesize(100) 18 | 19 | total := 0 20 | 21 | for cur_page := 0; cur_page < show_pages_upto; cur_page++ { 22 | params.Set("page", cur_page+1) 23 | questions, err := session.AllQuestions(params) 24 | 25 | if err != nil { 26 | fmt.Printf(err.Error()) 27 | } 28 | 29 | for _, question := range questions.Items { 30 | fmt.Printf("%v\n", question.Title) 31 | fmt.Printf("Asked By: %v on %v\n", question.Owner.Display_name, time.Unix(question.Creation_date, 0).UTC()) 32 | fmt.Printf("Link: %v\n\n", question.Link) 33 | } 34 | 35 | total += len(questions.Items) 36 | 37 | //break the loop if there aren't any more results 38 | if !questions.Has_more { 39 | break 40 | } 41 | 42 | fmt.Printf("Total Questions Fetched: %v\n\n", total) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/questions_asked_by_new_users.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "time" 5 | import "github.com/laktek/Stack-on-Go/stackongo" 6 | 7 | func main() { 8 | 9 | fmt.Printf("Questions asked by users registered in last 24 hours:\n") 10 | 11 | from_date := time.Now().Unix() - (60 * 60 * 24) 12 | 13 | session := stackongo.NewSession("stackoverflow") 14 | 15 | //set the params 16 | user_params := make(stackongo.Params) 17 | user_params.Add("fromdate", from_date) 18 | user_params.Add("sort", "creation") 19 | 20 | users, err := session.AllUsers(user_params) 21 | 22 | if err != nil { 23 | fmt.Printf(err.Error()) 24 | } 25 | 26 | var user_ids []int 27 | for _, user := range users.Items { 28 | user_ids = append(user_ids, user.User_id) 29 | } 30 | 31 | question_params := make(stackongo.Params) 32 | question_params.Add("sort", "creation") 33 | 34 | questions, err2 := session.QuestionsFromUsers(user_ids, question_params) 35 | 36 | if err2 != nil { 37 | fmt.Printf(err2.Error()) 38 | } 39 | 40 | for _, question := range questions.Items { 41 | fmt.Printf("%v\n", question.Title) 42 | fmt.Printf("Asked By: %v on %v\n", question.Owner.Display_name, time.Unix(question.Creation_date, 0).UTC()) 43 | fmt.Printf("Link: %v\n\n", question.Link) 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/trends.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "time" 5 | import "github.com/laktek/Stack-on-Go/stackongo" 6 | 7 | func main() { 8 | tags := []string{"Go", "Erlang", "Ruby", "Python", "JavaScript"} 9 | 10 | fmt.Printf("No.of Questions for Technology (Last 30 days):\n") 11 | 12 | from_date := time.Now().Unix() - (60 * 60 * 24 * 30) 13 | 14 | session := stackongo.NewSession("stackoverflow") 15 | 16 | //set the common params 17 | params := make(stackongo.Params) 18 | params.Add("filter", "total") 19 | params.Add("fromdate", from_date) 20 | 21 | for _, tag := range tags { 22 | params.Add("tagged", tag) 23 | results, err := session.AllQuestions(params) 24 | 25 | if err != nil { 26 | fmt.Printf(err.Error()) 27 | } 28 | 29 | fmt.Printf("%v %v\n", tag, results.Total) 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stackongo/access_tokens.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // InspectAccessTokens returns the properties for a set of access tokens. 9 | func InspectAccessTokens(access_tokens []string, params map[string]string) (output *AccessTokens, error error) { 10 | request_path := fmt.Sprintf("access-tokens/%v", strings.Join(access_tokens, ";")) 11 | 12 | output = new(AccessTokens) 13 | error = get(request_path, params, output) 14 | return 15 | } 16 | 17 | // DeauthenticateAccessTokens de-authorizes the app for the users with given access tokens. 18 | func DeauthenticateAccessTokens(access_tokens []string, params map[string]string) (output *AccessTokens, error error) { 19 | request_path := fmt.Sprintf("apps/%v/de-authenticate", strings.Join(access_tokens, ";")) 20 | 21 | output = new(AccessTokens) 22 | error = get(request_path, params, output) 23 | return 24 | } 25 | 26 | // InvalidateAccessTokens invalidates the given access tokens. 27 | func InvalidateAccessTokens(access_tokens []string, params map[string]string) (output *AccessTokens, error error) { 28 | request_path := fmt.Sprintf("access-tokens/%v/invalidate", strings.Join(access_tokens, ";")) 29 | 30 | output = new(AccessTokens) 31 | error = get(request_path, params, output) 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /stackongo/access_tokens_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInspectAccessTokens(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/access-tokens/abc;def;ghi", dummyAccessTokensResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | access_tokens, err := InspectAccessTokens([]string{"abc", "def", "ghi"}, map[string]string{"page": "1"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()); 15 | } 16 | 17 | if len(access_tokens.Items) != 3 { 18 | t.Error("Number of items wrong.") 19 | } 20 | 21 | if access_tokens.Items[0].Access_token != "abc" { 22 | t.Error("Access token invalid.") 23 | } 24 | 25 | if access_tokens.Items[0].Expires_on_date != 1328148124 { 26 | t.Error("Expires date invalid.") 27 | } 28 | 29 | if access_tokens.Items[0].Account_id != 1001 { 30 | t.Error("Account id invalid.") 31 | } 32 | 33 | if access_tokens.Items[0].Scope[0] != "read_inbox" { 34 | t.Error("Scope invalid.") 35 | } 36 | 37 | } 38 | 39 | func TestDeauthenticateAccessTokens(t *testing.T) { 40 | dummy_server := returnDummyResponseForPath("/2.0/apps/abc;def;ghi/de-authenticate", dummyAccessTokensResponse, t) 41 | defer closeDummyServer(dummy_server) 42 | 43 | _, err := DeauthenticateAccessTokens([]string{"abc", "def", "ghi"}, map[string]string{"page": "1"}) 44 | 45 | if err != nil { 46 | t.Error(err.Error()) 47 | } 48 | 49 | } 50 | 51 | func TesInvalidateAccessTokens(t *testing.T) { 52 | dummy_server := returnDummyResponseForPath("/2.0/access-tokens/abc;def;ghi/invalidate", dummyAccessTokensResponse, t) 53 | defer closeDummyServer(dummy_server) 54 | 55 | _, err := InvalidateAccessTokens([]string{"abc", "def", "ghi"}, map[string]string{"page": "1"}) 56 | 57 | if err != nil { 58 | t.Error(err.Error()) 59 | } 60 | 61 | } 62 | 63 | //Test Data 64 | 65 | var dummyAccessTokensResponse string = ` 66 | { 67 | "items": [ 68 | { 69 | "access_token": "abc", 70 | "expires_on_date": 1328148124, 71 | "account_id": 1001, 72 | "scope": ["read_inbox", "no_expiry"] 73 | }, 74 | { 75 | "access_token": "def", 76 | "expires_on_date": 1328148124, 77 | "account_id": 1002, 78 | "scope": ["read_inbox"] 79 | }, 80 | { 81 | "access_token": "ghi", 82 | "expires_on_date": 1328148124, 83 | "account_id": 1003, 84 | "scope": ["read_inbox"] 85 | } 86 | ] 87 | } 88 | ` 89 | -------------------------------------------------------------------------------- /stackongo/answers.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllAnswers returns all answers in site 9 | func (session Session) AllAnswers(params map[string]string) (output *Answers, error error) { 10 | output = new(Answers) 11 | error = session.get("answers", params, output) 12 | return 13 | } 14 | 15 | // Answers returns the answers with the given ids 16 | func (session Session) GetAnswers(ids []int, params map[string]string) (output *Answers, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"answers", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(Answers) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // AnswersForQuestions returns the answers for the questions identified with given ids 29 | func (session Session) AnswersForQuestions(ids []int, params map[string]string) (output *Answers, error error) { 30 | string_ids := []string{} 31 | for _, v := range ids { 32 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 33 | } 34 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";"), "answers"}, "/") 35 | 36 | output = new(Answers) 37 | error = session.get(request_path, params, output) 38 | return 39 | } 40 | 41 | // AnswersFromUsers returns the answers from the users identified with given ids 42 | func (session Session) AnswersFromUsers(ids []int, params map[string]string) (output *Answers, error error) { 43 | string_ids := []string{} 44 | for _, v := range ids { 45 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 46 | } 47 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "answers"}, "/") 48 | 49 | output = new(Answers) 50 | error = session.get(request_path, params, output) 51 | return 52 | } 53 | 54 | // TopAnswersFromUsers returns the top answers from the users identified with given ids for the questions with given tags 55 | func (session Session) TopAnswersFromUsers(ids []int, tags []string, params map[string]string) (output *Answers, error error) { 56 | 57 | string_ids := []string{} 58 | for _, v := range ids { 59 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 60 | } 61 | 62 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "tags", strings.Join(tags, ";"), "top-answers"}, "/") 63 | 64 | output = new(Answers) 65 | error = session.get(request_path, params, output) 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /stackongo/answers_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllAnswers(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/answers", dummyAnswersResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | answers, err := session.AllAnswers(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(answers.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if answers.Items[0].Answer_id != 9051117 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if answers.Items[0].Owner.Display_name != "Dalar" { 27 | t.Error("Owner invalid.") 28 | } 29 | 30 | if answers.Items[0].Creation_date != 1327814223 { 31 | t.Error("Date invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestGetAnswers(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/answers/1;2;3", dummyAnswersResponse, t) 38 | defer closeDummyServer(dummy_server) 39 | 40 | session := NewSession("stackoverflow") 41 | _, err := session.GetAnswers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | } 48 | 49 | func TestAnswersForQuestions(t *testing.T) { 50 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3/answers", dummyAnswersResponse, t) 51 | defer closeDummyServer(dummy_server) 52 | 53 | session := NewSession("stackoverflow") 54 | _, err := session.AnswersForQuestions([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 55 | 56 | if err != nil { 57 | t.Error(err.Error()) 58 | } 59 | } 60 | 61 | func TestAnswersFromUsers(t *testing.T) { 62 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/answers", dummyAnswersResponse, t) 63 | defer closeDummyServer(dummy_server) 64 | 65 | session := NewSession("stackoverflow") 66 | _, err := session.AnswersFromUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 67 | 68 | if err != nil { 69 | t.Error(err.Error()) 70 | } 71 | } 72 | 73 | func TestTopAnswersFromUsers(t *testing.T) { 74 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/tags/hello;world/top-answers", dummyAnswersResponse, t) 75 | defer closeDummyServer(dummy_server) 76 | 77 | session := NewSession("stackoverflow") 78 | _, err := session.TopAnswersFromUsers([]int{1, 2, 3}, []string{"hello", "world"}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 79 | 80 | if err != nil { 81 | t.Error(err.Error()) 82 | } 83 | } 84 | 85 | //Test Data 86 | 87 | var dummyAnswersResponse string = ` 88 | { 89 | "items": [ 90 | { 91 | "question_id": 9023805, 92 | "answer_id": 9051117, 93 | "creation_date": 1327814223, 94 | "last_activity_date": 1327814223, 95 | "score": 0, 96 | "is_accepted": false, 97 | "owner": { 98 | "user_id": 325674, 99 | "display_name": "Dalar", 100 | "reputation": 176, 101 | "user_type": "registered", 102 | "profile_image": "http://www.gravatar.com/avatar/d0c5cda979ec453235876282edbe1188?d=identicon&r=PG", 103 | "link": "http://stackoverflow.com/users/325674/dalar" 104 | } 105 | }, 106 | { 107 | "question_id": 9051070, 108 | "answer_id": 9051116, 109 | "creation_date": 1327814175, 110 | "last_activity_date": 1327814175, 111 | "score": 0, 112 | "is_accepted": false, 113 | "owner": { 114 | "user_id": 197913, 115 | "display_name": "Damir Arh", 116 | "reputation": 1019, 117 | "user_type": "registered", 118 | "profile_image": "http://www.gravatar.com/avatar/c85e6426e70c4ecf205a6187e7dbf174?d=identicon&r=PG", 119 | "link": "http://stackoverflow.com/users/197913/damir-arh" 120 | } 121 | }, 122 | { 123 | "question_id": 8415119, 124 | "answer_id": 9051112, 125 | "creation_date": 1327814056, 126 | "last_activity_date": 1327814056, 127 | "score": 0, 128 | "is_accepted": false, 129 | "owner": { 130 | "user_id": 1094837, 131 | "display_name": "Joseph Torraca", 132 | "reputation": 78, 133 | "user_type": "registered", 134 | "profile_image": "http://www.gravatar.com/avatar/292351179bec9b5119d0e3a7e743537a?d=identicon&r=PG", 135 | "link": "http://stackoverflow.com/users/1094837/joseph-torraca" 136 | } 137 | } 138 | ] 139 | } 140 | ` 141 | -------------------------------------------------------------------------------- /stackongo/auth.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type authError struct { 11 | Error map[string]string 12 | } 13 | 14 | var auth_url string = "https://stackexchange.com/oauth/access_token" 15 | 16 | // AuthURL returns the URL to redirect the user for authentication 17 | // It accepts the following arguments 18 | // client_id - Your App's registered ID 19 | // redirect_uri - URI to redirect after authentication 20 | // options - a map containing the following: 21 | // scope - set of previlages you need to access - can be empty, "read_inbox", "no_expiry" or "read_inbox,no_expiry" 22 | // state - optional string which will be returned as it is 23 | func AuthURL(client_id, redirect_uri string, options map[string]string) (output string) { 24 | auth_url, _ := url.Parse("https://stackexchange.com/oauth") 25 | auth_query := auth_url.Query() 26 | auth_query.Add("client_id", client_id) 27 | auth_query.Add("redirect_uri", redirect_uri) 28 | 29 | for key, value := range options { 30 | auth_query.Add(key, value) 31 | } 32 | 33 | auth_url.RawQuery = auth_query.Encode() 34 | return auth_url.String() 35 | } 36 | 37 | func ObtainAccessToken(client_id, client_secret, code, redirect_uri string) (output map[string]string, error error) { 38 | client := &http.Client{Transport: getTransport()} 39 | 40 | parsed_auth_url, _ := url.Parse(auth_url) 41 | auth_query := parsed_auth_url.Query() 42 | auth_query.Add("client_id", client_id) 43 | auth_query.Add("client_secret", client_secret) 44 | auth_query.Add("code", code) 45 | auth_query.Add("redirect_uri", redirect_uri) 46 | 47 | // make the request 48 | response, error := client.PostForm(auth_url, auth_query) 49 | 50 | if error != nil { 51 | return 52 | } 53 | 54 | //check whether the response is a bad request 55 | if response.StatusCode != 200 { 56 | collection := new(authError) 57 | error = parseResponse(response, collection) 58 | 59 | error = errors.New(collection.Error["type"] + ": " + collection.Error["message"]) 60 | } else { 61 | // if not process the output 62 | bytes, err2 := ioutil.ReadAll(response.Body) 63 | 64 | if err2 != nil { 65 | return output, err2 66 | } 67 | 68 | collection, err3 := url.ParseQuery(string(bytes)) 69 | output = map[string]string{"access_token": collection.Get("access_token"), "expires": collection.Get("expires")} 70 | error = err3 71 | } 72 | 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /stackongo/auth_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestAuthURL(t *testing.T) { 9 | result_uri, _ := url.Parse(AuthURL("abc", "www.my_app.com", map[string]string{"state": "test", "scope": "read_inbox,no_expiry"})) 10 | 11 | if result_uri.Query().Encode() != "state=test&scope=read_inbox%2Cno_expiry&redirect_uri=www.my_app.com&client_id=abc" { 12 | print(result_uri.Query().Encode()) 13 | t.Error("URL doesn't match") 14 | } 15 | } 16 | 17 | func TestObtainAccessTokenValid(t *testing.T) { 18 | dummy_server := returnDummyResponseForPath("/oauth/access_token", dummyAccessTokenResponse, t) 19 | defer dummy_server.Close() 20 | 21 | //change the host to use the test server 22 | auth_url = dummy_server.URL + "/oauth/access_token" 23 | 24 | token_output, _ := ObtainAccessToken("abc", "secret", "def", "www.my_app.com") 25 | 26 | if token_output["access_token"] != "my_token" { 27 | t.Error("invalid access token.") 28 | } 29 | 30 | if token_output["expires"] != "1234" { 31 | t.Error("invalid expiry.") 32 | } 33 | 34 | } 35 | 36 | func TestObtainAccessTokenInvalid(t *testing.T) { 37 | dummy_server := returnDummyErrorResponseForPath("/oauth/access_token", dummyAccessTokenErrorResponse, t) 38 | defer dummy_server.Close() 39 | 40 | //change the host to use the test server 41 | auth_url = dummy_server.URL + "/oauth/access_token" 42 | 43 | _, error := ObtainAccessToken("abc", "secret", "def", "www.my_app.com") 44 | 45 | if error.Error() != "invalid_request: some reason" { 46 | t.Error("invalid error message.") 47 | } 48 | } 49 | 50 | //test data 51 | var dummyAccessTokenResponse string = "access_token=my_token&expires=1234" 52 | 53 | var dummyAccessTokenErrorResponse string = ` 54 | { "error": { "type": "invalid_request", "message": "some reason" } } 55 | ` 56 | -------------------------------------------------------------------------------- /stackongo/badges.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllBadges returns all badges in site 9 | func (session Session) AllBadges(params map[string]string) (output *Badges, error error) { 10 | output = new(Badges) 11 | error = session.get("badges", params, output) 12 | return 13 | } 14 | 15 | // Badges returns the badges with the given ids 16 | func (session Session) GetBadges(ids []int, params map[string]string) (output *Badges, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"badges", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(Badges) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // NamedBadges returns all explicitly named badges 29 | func (session Session) NamedBadges(params map[string]string) (output *Badges, error error) { 30 | output = new(Badges) 31 | error = session.get("badges/name", params, output) 32 | return 33 | } 34 | 35 | // TagBadges returns the badges that are awarded for participation in specific tags 36 | func (session Session) TagBadges(params map[string]string) (output *Badges, error error) { 37 | output = new(Badges) 38 | error = session.get("badges/tags", params, output) 39 | return 40 | } 41 | 42 | // RecentBadgeRecipients returns the recent recipients of the given badges 43 | func (session Session) RecentBadgeRecipients(ids []int, params map[string]string) (output *Badges, error error) { 44 | string_ids := []string{} 45 | for _, v := range ids { 46 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 47 | } 48 | request_path := strings.Join([]string{"badges", strings.Join(string_ids, ";"), "recipients"}, "/") 49 | 50 | output = new(Badges) 51 | error = session.get(request_path, params, output) 52 | return 53 | 54 | } 55 | 56 | // RecentAllBadgeRecipients returns badges recently awarded 57 | func (session Session) RecentAllBadgeRecipients(params map[string]string) (output *Badges, error error) { 58 | output = new(Badges) 59 | error = session.get("badges/recipients", params, output) 60 | return 61 | } 62 | 63 | // BadgesOfUsers returns the badges earned by the users identified by a set of ids 64 | func (session Session) BadgesOfUsers(ids []int, params map[string]string) (output *Badges, error error) { 65 | string_ids := []string{} 66 | for _, v := range ids { 67 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 68 | } 69 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "badges"}, "/") 70 | 71 | output = new(Badges) 72 | error = session.get(request_path, params, output) 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /stackongo/badges_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllBadges(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/badges", dummyBadgesResponse, t) 9 | defer dummy_server.Close() 10 | 11 | session := NewSession("stackoverflow") 12 | badges, err := session.AllBadges(map[string]string{}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(badges.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if badges.Items[0].Badge_id != 455 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if badges.Items[0].Rank != "bronze" { 27 | t.Error("Rank invalid.") 28 | } 29 | 30 | if badges.Items[0].Badge_type != "tag_based" { 31 | t.Error("Badge type invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestGetBadges(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/badges/1;2;3", dummyBadgesResponse, t) 38 | defer dummy_server.Close() 39 | 40 | session := NewSession("stackoverflow") 41 | _, err := session.GetBadges([]int{1, 2, 3}, map[string]string{}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | } 48 | 49 | func TestNamedBadges(t *testing.T) { 50 | dummy_server := returnDummyResponseForPath("/2.0/badges/name", dummyBadgesResponse, t) 51 | defer closeDummyServer(dummy_server) 52 | 53 | session := NewSession("stackoverflow") 54 | _, err := session.NamedBadges(map[string]string{}) 55 | 56 | if err != nil { 57 | t.Error(err.Error()) 58 | } 59 | 60 | } 61 | 62 | func TestTagBadges(t *testing.T) { 63 | dummy_server := returnDummyResponseForPath("/2.0/badges/tags", dummyBadgesResponse, t) 64 | defer closeDummyServer(dummy_server) 65 | 66 | session := NewSession("stackoverflow") 67 | _, err := session.TagBadges(map[string]string{}) 68 | 69 | if err != nil { 70 | t.Error(err.Error()) 71 | } 72 | 73 | } 74 | 75 | func TestRecentAllBadgeRecipients(t *testing.T) { 76 | dummy_server := returnDummyResponseForPath("/2.0/badges/recipients", dummyBadgesResponse, t) 77 | defer closeDummyServer(dummy_server) 78 | 79 | session := NewSession("stackoverflow") 80 | badges, err := session.RecentAllBadgeRecipients(map[string]string{}) 81 | 82 | if err != nil { 83 | t.Error(err.Error()) 84 | } 85 | 86 | if badges.Items[0].User.Display_name != "Joel Martinez" { 87 | t.Error("User invalid.") 88 | } 89 | 90 | } 91 | 92 | func TestRecentBadgeRecipients(t *testing.T) { 93 | dummy_server := returnDummyResponseForPath("/2.0/badges/1;2;3/recipients", dummyBadgesResponse, t) 94 | defer closeDummyServer(dummy_server) 95 | 96 | session := NewSession("stackoverflow") 97 | badges, err := session.RecentBadgeRecipients([]int{1, 2, 3}, map[string]string{}) 98 | 99 | if err != nil { 100 | t.Error(err.Error()) 101 | } 102 | 103 | if badges.Items[0].User.Display_name != "Joel Martinez" { 104 | t.Error("User invalid.") 105 | } 106 | 107 | } 108 | 109 | func TestBadgeOfUsers(t *testing.T) { 110 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/badges", dummyBadgesResponse, t) 111 | defer closeDummyServer(dummy_server) 112 | 113 | session := NewSession("stackoverflow") 114 | badges, err := session.BadgesOfUsers([]int{1, 2, 3}, map[string]string{}) 115 | 116 | if err != nil { 117 | t.Error(err.Error()) 118 | } 119 | 120 | if badges.Items[0].User.Display_name != "Joel Martinez" { 121 | t.Error("User invalid.") 122 | } 123 | 124 | } 125 | 126 | //Test Data 127 | 128 | var dummyBadgesResponse string = ` 129 | { 130 | "items": [ 131 | { 132 | "badge_id": 455, 133 | "rank": "bronze", 134 | "name": "xna", 135 | "award_count": 2, 136 | "badge_type": "tag_based", 137 | "user": { 138 | "user_id": 5416, 139 | "display_name": "Joel Martinez", 140 | "reputation": 15030, 141 | "user_type": "registered", 142 | "profile_image": "http://www.gravatar.com/avatar/c7fbf557a6c37f0ae2b300f6799c767f?d=identicon&r=PG", 143 | "link": "http://stackoverflow.com/users/5416/joel-martinez" 144 | }, 145 | "link": "http://stackoverflow.com/badges/455/xna" 146 | }, 147 | { 148 | "badge_id": 456, 149 | "rank": "bronze", 150 | "name": "elisp", 151 | "award_count": 7, 152 | "badge_type": "tag_based", 153 | "link": "http://stackoverflow.com/badges/456/elisp" 154 | }, 155 | { 156 | "badge_id": 457, 157 | "rank": "bronze", 158 | "name": "latex", 159 | "award_count": 10, 160 | "badge_type": "tag_based", 161 | "link": "http://stackoverflow.com/badges/457/latex" 162 | } 163 | ] 164 | } 165 | ` 166 | -------------------------------------------------------------------------------- /stackongo/collections.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | type AccessTokens struct { 4 | Items []AccessToken 5 | Error_id int 6 | Error_name string 7 | Error_message string 8 | 9 | Backoff int 10 | Has_more bool 11 | Page int 12 | Page_size int 13 | Quota_max int 14 | Quota_remaining int 15 | Total int 16 | Type string 17 | } 18 | 19 | type Answers struct { 20 | Items []Answer 21 | Error_id int 22 | Error_name string 23 | Error_message string 24 | 25 | Backoff int 26 | Has_more bool 27 | Page int 28 | Page_size int 29 | Quota_max int 30 | Quota_remaining int 31 | Total int 32 | Type string 33 | } 34 | 35 | type Badges struct { 36 | Items []Badge 37 | Error_id int 38 | Error_name string 39 | Error_message string 40 | 41 | Backoff int 42 | Has_more bool 43 | Page int 44 | Page_size int 45 | Quota_max int 46 | Quota_remaining int 47 | Total int 48 | Type string 49 | } 50 | 51 | type Comments struct { 52 | Items []Comment 53 | Error_id int 54 | Error_name string 55 | Error_message string 56 | 57 | Backoff int 58 | Has_more bool 59 | Page int 60 | Page_size int 61 | Quota_max int 62 | Quota_remaining int 63 | Total int 64 | Type string 65 | } 66 | 67 | type Errors struct { 68 | Items []Error 69 | Error_id int 70 | Error_name string 71 | Error_message string 72 | 73 | Backoff int 74 | Has_more bool 75 | Page int 76 | Page_size int 77 | Quota_max int 78 | Quota_remaining int 79 | Total int 80 | Type string 81 | } 82 | 83 | type Events struct { 84 | Items []Event 85 | Error_id int 86 | Error_name string 87 | Error_message string 88 | 89 | Backoff int 90 | Has_more bool 91 | Page int 92 | Page_size int 93 | Quota_max int 94 | Quota_remaining int 95 | Total int 96 | Type string 97 | } 98 | 99 | type infoCollection struct { 100 | Items []Info 101 | Error_id int 102 | Error_name string 103 | Error_message string 104 | 105 | Backoff int 106 | Has_more bool 107 | Page int 108 | Page_size int 109 | Quota_max int 110 | Quota_remaining int 111 | Total int 112 | Type string 113 | } 114 | 115 | type InboxItems struct { 116 | Items []InboxItem 117 | Error_id int 118 | Error_name string 119 | Error_message string 120 | 121 | Backoff int 122 | Has_more bool 123 | Page int 124 | Page_size int 125 | Quota_max int 126 | Quota_remaining int 127 | Total int 128 | Type string 129 | } 130 | 131 | type Filters struct { 132 | Items []Filter 133 | Error_id int 134 | Error_name string 135 | Error_message string 136 | 137 | Backoff int 138 | Has_more bool 139 | Page int 140 | Page_size int 141 | Quota_max int 142 | Quota_remaining int 143 | Total int 144 | Type string 145 | } 146 | 147 | type NetworkUsers struct { 148 | Items []NetworkUser 149 | Error_id int 150 | Error_name string 151 | Error_message string 152 | 153 | Backoff int 154 | Has_more bool 155 | Page int 156 | Page_size int 157 | Quota_max int 158 | Quota_remaining int 159 | Total int 160 | Type string 161 | } 162 | 163 | type Posts struct { 164 | Items []Post 165 | Error_id int 166 | Error_name string 167 | Error_message string 168 | 169 | Backoff int 170 | Has_more bool 171 | Page int 172 | Page_size int 173 | Quota_max int 174 | Quota_remaining int 175 | Total int 176 | Type string 177 | } 178 | 179 | type Privileges struct { 180 | Items []Privilege 181 | Error_id int 182 | Error_name string 183 | Error_message string 184 | 185 | Backoff int 186 | Has_more bool 187 | Page int 188 | Page_size int 189 | Quota_max int 190 | Quota_remaining int 191 | Total int 192 | Type string 193 | } 194 | 195 | type Questions struct { 196 | Items []Question 197 | Error_id int 198 | Error_name string 199 | Error_message string 200 | 201 | Backoff int 202 | Has_more bool 203 | Page int 204 | Page_size int 205 | Quota_max int 206 | Quota_remaining int 207 | Total int 208 | Type string 209 | } 210 | 211 | type QuestionTimelines struct { 212 | Items []QuestionTimeline 213 | Error_id int 214 | Error_name string 215 | Error_message string 216 | 217 | Backoff int 218 | Has_more bool 219 | Page int 220 | Page_size int 221 | Quota_max int 222 | Quota_remaining int 223 | Total int 224 | Type string 225 | } 226 | 227 | type Reputations struct { 228 | Items []Reputation 229 | Error_id int 230 | Error_name string 231 | Error_message string 232 | Backoff int 233 | Has_more bool 234 | Page int 235 | Page_size int 236 | Quota_max int 237 | Quota_remaining int 238 | Total int 239 | Type string 240 | } 241 | 242 | type Revisions struct { 243 | Items []Revision 244 | Error_id int 245 | Error_name string 246 | Error_message string 247 | 248 | Backoff int 249 | Has_more bool 250 | Page int 251 | Page_size int 252 | Quota_max int 253 | Quota_remaining int 254 | Total int 255 | Type string 256 | } 257 | 258 | type Sites struct { 259 | Items []Site 260 | Error_id int 261 | Error_name string 262 | Error_message string 263 | 264 | Backoff int 265 | Has_more bool 266 | Page int 267 | Page_size int 268 | Quota_max int 269 | Quota_remaining int 270 | Total int 271 | Type string 272 | } 273 | 274 | type SuggestedEdits struct { 275 | Items []SuggestedEdit 276 | Error_id int 277 | Error_name string 278 | Error_message string 279 | Backoff int 280 | Has_more bool 281 | Page int 282 | Page_size int 283 | Quota_max int 284 | Quota_remaining int 285 | Total int 286 | Type string 287 | } 288 | 289 | type Tags struct { 290 | Items []Tag 291 | Error_id int 292 | Error_name string 293 | Error_message string 294 | Backoff int 295 | Has_more bool 296 | Page int 297 | Page_size int 298 | Quota_max int 299 | Quota_remaining int 300 | Total int 301 | Type string 302 | } 303 | 304 | type TagScores struct { 305 | Items []TagScore 306 | Error_id int 307 | Error_name string 308 | Error_message string 309 | Backoff int 310 | Has_more bool 311 | Page int 312 | Page_size int 313 | Quota_max int 314 | Quota_remaining int 315 | Total int 316 | Type string 317 | } 318 | 319 | type TagSynonyms struct { 320 | Items []TagSynonym 321 | Error_id int 322 | Error_name string 323 | Error_message string 324 | 325 | Backoff int 326 | Has_more bool 327 | Page int 328 | Page_size int 329 | Quota_max int 330 | Quota_remaining int 331 | Total int 332 | Type string 333 | } 334 | 335 | type TagWikis struct { 336 | Items []TagWiki 337 | Error_id int 338 | Error_name string 339 | Error_message string 340 | Backoff int 341 | Has_more bool 342 | Page int 343 | Page_size int 344 | Quota_max int 345 | Quota_remaining int 346 | Total int 347 | Type string 348 | } 349 | 350 | type TopTags struct { 351 | Items []TopTag 352 | Error_id int 353 | Error_name string 354 | Error_message string 355 | Backoff int 356 | Has_more bool 357 | Page int 358 | Page_size int 359 | Quota_max int 360 | Quota_remaining int 361 | Total int 362 | Type string 363 | } 364 | 365 | type Users struct { 366 | Items []User 367 | Error_id int 368 | Error_name string 369 | Error_message string 370 | Backoff int 371 | Has_more bool 372 | Page int 373 | Page_size int 374 | Quota_max int 375 | Quota_remaining int 376 | Total int 377 | Type string 378 | } 379 | 380 | type UserTimelines struct { 381 | Items []UserTimeline 382 | Error_id int 383 | Error_name string 384 | Error_message string 385 | Backoff int 386 | Has_more bool 387 | Page int 388 | Page_size int 389 | Quota_max int 390 | Quota_remaining int 391 | Total int 392 | Type string 393 | } 394 | -------------------------------------------------------------------------------- /stackongo/comments.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllComments returns all comments in site 9 | func (session Session) AllComments(params map[string]string) (output *Comments, error error) { 10 | output = new(Comments) 11 | error = session.get("comments", params, output) 12 | return 13 | } 14 | 15 | // Comments returns the comments with the given ids 16 | func (session Session) GetComments(ids []int, params map[string]string) (output *Comments, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"comments", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(Comments) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // CommentsForQuestions returns the comments for the questions identified with given ids 29 | func (session Session) CommentsForQuestions(ids []int, params map[string]string) (output *Comments, error error) { 30 | string_ids := []string{} 31 | for _, v := range ids { 32 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 33 | } 34 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";"), "comments"}, "/") 35 | 36 | output = new(Comments) 37 | error = session.get(request_path, params, output) 38 | return 39 | } 40 | 41 | // CommentsForAnswers returns the comments for the answers identified with given ids 42 | func (session Session) CommentsForAnswers(ids []int, params map[string]string) (output *Comments, error error) { 43 | string_ids := []string{} 44 | for _, v := range ids { 45 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 46 | } 47 | request_path := strings.Join([]string{"answers", strings.Join(string_ids, ";"), "comments"}, "/") 48 | 49 | output = new(Comments) 50 | error = session.get(request_path, params, output) 51 | return 52 | } 53 | 54 | // CommentsForPosts returns the comments for the posts identified with given ids 55 | func (session Session) CommentsForPosts(ids []int, params map[string]string) (output *Comments, error error) { 56 | string_ids := []string{} 57 | for _, v := range ids { 58 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 59 | } 60 | request_path := strings.Join([]string{"posts", strings.Join(string_ids, ";"), "comments"}, "/") 61 | 62 | output = new(Comments) 63 | error = session.get(request_path, params, output) 64 | return 65 | } 66 | 67 | // CommentsFromUsers returns the comments from the users identified with given ids 68 | func (session Session) CommentsFromUsers(ids []int, params map[string]string) (output *Comments, error error) { 69 | string_ids := []string{} 70 | for _, v := range ids { 71 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 72 | } 73 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "comments"}, "/") 74 | 75 | output = new(Comments) 76 | error = session.get(request_path, params, output) 77 | return 78 | } 79 | 80 | // CommentsMentionedUsers returns the comments mentioning the users identified with given ids 81 | func (session Session) CommentsMentionedUsers(ids []int, params map[string]string) (output *Comments, error error) { 82 | string_ids := []string{} 83 | for _, v := range ids { 84 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 85 | } 86 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "mentioned"}, "/") 87 | 88 | output = new(Comments) 89 | error = session.get(request_path, params, output) 90 | return 91 | } 92 | 93 | // CommentsFromUsersTo returns the comments to a user from the users identified with given ids 94 | func (session Session) CommentsFromUsersTo(ids []int, to int, params map[string]string) (output *Comments, error error) { 95 | string_ids := []string{} 96 | for _, v := range ids { 97 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 98 | } 99 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "comments", fmt.Sprintf("%v", to)}, "/") 100 | 101 | output = new(Comments) 102 | error = session.get(request_path, params, output) 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /stackongo/comments_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllComments(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/comments", dummyCommentsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | comments, err := session.AllComments(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(comments.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if comments.Items[0].Comment_id != 11354978 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if comments.Items[0].Owner.Display_name != "mynameisneo" { 27 | t.Error("Owner invalid.") 28 | } 29 | 30 | if comments.Items[1].Reply_to_user.Display_name != "user1056824" { 31 | t.Error("Owner invalid.") 32 | } 33 | 34 | if comments.Items[0].Creation_date != 1327798867 { 35 | t.Error("Date invalid.") 36 | } 37 | 38 | } 39 | 40 | func TestGetComments(t *testing.T) { 41 | dummy_server := returnDummyResponseForPath("/2.0/comments/1;2;3", dummyCommentsResponse, t) 42 | defer closeDummyServer(dummy_server) 43 | 44 | session := NewSession("stackoverflow") 45 | _, err := session.GetComments([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 46 | 47 | if err != nil { 48 | t.Error(err.Error()) 49 | } 50 | 51 | } 52 | 53 | func TestCommentsForQuestions(t *testing.T) { 54 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3/comments", dummyCommentsResponse, t) 55 | defer closeDummyServer(dummy_server) 56 | 57 | session := NewSession("stackoverflow") 58 | _, err := session.CommentsForQuestions([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 59 | 60 | if err != nil { 61 | t.Error(err.Error()) 62 | } 63 | 64 | } 65 | 66 | func TestCommentsForAnswers(t *testing.T) { 67 | dummy_server := returnDummyResponseForPath("/2.0/answers/1;2;3/comments", dummyCommentsResponse, t) 68 | defer closeDummyServer(dummy_server) 69 | 70 | session := NewSession("stackoverflow") 71 | _, err := session.CommentsForAnswers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 72 | 73 | if err != nil { 74 | t.Error(err.Error()) 75 | } 76 | 77 | } 78 | 79 | func TestCommentsForPosts(t *testing.T) { 80 | dummy_server := returnDummyResponseForPath("/2.0/posts/1;2;3/comments", dummyCommentsResponse, t) 81 | defer closeDummyServer(dummy_server) 82 | 83 | session := NewSession("stackoverflow") 84 | _, err := session.CommentsForPosts([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 85 | 86 | if err != nil { 87 | t.Error(err.Error()) 88 | } 89 | 90 | } 91 | 92 | func TestCommentsFromUsers(t *testing.T) { 93 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/comments", dummyCommentsResponse, t) 94 | defer closeDummyServer(dummy_server) 95 | 96 | session := NewSession("stackoverflow") 97 | _, err := session.CommentsFromUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 98 | 99 | if err != nil { 100 | t.Error(err.Error()) 101 | } 102 | 103 | } 104 | 105 | func TestCommentsMentionedUsers(t *testing.T) { 106 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/mentioned", dummyCommentsResponse, t) 107 | defer closeDummyServer(dummy_server) 108 | 109 | session := NewSession("stackoverflow") 110 | _, err := session.CommentsMentionedUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 111 | 112 | if err != nil { 113 | t.Error(err.Error()) 114 | } 115 | 116 | } 117 | 118 | func TestCommentsFromUsersTo(t *testing.T) { 119 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/comments/4", dummyCommentsResponse, t) 120 | defer closeDummyServer(dummy_server) 121 | 122 | session := NewSession("stackoverflow") 123 | _, err := session.CommentsFromUsersTo([]int{1, 2, 3}, 4, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 124 | 125 | if err != nil { 126 | t.Error(err.Error()) 127 | } 128 | 129 | } 130 | 131 | //Test Data 132 | 133 | var dummyCommentsResponse string = ` 134 | { 135 | "items": [ 136 | { 137 | "comment_id": 11354978, 138 | "post_id": 9050020, 139 | "creation_date": 1327798867, 140 | "edited": false, 141 | "owner": { 142 | "user_id": 1127391, 143 | "display_name": "mynameisneo", 144 | "reputation": 57, 145 | "user_type": "registered", 146 | "profile_image": "http://www.gravatar.com/avatar/002fa792e5474d3c531da293c6d1a924?d=identicon&r=PG", 147 | "link": "http://stackoverflow.com/users/1127391/mynameisneo" 148 | } 149 | }, 150 | { 151 | "comment_id": 11354977, 152 | "post_id": 9039268, 153 | "creation_date": 1327798866, 154 | "edited": false, 155 | "owner": { 156 | "user_id": 133414, 157 | "display_name": "mvanveen", 158 | "reputation": 266, 159 | "user_type": "registered", 160 | "profile_image": "http://www.gravatar.com/avatar/6695f249160eba47fed3e5fcefd2c942?d=identicon&r=PG", 161 | "link": "http://stackoverflow.com/users/133414/mvanveen" 162 | }, 163 | "reply_to_user": { 164 | "user_id": 1056824, 165 | "display_name": "user1056824", 166 | "reputation": 11, 167 | "user_type": "registered", 168 | "profile_image": "http://www.gravatar.com/avatar/573fc5433388d9245b0e9f1c059e2bb1?d=identicon&r=PG", 169 | "link": "http://stackoverflow.com/users/1056824/user1056824" 170 | } 171 | }, 172 | { 173 | "comment_id": 11354976, 174 | "post_id": 9049774, 175 | "creation_date": 1327798862, 176 | "edited": false, 177 | "owner": { 178 | "user_id": 771771, 179 | "display_name": "Marty Griffin", 180 | "reputation": 39, 181 | "user_type": "registered", 182 | "profile_image": "http://www.gravatar.com/avatar/6e7c1d04d78c984607e864b93227f0c8?d=identicon&r=PG", 183 | "link": "http://stackoverflow.com/users/771771/marty-griffin" 184 | } 185 | } 186 | ] 187 | } 188 | ` 189 | -------------------------------------------------------------------------------- /stackongo/errors.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "fmt" 4 | 5 | // AllErrors returns the various error codes that can be produced by the API. 6 | func AllErrors(params map[string]string) (output *Errors, error error) { 7 | output = new(Errors) 8 | error = get("errors", params, output) 9 | return 10 | } 11 | 12 | // SimulateError allows you to simulate an error response 13 | func SimulateError(id int) (output *Error, error error) { 14 | request_path := fmt.Sprintf("errors/%v", id) 15 | 16 | output = new(Error) 17 | error = get(request_path, map[string]string{}, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/errors_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllErrors(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/errors", dummyErrorsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | errors, err := AllErrors(map[string]string{"page": "1"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | 17 | if len(errors.Items) != 3 { 18 | t.Error("Number of items wrong.") 19 | } 20 | 21 | if errors.Items[0].Error_id != 400 { 22 | t.Error("Error id invalid.") 23 | } 24 | 25 | if errors.Items[0].Error_name != "bad_parameter" { 26 | t.Error("error name invalid.") 27 | } 28 | 29 | } 30 | 31 | func TestSimulateError(t *testing.T) { 32 | dummy_server := returnDummyResponseForPath("/2.0/errors/404", dummyErrorResponse, t) 33 | defer closeDummyServer(dummy_server) 34 | 35 | error, err := SimulateError(404) 36 | 37 | if err != nil { 38 | t.Error(err.Error()) 39 | } 40 | 41 | if error.Error_name != "no_method" { 42 | t.Error("Error name invalid.") 43 | } 44 | 45 | if error.Error_message != "simulated" { 46 | t.Error("Error message invalid.") 47 | } 48 | 49 | } 50 | 51 | //Test Data 52 | 53 | var dummyErrorResponse string = ` 54 | { 55 | "error_id": 404, 56 | "error_name": "no_method", 57 | "error_message": "simulated" 58 | } 59 | ` 60 | 61 | var dummyErrorsResponse string = ` 62 | { 63 | "items": [ 64 | { 65 | "error_id": 400, 66 | "error_name": "bad_parameter", 67 | "description": "An malformed parameter was passed" 68 | }, 69 | { 70 | "error_id": 401, 71 | "error_name": "access_token_required", 72 | "description": "No access_token was passed" 73 | }, 74 | { 75 | "error_id": 402, 76 | "error_name": "invalid_access_token", 77 | "description": "An access_token that is malformed, expired, or otherwise incorrect was passed" 78 | } 79 | ] 80 | } 81 | ` 82 | -------------------------------------------------------------------------------- /stackongo/events.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | // Events returns a stream of events that have occurred on the site 4 | // This method requires an access_token. 5 | func (session Session) Events(params map[string]string, auth map[string]string) (output *Events, error error) { 6 | //add auth params 7 | for key, value := range auth { 8 | params[key] = value 9 | } 10 | 11 | output = new(Events) 12 | error = session.get("events", params, output) 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /stackongo/events_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEvents(t *testing.T) { 8 | dummy_server := returnDummyResponseForPathAndParams("/2.0/events", map[string]string{"key": "app123", "access_token": "abc"}, dummyEventsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | events, err := session.Events(map[string]string{"page": "1"}, map[string]string{"key": "app123", "access_token": "abc"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(events.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if events.Items[0].Event_type != "comment_posted" { 23 | t.Error("Event type invalid.") 24 | } 25 | 26 | if events.Items[0].Event_id != 11462515 { 27 | t.Error("Event id invalid.") 28 | } 29 | 30 | if events.Items[0].Creation_date != 1328226264 { 31 | t.Error("Date invalid.") 32 | } 33 | 34 | } 35 | 36 | //Test Data 37 | 38 | var dummyEventsResponse string = ` 39 | { 40 | "items": [ 41 | { 42 | "event_type": "comment_posted", 43 | "event_id": 11462515, 44 | "creation_date": 1328226264 45 | }, 46 | { 47 | "event_type": "answer_posted", 48 | "event_id": 9121759, 49 | "creation_date": 1328226257 50 | }, 51 | { 52 | "event_type": "question_posted", 53 | "event_id": 9121758, 54 | "creation_date": 1328226255 55 | } 56 | ] 57 | } 58 | ` 59 | -------------------------------------------------------------------------------- /stackongo/filters.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "strings" 4 | 5 | // CreateFilter creates a new filter given a list of includes, excludes, a base filter, and whether or not this filter should be "unsafe". 6 | func CreateFilter(params map[string]string) (output *Filters, error error) { 7 | output = new(Filters) 8 | error = get("filters/create", params, output) 9 | return 10 | } 11 | 12 | // InspectFilters returns the fields included by the given filters, and the "safeness" of those filters. 13 | func InspectFilters(filters []string, params map[string]string) (output *Filters, error error) { 14 | request_path := strings.Join([]string{"filters", strings.Join(filters, ";")}, "/") 15 | 16 | output = new(Filters) 17 | error = get(request_path, params, output) 18 | return 19 | 20 | } 21 | -------------------------------------------------------------------------------- /stackongo/filters_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCreateFilter(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/filters/create", dummyFiltersResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | filters, err := CreateFilter(map[string]string{"unsafe": "false"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | 17 | if filters.Items[0].Filter != "default" { 18 | t.Error("Filter invalid.") 19 | } 20 | 21 | if filters.Items[0].Included_fields[0] != ".backoff" { 22 | t.Error("Included fields are invalid.") 23 | } 24 | 25 | if filters.Items[0].Filter_type != "safe" { 26 | t.Error("Filter type invalid.") 27 | } 28 | 29 | } 30 | 31 | func TestInspectFilters(t *testing.T) { 32 | dummy_server := returnDummyResponseForPath("/2.0/filters/default;test", dummyFiltersResponse, t) 33 | defer closeDummyServer(dummy_server) 34 | 35 | _, err := InspectFilters([]string{"default", "test"}, map[string]string{"page": "1"}) 36 | 37 | if err != nil { 38 | t.Error(err.Error()) 39 | } 40 | 41 | } 42 | 43 | //Test Data 44 | 45 | var dummyFiltersResponse string = ` 46 | { 47 | "items": [ 48 | { 49 | "filter": "default", 50 | "included_fields": [ 51 | ".backoff", 52 | ".error_id", 53 | ".error_message", 54 | ".error_name", 55 | ".has_more", 56 | ".items", 57 | ".quota_max", 58 | ".quota_remaining", 59 | "access_token.access_token", 60 | "access_token.account_id", 61 | "access_token.expires_on_date", 62 | "access_token.scope", 63 | "answer.answer_id", 64 | "answer.community_owned_date", 65 | "answer.creation_date", 66 | "answer.is_accepted", 67 | "answer.last_activity_date", 68 | "answer.last_edit_date", 69 | "answer.locked_date", 70 | "answer.owner", 71 | "answer.question_id", 72 | "answer.score", 73 | "badge.award_count", 74 | "badge.badge_id", 75 | "badge.badge_type", 76 | "badge.link", 77 | "badge.name", 78 | "badge.rank", 79 | "badge.user", 80 | "badge_count.bronze", 81 | "badge_count.gold", 82 | "badge_count.silver", 83 | "comment.comment_id", 84 | "comment.creation_date", 85 | "comment.edited", 86 | "comment.owner", 87 | "comment.post_id", 88 | "comment.reply_to_user", 89 | "comment.score", 90 | "error.description", 91 | "error.error_id", 92 | "error.error_name", 93 | "event.creation_date", 94 | "event.event_id", 95 | "event.event_type", 96 | "filter.filter", 97 | "filter.filter_type", 98 | "filter.included_fields", 99 | "inbox_item.answer_id", 100 | "inbox_item.comment_id", 101 | "inbox_item.creation_date", 102 | "inbox_item.is_unread", 103 | "inbox_item.item_type", 104 | "inbox_item.link", 105 | "inbox_item.question_id", 106 | "inbox_item.site", 107 | "inbox_item.title", 108 | "info.answers_per_minute", 109 | "info.api_revision", 110 | "info.badges_per_minute", 111 | "info.new_active_users", 112 | "info.questions_per_minute", 113 | "info.total_accepted", 114 | "info.total_answers", 115 | "info.total_badges", 116 | "info.total_comments", 117 | "info.total_questions", 118 | "info.total_unanswered", 119 | "info.total_users", 120 | "info.total_votes", 121 | "migration_info.on_date", 122 | "migration_info.other_site", 123 | "migration_info.question_id", 124 | "network_user.account_id", 125 | "network_user.answer_count", 126 | "network_user.badge_counts", 127 | "network_user.creation_date", 128 | "network_user.last_access_date", 129 | "network_user.question_count", 130 | "network_user.reputation", 131 | "network_user.site_name", 132 | "network_user.site_url", 133 | "network_user.user_id", 134 | "post.creation_date", 135 | "post.last_activity_date", 136 | "post.last_edit_date", 137 | "post.owner", 138 | "post.post_id", 139 | "post.post_type", 140 | "post.score", 141 | "privilege.description", 142 | "privilege.reputation", 143 | "privilege.short_description", 144 | "question.accepted_answer_id", 145 | "question.answer_count", 146 | "question.bounty_amount", 147 | "question.bounty_closes_date", 148 | "question.closed_date", 149 | "question.closed_reason", 150 | "question.community_owned_date", 151 | "question.creation_date", 152 | "question.is_answered", 153 | "question.last_activity_date", 154 | "question.last_edit_date", 155 | "question.link", 156 | "question.locked_date", 157 | "question.migrated_from", 158 | "question.migrated_to", 159 | "question.owner", 160 | "question.protected_date", 161 | "question.question_id", 162 | "question.score", 163 | "question.tags", 164 | "question.title", 165 | "question.view_count", 166 | "question_timeline.comment_id", 167 | "question_timeline.creation_date", 168 | "question_timeline.down_vote_count", 169 | "question_timeline.owner", 170 | "question_timeline.post_id", 171 | "question_timeline.question_id", 172 | "question_timeline.revision_guid", 173 | "question_timeline.timeline_type", 174 | "question_timeline.up_vote_count", 175 | "question_timeline.user", 176 | "related_site.api_site_parameter", 177 | "related_site.name", 178 | "related_site.relation", 179 | "related_site.site_url", 180 | "reputation.on_date", 181 | "reputation.post_id", 182 | "reputation.post_type", 183 | "reputation.reputation_change", 184 | "reputation.user_id", 185 | "reputation.vote_type", 186 | "revision.comment", 187 | "revision.creation_date", 188 | "revision.is_rollback", 189 | "revision.last_tags", 190 | "revision.last_title", 191 | "revision.post_id", 192 | "revision.post_type", 193 | "revision.revision_guid", 194 | "revision.revision_number", 195 | "revision.revision_type", 196 | "revision.set_community_wiki", 197 | "revision.tags", 198 | "revision.title", 199 | "revision.user", 200 | "shallow_user.accept_rate", 201 | "shallow_user.display_name", 202 | "shallow_user.link", 203 | "shallow_user.profile_image", 204 | "shallow_user.reputation", 205 | "shallow_user.user_id", 206 | "shallow_user.user_type", 207 | "site.aliases", 208 | "site.api_site_parameter", 209 | "site.audience", 210 | "site.closed_beta_date", 211 | "site.favicon_url", 212 | "site.icon_url", 213 | "site.launch_date", 214 | "site.logo_url", 215 | "site.markdown_extensions", 216 | "site.name", 217 | "site.open_beta_date", 218 | "site.related_sites", 219 | "site.site_state", 220 | "site.site_type", 221 | "site.site_url", 222 | "site.styling", 223 | "site.twitter_account", 224 | "styling.link_color", 225 | "styling.tag_background_color", 226 | "styling.tag_foreground_color", 227 | "suggested_edit.approval_date", 228 | "suggested_edit.comment", 229 | "suggested_edit.creation_date", 230 | "suggested_edit.post_id", 231 | "suggested_edit.post_type", 232 | "suggested_edit.proposing_user", 233 | "suggested_edit.rejection_date", 234 | "suggested_edit.suggested_edit_id", 235 | "suggested_edit.tags", 236 | "suggested_edit.title", 237 | "tag.count", 238 | "tag.has_synonyms", 239 | "tag.is_moderator_only", 240 | "tag.is_required", 241 | "tag.name", 242 | "tag.user_id", 243 | "tag_score.post_count", 244 | "tag_score.score", 245 | "tag_score.user", 246 | "tag_synonym.applied_count", 247 | "tag_synonym.creation_date", 248 | "tag_synonym.from_tag", 249 | "tag_synonym.last_applied_date", 250 | "tag_synonym.to_tag", 251 | "tag_wiki.body_last_edit_date", 252 | "tag_wiki.excerpt", 253 | "tag_wiki.excerpt_last_edit_date", 254 | "tag_wiki.tag_name", 255 | "top_tag.answer_count", 256 | "top_tag.answer_score", 257 | "top_tag.question_count", 258 | "top_tag.question_score", 259 | "top_tag.tag_name", 260 | "user.accept_rate", 261 | "user.account_id", 262 | "user.age", 263 | "user.badge_counts", 264 | "user.creation_date", 265 | "user.display_name", 266 | "user.is_employee", 267 | "user.last_access_date", 268 | "user.last_modified_date", 269 | "user.link", 270 | "user.location", 271 | "user.profile_image", 272 | "user.reputation", 273 | "user.reputation_change_day", 274 | "user.reputation_change_month", 275 | "user.reputation_change_quarter", 276 | "user.reputation_change_week", 277 | "user.reputation_change_year", 278 | "user.timed_penalty_date", 279 | "user.user_id", 280 | "user.user_type", 281 | "user.website_url", 282 | "user_timeline.badge_id", 283 | "user_timeline.comment_id", 284 | "user_timeline.creation_date", 285 | "user_timeline.detail", 286 | "user_timeline.post_id", 287 | "user_timeline.post_type", 288 | "user_timeline.suggested_edit_id", 289 | "user_timeline.timeline_type", 290 | "user_timeline.title", 291 | "user_timeline.user_id" 292 | ], 293 | "filter_type": "safe" 294 | } 295 | ] 296 | } 297 | ` 298 | -------------------------------------------------------------------------------- /stackongo/inbox.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | // Inbox returns authenticated user's inbox. 4 | // This method requires an access_token, with a scope containing "read_inbox". 5 | func Inbox(params map[string]string, auth map[string]string) (output *InboxItems, error error) { 6 | //add auth params 7 | for key, value := range auth { 8 | params[key] = value 9 | } 10 | 11 | output = new(InboxItems) 12 | error = get("inbox", params, output) 13 | return 14 | } 15 | 16 | // UnreadInbox returns unread items in an authenticated user's inbox. 17 | // This method requires an access_token, with a scope containing "read_inbox". 18 | func UnreadInbox(params map[string]string, auth map[string]string) (output *InboxItems, error error) { 19 | //add auth params 20 | for key, value := range auth { 21 | params[key] = value 22 | } 23 | 24 | output = new(InboxItems) 25 | error = get("inbox/unread", params, output) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /stackongo/inbox_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInbox(t *testing.T) { 8 | dummy_server := returnDummyResponseForPathAndParams("/2.0/inbox", map[string]string{"key": "app123", "access_token": "abc"}, dummyInboxResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | inbox_items, err := Inbox(map[string]string{"page": "1"}, map[string]string{"key": "app123", "access_token": "abc"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | 17 | if len(inbox_items.Items) != 1 { 18 | t.Error("Number of items wrong.") 19 | } 20 | 21 | if inbox_items.Items[0].Item_type != "new_answer" { 22 | t.Error("Type invalid.") 23 | } 24 | 25 | if inbox_items.Items[0].Question_id != 1202 { 26 | t.Error("Question id invalid.") 27 | } 28 | 29 | if inbox_items.Items[0].Title != "Sample question" { 30 | t.Error("Title invalid.") 31 | } 32 | 33 | if inbox_items.Items[0].Creation_date != 1328148124 { 34 | t.Error("Creation date invalid.") 35 | } 36 | 37 | if inbox_items.Items[0].Site.Name != "Stack Apps" { 38 | t.Error("Creation date invalid.") 39 | } 40 | 41 | } 42 | 43 | func TestUnreadInbox(t *testing.T) { 44 | dummy_server := returnDummyResponseForPathAndParams("/2.0/inbox/unread", map[string]string{"key": "app123", "access_token": "abc"}, dummyInboxResponse, t) 45 | defer closeDummyServer(dummy_server) 46 | 47 | _, err := UnreadInbox(map[string]string{"page": "1"}, map[string]string{"key": "app123", "access_token": "abc"}) 48 | 49 | if err != nil { 50 | t.Error(err.Error()) 51 | } 52 | 53 | } 54 | 55 | //Test Data 56 | 57 | var dummyInboxResponse string = ` 58 | { 59 | "items": [ 60 | { 61 | "item_type": "new_answer", 62 | "question_id": 1202, 63 | "answer_id": 1, 64 | "title": "Sample question", 65 | "creation_date": 1328148124, 66 | "is_unread": false, 67 | "site": { 68 | "site_type": "main_site", 69 | "name": "Stack Apps", 70 | "logo_url": "http://sstatic.net/stackapps/img/logo.png", 71 | "api_site_parameter": "stackapps", 72 | "site_url": "http://stackapps.com", 73 | "audience": "apps, scripts, and development with the Stack Exchange API", 74 | "icon_url": "http://sstatic.net/stackapps/img/apple-touch-icon.png", 75 | "site_state": "normal", 76 | "styling": { 77 | "link_color": "#0077DD", 78 | "tag_foreground_color": "#555555", 79 | "tag_background_color": "#E7ECEC" 80 | }, 81 | "launch_date": 1274313600, 82 | "favicon_url": "http://sstatic.net/stackapps/img/favicon.ico", 83 | "related_sites": [ 84 | { 85 | "name": "Chat Stack Exchange", 86 | "site_url": "http://chat.stackexchange.com", 87 | "relation": "chat" 88 | } 89 | ] 90 | }, 91 | "link": "http://stackapps.com/questions/1220/sample-question/1#1" 92 | } 93 | ] 94 | } 95 | ` 96 | -------------------------------------------------------------------------------- /stackongo/info.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "errors" 4 | 5 | // Info returns a collection of statistics about the site 6 | func (session Session) Info() (output Info, error error) { 7 | collection := new(infoCollection) 8 | error = session.get("info", map[string]string{}, collection) 9 | 10 | if len(collection.Items) > 0 { 11 | output = collection.Items[0] 12 | } else { 13 | error = errors.New("Site not found") 14 | } 15 | 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /stackongo/info_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInfo(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/info", dummyInfoResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | info, err := session.Info() 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if info.Total_badges != 2575799 { 19 | t.Error("Total badges invalid.") 20 | } 21 | 22 | if info.Badges_per_minute != 1.41 { 23 | t.Error("Badges per minute invalid.") 24 | } 25 | 26 | if info.Api_revision != "2012.1.27.662" { 27 | t.Error("API revision invalid.") 28 | } 29 | 30 | } 31 | 32 | func TestNoInfo(t *testing.T) { 33 | dummy_server := returnDummyResponseForPath("/2.0/info", dummyMetaInfoResponse, t) 34 | defer closeDummyServer(dummy_server) 35 | 36 | session := NewSession("stackoverflow") 37 | _, err := session.Info() 38 | 39 | if err.Error() != "Site not found" { 40 | t.Error("Error didn't match") 41 | } 42 | } 43 | 44 | //Test Data 45 | 46 | var dummyInfoResponse string = ` 47 | { 48 | "items": [ 49 | { 50 | "total_questions": 2578583, 51 | "total_unanswered": 2061766, 52 | "total_accepted": 1615008, 53 | "total_answers": 5429736, 54 | "questions_per_minute": 1.4, 55 | "answers_per_minute": 2.95, 56 | "total_comments": 10606280, 57 | "total_votes": 16305351, 58 | "total_badges": 2575799, 59 | "badges_per_minute": 1.41, 60 | "total_users": 969429, 61 | "new_active_users": 49, 62 | "api_revision": "2012.1.27.662" 63 | } 64 | ], 65 | "quota_remaining": 9974, 66 | "quota_max": 10000, 67 | "backoff": 10, 68 | "has_more": false 69 | } 70 | ` 71 | -------------------------------------------------------------------------------- /stackongo/network_users.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AssociatedAccounts returns all associated accounts for the given user ids. 9 | func AssociatedAccounts(ids []int, params map[string]string) (output *NetworkUsers, error error) { 10 | 11 | string_ids := []string{} 12 | for _, v := range ids { 13 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 14 | } 15 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "associated"}, "/") 16 | 17 | output = new(NetworkUsers) 18 | error = get(request_path, params, output) 19 | return 20 | 21 | } 22 | -------------------------------------------------------------------------------- /stackongo/network_users_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAssociatedAccounts(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/associated", dummyNetworkUsersResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | users, err := AssociatedAccounts([]int{1, 2, 3}, map[string]string{"page": "1"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | 17 | if len(users.Items) != 3 { 18 | t.Error("Number of items wrong.") 19 | } 20 | 21 | if users.Items[0].Site_name != "Stack Overflow" { 22 | t.Error("Site name invalid.") 23 | } 24 | 25 | if users.Items[0].User_id != 1 { 26 | t.Error("user id invalid.") 27 | } 28 | 29 | if users.Items[0].Badge_counts.Gold != 25 { 30 | t.Error("badge count invalid.") 31 | } 32 | 33 | if users.Items[0].Last_access_date != 1328143328 { 34 | t.Error("last access date invalid.") 35 | } 36 | 37 | } 38 | 39 | //Test Data 40 | 41 | var dummyNetworkUsersResponse string = ` 42 | { 43 | "items": [ 44 | { 45 | "site_name": "Stack Overflow", 46 | "site_url": "http://stackoverflow.com", 47 | "user_id": 1, 48 | "reputation": 17614, 49 | "account_id": 1, 50 | "creation_date": 1217514151, 51 | "badge_counts": { 52 | "gold": 25, 53 | "silver": 84, 54 | "bronze": 97 55 | }, 56 | "last_access_date": 1328143328, 57 | "answer_count": 148, 58 | "question_count": 15 59 | }, 60 | { 61 | "site_name": "Server Fault", 62 | "site_url": "http://serverfault.com", 63 | "user_id": 1, 64 | "reputation": 4524, 65 | "account_id": 1, 66 | "creation_date": 1241075307, 67 | "badge_counts": { 68 | "gold": 4, 69 | "silver": 33, 70 | "bronze": 58 71 | }, 72 | "last_access_date": 1328142890, 73 | "answer_count": 42, 74 | "question_count": 19 75 | }, 76 | { 77 | "site_name": "Super User", 78 | "site_url": "http://superuser.com", 79 | "user_id": 1, 80 | "reputation": 8083, 81 | "account_id": 1, 82 | "creation_date": 1247439102, 83 | "badge_counts": { 84 | "gold": 9, 85 | "silver": 47, 86 | "bronze": 86 87 | }, 88 | "last_access_date": 1328145149, 89 | "answer_count": 149, 90 | "question_count": 14 91 | } 92 | ] 93 | } 94 | ` 95 | -------------------------------------------------------------------------------- /stackongo/params.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Params map[string]string 10 | 11 | // Add adds a new parameter given by the key and value 12 | func (p Params) Add(key string, value interface{}) { 13 | p[key] = fmt.Sprintf("%v", value) 14 | } 15 | 16 | // Set change the value of the given param 17 | func (p Params) Set(key string, value interface{}) { 18 | p.Add(key, value) 19 | } 20 | 21 | // Del deletes the given parameter 22 | func (p Params) Del(key string) { 23 | delete(p, key) 24 | } 25 | 26 | // Page sets the page parameter 27 | func (p Params) Page(page int) { 28 | p.Add("page", page) 29 | } 30 | 31 | // Pagesize sets the number of results for a page 32 | func (p Params) Pagesize(num int) { 33 | p.Add("pagesize", num) 34 | } 35 | 36 | // Fromdate sets fromdate parameter from the given *Time reference 37 | func (p Params) Fromdate(date time.Time) { 38 | p.Add("fromdate", date.Unix()) 39 | } 40 | 41 | // Todate sets todate parameter from the given *Time reference 42 | func (p Params) Todate(date time.Time) { 43 | p.Add("todate", date.Unix()) 44 | } 45 | 46 | // Sort sets the field to sort results 47 | func (p Params) Sort(field string) { 48 | p.Add("sort", field) 49 | } 50 | 51 | // Order sets order to sort results 52 | func (p Params) Order(order string) { 53 | p.Add("order", order) 54 | } 55 | 56 | // Min sets the minimum value the field can take 57 | func (p Params) Min(num int) { 58 | p.Add("min", num) 59 | } 60 | 61 | // Max sets the maximum value the field can take 62 | func (p Params) Max(num int) { 63 | p.Add("max", num) 64 | } 65 | 66 | // AddVectorized adds a new parameter 67 | // using the given key and vectorized value (eg. tagged=ruby;java;go) 68 | func (p Params) AddVectorized(key string, values []string) { 69 | p[key] = strings.Join(values, ";") 70 | } 71 | -------------------------------------------------------------------------------- /stackongo/params_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestParamsAdd(t *testing.T) { 9 | params := make(Params) 10 | params.Add("key", 5) 11 | 12 | if params["key"] != "5" { 13 | t.Errorf("%v doesn't match expectation", params) 14 | } 15 | } 16 | 17 | func TestParamsSet(t *testing.T) { 18 | params := make(Params) 19 | params.Set("key", 5) 20 | 21 | if params["key"] != "5" { 22 | t.Errorf("%v doesn't match expectation", params) 23 | } 24 | } 25 | 26 | func TestParamsDel(t *testing.T) { 27 | params := make(Params) 28 | params.Add("key", "test") 29 | 30 | params.Del("key") 31 | 32 | if params["key"] == "test" { 33 | t.Errorf("%v doesn't match expectation", params) 34 | } 35 | } 36 | 37 | func TestParamsPage(t *testing.T) { 38 | params := make(Params) 39 | params.Page(5) 40 | 41 | if params["page"] != "5" { 42 | t.Errorf("%v doesn't match expectation", params) 43 | } 44 | } 45 | 46 | func TestParamsPagesize(t *testing.T) { 47 | params := make(Params) 48 | params.Pagesize(25) 49 | 50 | if params["pagesize"] != "25" { 51 | t.Errorf("%v doesn't match expectation", params) 52 | } 53 | } 54 | 55 | func TestParamsFromdate(t *testing.T) { 56 | params := make(Params) 57 | date, _ := time.Parse(time.UnixDate, "Thu Feb 4 21:00:57 PST 2012") 58 | params.Fromdate(date) 59 | 60 | if params["fromdate"] != "1328389257" { 61 | t.Errorf("%v doesn't match expectation", params) 62 | } 63 | } 64 | 65 | func TestParamsTodate(t *testing.T) { 66 | params := make(Params) 67 | date, _ := time.Parse(time.UnixDate, "Thu Feb 4 21:00:57 PST 2012") 68 | params.Todate(date) 69 | 70 | if params["todate"] != "1328389257" { 71 | t.Errorf("%v doesn't match expectation", params) 72 | } 73 | } 74 | 75 | func TestParamsSort(t *testing.T) { 76 | params := make(Params) 77 | params.Sort("reputation") 78 | 79 | if params["sort"] != "reputation" { 80 | t.Errorf("%v doesn't match expectation", params) 81 | } 82 | } 83 | 84 | func TestParamsOrder(t *testing.T) { 85 | params := make(Params) 86 | params.Order("desc") 87 | 88 | if params["order"] != "desc" { 89 | t.Errorf("%v doesn't match expectation", params) 90 | } 91 | } 92 | 93 | func TestParamsMin(t *testing.T) { 94 | params := make(Params) 95 | params.Min(1) 96 | 97 | if params["min"] != "1" { 98 | t.Errorf("%v doesn't match expectation", params) 99 | } 100 | } 101 | 102 | func TestParamsMax(t *testing.T) { 103 | params := make(Params) 104 | params.Max(1000) 105 | 106 | if params["max"] != "1000" { 107 | t.Errorf("%v doesn't match expectation", params) 108 | } 109 | } 110 | 111 | func TestParamsAddVectorized(t *testing.T) { 112 | params := make(Params) 113 | params.AddVectorized("tagged", []string{"ruby", "java", "go"}) 114 | 115 | if params["tagged"] != "ruby;java;go" { 116 | t.Errorf("%v doesn't match expectation", params) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /stackongo/posts.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllPosts returns all Posts in site 9 | func (session Session) AllPosts(params map[string]string) (output *Posts, error error) { 10 | output = new(Posts) 11 | error = session.get("posts", params, output) 12 | return 13 | } 14 | 15 | // Posts returns the posts with the given ids 16 | func (session Session) GetPosts(ids []int, params map[string]string) (output *Posts, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"posts", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(Posts) 24 | error = session.get(request_path, params, output) 25 | return 26 | 27 | } 28 | -------------------------------------------------------------------------------- /stackongo/posts_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllPosts(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/posts", dummyPostsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | posts, err := session.AllPosts(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(posts.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if posts.Items[0].Post_id != 9051104 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if posts.Items[0].Post_type != "question" { 27 | t.Error("Post type invalid.") 28 | } 29 | 30 | if posts.Items[0].Owner.Display_name != "atbebtg" { 31 | t.Error("Owner invalid.") 32 | } 33 | 34 | if posts.Items[0].Creation_date != 1327813841 { 35 | t.Error("Date invalid.") 36 | } 37 | 38 | } 39 | 40 | func TestGetPosts(t *testing.T) { 41 | dummy_server := returnDummyResponseForPath("/2.0/posts/1;2;3", dummyPostsResponse, t) 42 | defer closeDummyServer(dummy_server) 43 | 44 | session := NewSession("stackoverflow") 45 | _, err := session.GetPosts([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 46 | 47 | if err != nil { 48 | t.Error(err.Error()) 49 | } 50 | 51 | } 52 | 53 | //Test Data 54 | 55 | var dummyPostsResponse string = ` 56 | { 57 | "items": [ 58 | { 59 | "post_id": 9051104, 60 | "post_type": "question", 61 | "owner": { 62 | "user_id": 502574, 63 | "display_name": "atbebtg", 64 | "reputation": 614, 65 | "user_type": "registered", 66 | "profile_image": "http://www.gravatar.com/avatar/3d796e73a7bf18f9338290dbca0d6717?d=identicon&r=PG", 67 | "link": "http://stackoverflow.com/users/502574/atbebtg" 68 | }, 69 | "creation_date": 1327813841, 70 | "last_activity_date": 1327813841, 71 | "score": 0 72 | }, 73 | { 74 | "post_id": 9051103, 75 | "post_type": "question", 76 | "owner": { 77 | "user_id": 460920, 78 | "display_name": "user460920", 79 | "reputation": 16, 80 | "user_type": "registered", 81 | "profile_image": "http://www.gravatar.com/avatar/10ea58fd1c8456a24d9f671d446935df?d=identicon&r=PG", 82 | "link": "http://stackoverflow.com/users/460920/user460920" 83 | }, 84 | "creation_date": 1327813831, 85 | "last_activity_date": 1327813831, 86 | "score": 0 87 | }, 88 | { 89 | "post_id": 9051102, 90 | "post_type": "question", 91 | "owner": { 92 | "user_id": 483619, 93 | "display_name": "Dave Ferguson", 94 | "reputation": 554, 95 | "user_type": "registered", 96 | "profile_image": "http://www.gravatar.com/avatar/a7bdbd77eff0feb358f106588a83b149?d=identicon&r=PG", 97 | "link": "http://stackoverflow.com/users/483619/dave-ferguson" 98 | }, 99 | "creation_date": 1327813751, 100 | "last_activity_date": 1327813751, 101 | "score": 0 102 | } 103 | ] 104 | } 105 | ` 106 | -------------------------------------------------------------------------------- /stackongo/privileges.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "fmt" 4 | 5 | // AllPrivileges returns all privileges available in site 6 | func (session Session) AllPrivileges(params map[string]string) (output *Privileges, error error) { 7 | output = new(Privileges) 8 | error = session.get("privileges", params, output) 9 | return 10 | } 11 | 12 | // PrivilegesForUser returns the privileges a user has 13 | func (session Session) PrivilegesForUser(id int, params map[string]string) (output *Privileges, error error) { 14 | request_path := fmt.Sprintf("users/%v/privileges", id) 15 | 16 | output = new(Privileges) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/privileges_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllPrivileges(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/privileges", dummyPrivilegesResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | privileges, err := session.AllPrivileges(map[string]string{}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(privileges.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if privileges.Items[0].Short_description != "create posts" { 23 | t.Error("Short description is invalid.") 24 | } 25 | 26 | if privileges.Items[0].Reputation != 1 { 27 | t.Error("Reputation is invalid.") 28 | } 29 | 30 | } 31 | 32 | func TestPrivilegesForUser(t *testing.T) { 33 | dummy_server := returnDummyResponseForPath("/2.0/users/1/privileges", dummyPrivilegesResponse, t) 34 | defer closeDummyServer(dummy_server) 35 | 36 | session := NewSession("stackoverflow") 37 | _, err := session.PrivilegesForUser(1, map[string]string{}) 38 | 39 | if err != nil { 40 | t.Error(err.Error()) 41 | } 42 | 43 | } 44 | 45 | //Test Data 46 | 47 | var dummyPrivilegesResponse string = ` 48 | { 49 | "items": [ 50 | { 51 | "short_description": "create posts", 52 | "description": "Ask and answer questions", 53 | "reputation": 1 54 | }, 55 | { 56 | "short_description": "participate in meta", 57 | "description": "Participate in per-site meta", 58 | "reputation": 5 59 | }, 60 | { 61 | "short_description": "remove new user restrictions", 62 | "description": "Remove new user restrictions", 63 | "reputation": 10 64 | } 65 | ] 66 | } 67 | ` 68 | -------------------------------------------------------------------------------- /stackongo/question_timelines.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // TimelineForQuestions returns a subset of the events that have happened to the questions identified with ids. 9 | func (session Session) TimelineForQuestions(ids []int, params map[string]string) (output *QuestionTimelines, error error) { 10 | string_ids := []string{} 11 | for _, v := range ids { 12 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 13 | } 14 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";"), "timeline"}, "/") 15 | 16 | output = new(QuestionTimelines) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/question_timelines_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTimelineForQuestions(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3/timeline", dummyQuestionTimelinesResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | question_timelines, err := session.TimelineForQuestions([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(question_timelines.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if question_timelines.Items[0].Timeline_type != "comment" { 23 | t.Error("Timeline type invalid.") 24 | } 25 | 26 | if question_timelines.Items[0].Creation_date != 1328052381 { 27 | t.Error("Date invalid.") 28 | } 29 | 30 | if question_timelines.Items[0].User.Display_name != "eouw0o83hf" { 31 | t.Error("User invalid.") 32 | } 33 | 34 | } 35 | 36 | //Test Data 37 | 38 | var dummyQuestionTimelinesResponse string = ` 39 | { 40 | "items": [ 41 | { 42 | "timeline_type": "comment", 43 | "question_id": 9087138, 44 | "post_id": 9087152, 45 | "comment_id": 11413013, 46 | "creation_date": 1328052381, 47 | "user": { 48 | "user_id": 570190, 49 | "display_name": "eouw0o83hf", 50 | "reputation": 242, 51 | "user_type": "registered", 52 | "profile_image": "http://www.gravatar.com/avatar/e0ff68bf6b06206afd062af7ed0d640a?d=identicon&r=PG", 53 | "link": "http://stackoverflow.com/users/570190/eouw0o83hf", 54 | "accept_rate": 75 55 | } 56 | }, 57 | { 58 | "timeline_type": "comment", 59 | "question_id": 9087138, 60 | "post_id": 9087152, 61 | "comment_id": 11412816, 62 | "creation_date": 1328051616, 63 | "user": { 64 | "user_id": 264918, 65 | "display_name": "EB.", 66 | "reputation": 60, 67 | "user_type": "registered", 68 | "profile_image": "http://www.gravatar.com/avatar/21ddc54277d0234a30a70b3460270dd8?d=identicon&r=PG", 69 | "link": "http://stackoverflow.com/users/264918/eb", 70 | "accept_rate": 83 71 | } 72 | }, 73 | { 74 | "timeline_type": "comment", 75 | "question_id": 9087138, 76 | "post_id": 9087152, 77 | "comment_id": 11412653, 78 | "creation_date": 1328050976, 79 | "user": { 80 | "user_id": 570190, 81 | "display_name": "eouw0o83hf", 82 | "reputation": 242, 83 | "user_type": "registered", 84 | "profile_image": "http://www.gravatar.com/avatar/e0ff68bf6b06206afd062af7ed0d640a?d=identicon&r=PG", 85 | "link": "http://stackoverflow.com/users/570190/eouw0o83hf", 86 | "accept_rate": 75 87 | } 88 | } 89 | ] 90 | } 91 | ` 92 | -------------------------------------------------------------------------------- /stackongo/questions.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllQuestions returns all questions 9 | func (session Session) AllQuestions(params map[string]string) (output *Questions, error error) { 10 | output = new(Questions) 11 | error = session.get("questions", params, output) 12 | return 13 | } 14 | 15 | // Questions returns the questions for given ids 16 | func (session Session) GetQuestions(ids []int, params map[string]string) (output *Questions, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(Questions) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // UnansweredQuestions returns all unanswered questions 29 | func (session Session) UnansweredQuestions(params map[string]string) (output *Questions, error error) { 30 | output = new(Questions) 31 | error = session.get("questions/unanswered", params, output) 32 | return 33 | } 34 | 35 | // QuestionsWithNoAnswers returns questions with no answers 36 | func (session Session) QuestionsWithNoAnswers(params map[string]string) (output *Questions, error error) { 37 | output = new(Questions) 38 | error = session.get("questions/no-answers", params, output) 39 | return 40 | } 41 | 42 | // ReleatedQuestions returns the questions releated to the questions identified with given ids 43 | func (session Session) RelatedQuestions(ids []int, params map[string]string) (output *Questions, error error) { 44 | string_ids := []string{} 45 | for _, v := range ids { 46 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 47 | } 48 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";"), "related"}, "/") 49 | 50 | output = new(Questions) 51 | error = session.get(request_path, params, output) 52 | return 53 | } 54 | 55 | // LinkedQuestions returns the questions linked to the questions identified with given ids 56 | func (session Session) LinkedQuestions(ids []int, params map[string]string) (output *Questions, error error) { 57 | string_ids := []string{} 58 | for _, v := range ids { 59 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 60 | } 61 | request_path := strings.Join([]string{"questions", strings.Join(string_ids, ";"), "linked"}, "/") 62 | 63 | output = new(Questions) 64 | error = session.get(request_path, params, output) 65 | return 66 | } 67 | 68 | // QuestionsFromUsers returns the questions asked by the users with given ids 69 | func (session Session) QuestionsFromUsers(ids []int, params map[string]string) (output *Questions, error error) { 70 | string_ids := []string{} 71 | for _, v := range ids { 72 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 73 | } 74 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "questions"}, "/") 75 | 76 | output = new(Questions) 77 | error = session.get(request_path, params, output) 78 | return 79 | } 80 | 81 | // QuestionsWithNoAnswersFromUsers returns the questions without answers asked by the users with given ids 82 | func (session Session) QuestionsWithNoAnswersFromUsers(ids []int, params map[string]string) (output *Questions, error error) { 83 | string_ids := []string{} 84 | for _, v := range ids { 85 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 86 | } 87 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "questions", "no-answers"}, "/") 88 | 89 | output = new(Questions) 90 | error = session.get(request_path, params, output) 91 | return 92 | } 93 | 94 | // UnacceptedQuestionsFromUsers returns the unaccepted questions asked by the users with given ids 95 | func (session Session) UnacceptedQuestionsFromUsers(ids []int, params map[string]string) (output *Questions, error error) { 96 | string_ids := []string{} 97 | for _, v := range ids { 98 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 99 | } 100 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "questions", "unaccepted"}, "/") 101 | 102 | output = new(Questions) 103 | error = session.get(request_path, params, output) 104 | return 105 | } 106 | 107 | // UnansweredQuestionsFromUsers returns the unanswered questions asked by the users with given ids 108 | func (session Session) UnansweredQuestionsFromUsers(ids []int, params map[string]string) (output *Questions, error error) { 109 | string_ids := []string{} 110 | for _, v := range ids { 111 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 112 | } 113 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "questions", "unanswered"}, "/") 114 | 115 | output = new(Questions) 116 | error = session.get(request_path, params, output) 117 | return 118 | } 119 | 120 | // FavoriteQuestionsFromUsers returns the favorite questions by users with given ids 121 | func (session Session) FavoriteQuestionsFromUsers(ids []int, params map[string]string) (output *Questions, error error) { 122 | string_ids := []string{} 123 | for _, v := range ids { 124 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 125 | } 126 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "favorites"}, "/") 127 | 128 | output = new(Questions) 129 | error = session.get(request_path, params, output) 130 | return 131 | } 132 | 133 | // TopQuestionsFromUsers returns the top questions from the users identified with given ids for the questions with given tags 134 | func (session Session) TopQuestionsFromUsers(ids []int, tags []string, params map[string]string) (output *Questions, error error) { 135 | 136 | string_ids := []string{} 137 | for _, v := range ids { 138 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 139 | } 140 | 141 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "tags", strings.Join(tags, ";"), "top-questions"}, "/") 142 | 143 | output = new(Questions) 144 | error = session.get(request_path, params, output) 145 | return 146 | } 147 | 148 | // FAQForTags returns the frequently asked questions for the given tags 149 | func (session Session) FAQForTags(tags []string, params map[string]string) (output *Questions, error error) { 150 | request_path := strings.Join([]string{"tags", strings.Join(tags, ";"), "faq"}, "/") 151 | 152 | output = new(Questions) 153 | error = session.get(request_path, params, output) 154 | return 155 | } 156 | 157 | // Search queries the post titles with the given query and returns the matching questions. You can used params to set other criteria such as `tagged` 158 | func (session Session) Search(query string, params map[string]string) (output *Questions, error error) { 159 | request_path := "search" 160 | // set query as a param 161 | params["intitle"] = query 162 | 163 | output = new(Questions) 164 | error = session.get(request_path, params, output) 165 | return 166 | } 167 | 168 | // Similar returns questions similar to the given query 169 | func (session Session) Similar(query string, params map[string]string) (output *Questions, error error) { 170 | request_path := "similar" 171 | // set query as a param 172 | params["title"] = query 173 | 174 | output = new(Questions) 175 | error = session.get(request_path, params, output) 176 | return 177 | } 178 | -------------------------------------------------------------------------------- /stackongo/questions_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | "strings" 6 | ) 7 | 8 | func TestAllQuestions(t *testing.T) { 9 | dummy_server := returnDummyResponseForPath("/2.0/questions", dummyQuestionsResponse, t) 10 | defer closeDummyServer(dummy_server) 11 | 12 | session := NewSession("stackoverflow") 13 | questions, err := session.AllQuestions(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 14 | 15 | if err != nil { 16 | t.Error(err.Error()) 17 | } 18 | 19 | if len(questions.Items) != 3 { 20 | t.Error("Number of items wrong.") 21 | } 22 | 23 | if questions.Items[0].Title != "GetAvailableWebTemplates returns nothing" { 24 | t.Error("Title invalid.") 25 | } 26 | 27 | if strings.Join(questions.Items[0].Tags, ", ") != "sharepoint, templates" { 28 | t.Error("Tags invalid.") 29 | } 30 | 31 | if questions.Items[0].Owner.Display_name != "Nacht" { 32 | t.Error("Owner invalid.") 33 | } 34 | 35 | if questions.Items[0].Creation_date != 1327453582 { 36 | t.Error("Date invalid.") 37 | } 38 | 39 | } 40 | 41 | func TestGetQuestions(t *testing.T) { 42 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3", dummyQuestionsResponse, t) 43 | defer closeDummyServer(dummy_server) 44 | 45 | session := NewSession("stackoverflow") 46 | _, err := session.GetQuestions([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 47 | 48 | if err != nil { 49 | t.Error(err.Error()) 50 | } 51 | 52 | } 53 | 54 | func TestUnansweredQuestions(t *testing.T) { 55 | dummy_server := returnDummyResponseForPath("/2.0/questions/unanswered", dummyQuestionsResponse, t) 56 | defer closeDummyServer(dummy_server) 57 | 58 | session := NewSession("stackoverflow") 59 | _, err := session.UnansweredQuestions(map[string]string{}) 60 | 61 | if err != nil { 62 | t.Error(err.Error()) 63 | } 64 | 65 | } 66 | 67 | func TestQuestionsWithNoAnswers(t *testing.T) { 68 | dummy_server := returnDummyResponseForPath("/2.0/questions/no-answers", dummyQuestionsResponse, t) 69 | defer closeDummyServer(dummy_server) 70 | 71 | session := NewSession("stackoverflow") 72 | _, err := session.QuestionsWithNoAnswers(map[string]string{}) 73 | 74 | if err != nil { 75 | t.Error(err.Error()) 76 | } 77 | 78 | } 79 | 80 | func TestRelatedQuestions(t *testing.T) { 81 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3/related", dummyQuestionsResponse, t) 82 | defer closeDummyServer(dummy_server) 83 | 84 | session := NewSession("stackoverflow") 85 | _, err := session.RelatedQuestions([]int{1, 2, 3}, map[string]string{}) 86 | 87 | if err != nil { 88 | t.Error(err.Error()) 89 | } 90 | 91 | } 92 | 93 | func TestLinkedQuestions(t *testing.T) { 94 | dummy_server := returnDummyResponseForPath("/2.0/questions/1;2;3/linked", dummyQuestionsResponse, t) 95 | defer closeDummyServer(dummy_server) 96 | 97 | session := NewSession("stackoverflow") 98 | _, err := session.LinkedQuestions([]int{1, 2, 3}, map[string]string{}) 99 | 100 | if err != nil { 101 | t.Error(err.Error()) 102 | } 103 | 104 | } 105 | 106 | func TestQuestionsFromUsers(t *testing.T) { 107 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/questions", dummyQuestionsResponse, t) 108 | defer closeDummyServer(dummy_server) 109 | 110 | session := NewSession("stackoverflow") 111 | _, err := session.QuestionsFromUsers([]int{1, 2, 3}, map[string]string{}) 112 | 113 | if err != nil { 114 | t.Error(err.Error()) 115 | } 116 | 117 | } 118 | 119 | func TestQuestionsWithNoAnswersFromUsers(t *testing.T) { 120 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/questions/no-answers", dummyQuestionsResponse, t) 121 | defer closeDummyServer(dummy_server) 122 | 123 | session := NewSession("stackoverflow") 124 | _, err := session.QuestionsWithNoAnswersFromUsers([]int{1, 2, 3}, map[string]string{}) 125 | 126 | if err != nil { 127 | t.Error(err.Error()) 128 | } 129 | 130 | } 131 | 132 | func TestUnacceptedQuestionsFromUsers(t *testing.T) { 133 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/questions/unaccepted", dummyQuestionsResponse, t) 134 | defer closeDummyServer(dummy_server) 135 | 136 | session := NewSession("stackoverflow") 137 | _, err := session.UnacceptedQuestionsFromUsers([]int{1, 2, 3}, map[string]string{}) 138 | 139 | if err != nil { 140 | t.Error(err.Error()) 141 | } 142 | 143 | } 144 | 145 | func TestUnansweredQuestionsFromUsers(t *testing.T) { 146 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/questions/unanswered", dummyQuestionsResponse, t) 147 | defer closeDummyServer(dummy_server) 148 | 149 | session := NewSession("stackoverflow") 150 | _, err := session.UnansweredQuestionsFromUsers([]int{1, 2, 3}, map[string]string{}) 151 | 152 | if err != nil { 153 | t.Error(err.Error()) 154 | } 155 | 156 | } 157 | 158 | func TestFavoriteQuestionsFromUsers(t *testing.T) { 159 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/favorites", dummyQuestionsResponse, t) 160 | defer closeDummyServer(dummy_server) 161 | 162 | session := NewSession("stackoverflow") 163 | _, err := session.FavoriteQuestionsFromUsers([]int{1, 2, 3}, map[string]string{}) 164 | 165 | if err != nil { 166 | t.Error(err.Error()) 167 | } 168 | 169 | } 170 | 171 | func TestTopQuestionsFromUsers(t *testing.T) { 172 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/tags/hello;world/top-questions", dummyQuestionsResponse, t) 173 | defer closeDummyServer(dummy_server) 174 | 175 | session := NewSession("stackoverflow") 176 | _, err := session.TopQuestionsFromUsers([]int{1, 2, 3}, []string{"hello", "world"}, map[string]string{}) 177 | 178 | if err != nil { 179 | t.Error(err.Error()) 180 | } 181 | 182 | } 183 | 184 | func TestFAQForTags(t *testing.T) { 185 | dummy_server := returnDummyResponseForPath("/2.0/tags/hello;world/faq", dummyQuestionsResponse, t) 186 | defer closeDummyServer(dummy_server) 187 | 188 | session := NewSession("stackoverflow") 189 | _, err := session.FAQForTags([]string{"hello", "world"}, map[string]string{}) 190 | 191 | if err != nil { 192 | t.Error(err.Error()) 193 | } 194 | 195 | } 196 | 197 | func TestSearch(t *testing.T) { 198 | dummy_server := returnDummyResponseForPathAndParams("/2.0/search", map[string]string{"intitle": "hello world", "tagged": "basic"}, dummyQuestionsResponse, t) 199 | defer closeDummyServer(dummy_server) 200 | 201 | session := NewSession("stackoverflow") 202 | _, err := session.Search("hello world", map[string]string{"tagged": "basic"}) 203 | 204 | if err != nil { 205 | t.Error(err.Error()) 206 | } 207 | 208 | } 209 | 210 | func TestSimilar(t *testing.T) { 211 | dummy_server := returnDummyResponseForPathAndParams("/2.0/similar", map[string]string{"title": "hello world", "tagged": "basic"}, dummyQuestionsResponse, t) 212 | defer closeDummyServer(dummy_server) 213 | 214 | session := NewSession("stackoverflow") 215 | _, err := session.Similar("hello world", map[string]string{"tagged": "basic"}) 216 | 217 | if err != nil { 218 | t.Error(err.Error()) 219 | } 220 | 221 | } 222 | 223 | //Test Data 224 | 225 | var dummyQuestionsResponse string = `{ 226 | "items": [ 227 | { 228 | "question_id": 8996647, 229 | "creation_date": 1327453582, 230 | "last_activity_date": 1327453582, 231 | "score": 0, 232 | "answer_count": 0, 233 | "title": "GetAvailableWebTemplates returns nothing", 234 | "tags": [ 235 | "sharepoint", 236 | "templates" 237 | ], 238 | "view_count": 1, 239 | "owner": { 240 | "user_id": 983442, 241 | "display_name": "Nacht", 242 | "reputation": 132, 243 | "user_type": "registered", 244 | "profile_image": "http://www.gravatar.com/avatar/5775b55f9b3be541dbbf9967cf3d1f16?d=identicon&r=PG", 245 | "link": "http://stackoverflow.com/users/983442/nacht" 246 | }, 247 | "link": "http://stackoverflow.com/questions/8996647/getavailablewebtemplates-returns-nothing", 248 | "is_answered": false 249 | }, 250 | { 251 | "question_id": 8996646, 252 | "creation_date": 1327453577, 253 | "last_activity_date": 1327453577, 254 | "score": 0, 255 | "answer_count": 0, 256 | "title": "Netbeans c++ remote execution", 257 | "tags": [ 258 | "c", 259 | "netbeans", 260 | "remote-server" 261 | ], 262 | "view_count": 1, 263 | "owner": { 264 | "user_id": 676516, 265 | "display_name": "James Cotter", 266 | "reputation": 7, 267 | "user_type": "registered", 268 | "profile_image": "http://www.gravatar.com/avatar/b3133db6c2e37607dd036766f3bcf6fd?d=identicon&r=PG", 269 | "link": "http://stackoverflow.com/users/676516/james-cotter" 270 | }, 271 | "link": "http://stackoverflow.com/questions/8996646/netbeans-c-remote-execution", 272 | "is_answered": false 273 | }, 274 | { 275 | "question_id": 8996578, 276 | "creation_date": 1327452958, 277 | "last_activity_date": 1327453572, 278 | "score": 0, 279 | "answer_count": 1, 280 | "title": "UrlEncode for Silverlight?", 281 | "tags": [ 282 | "silverlight" 283 | ], 284 | "view_count": 4, 285 | "owner": { 286 | "user_id": 5274, 287 | "display_name": "Jonathan Allen", 288 | "reputation": 14239, 289 | "user_type": "registered", 290 | "profile_image": "http://www.gravatar.com/avatar/71c1027d2483fe242b0affc5e59df647?d=identicon&r=PG", 291 | "link": "http://stackoverflow.com/users/5274/jonathan-allen" 292 | }, 293 | "link": "http://stackoverflow.com/questions/8996578/urlencode-for-silverlight", 294 | "is_answered": false 295 | } 296 | ], 297 | "quota_remaining": 9989, 298 | "quota_max": 10000, 299 | "has_more": true 300 | }` 301 | -------------------------------------------------------------------------------- /stackongo/reputations.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ReputationChangesForUsers returns a subset of the reputation changes for users with given ids. 9 | func (session Session) ReputationChangesForUsers(ids []int, params map[string]string) (output *Reputations, error error) { 10 | string_ids := []string{} 11 | for _, v := range ids { 12 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 13 | } 14 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "reputation"}, "/") 15 | 16 | output = new(Reputations) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/reputations_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReputationChangesForUsers(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/reputation", dummyReputationsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | reputations, err := session.ReputationChangesForUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(reputations.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if reputations.Items[0].User_id != 1 { 23 | t.Error("User ID is invalid.") 24 | } 25 | 26 | if reputations.Items[0].Post_type != "answer" { 27 | t.Error("Post type is invalid.") 28 | } 29 | 30 | if reputations.Items[0].On_date != 1326828986 { 31 | t.Error("On date is invalid.") 32 | } 33 | 34 | } 35 | 36 | //Test Data 37 | 38 | var dummyReputationsResponse string = ` 39 | { 40 | "items": [ 41 | { 42 | "user_id": 1, 43 | "post_id": 225592, 44 | "post_type": "answer", 45 | "vote_type": "up_votes", 46 | "reputation_change": 160, 47 | "on_date": 1326828986 48 | }, 49 | { 50 | "user_id": 2, 51 | "post_id": 1722923, 52 | "post_type": "answer", 53 | "vote_type": "up_votes", 54 | "reputation_change": 10, 55 | "on_date": 1326366463 56 | }, 57 | { 58 | "user_id": 3, 59 | "post_id": 7038708, 60 | "post_type": "question", 61 | "vote_type": "up_votes", 62 | "reputation_change": 10, 63 | "on_date": 1325566470 64 | } 65 | ] 66 | } 67 | ` 68 | -------------------------------------------------------------------------------- /stackongo/resource_types.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | type AccessToken struct { 4 | Access_token string 5 | Expires_on_date int64 6 | Account_id int 7 | Scope []string 8 | } 9 | 10 | type Answer struct { 11 | Question_id int 12 | Answer_id int 13 | Locked_date int64 14 | Creation_date int64 15 | Last_edit_date int64 16 | Last_activity_date int64 17 | Score int 18 | Community_owned_date int64 19 | Is_accepted bool 20 | Body string 21 | Owner ShallowUser 22 | Title string 23 | Up_vote_count int 24 | Down_vote_count int 25 | Comments []Comment 26 | Link string 27 | } 28 | 29 | type Badge struct { 30 | Badge_id int 31 | Rank string 32 | Name string 33 | Description string 34 | Award_count int 35 | Badge_type string 36 | User ShallowUser 37 | Link string 38 | } 39 | 40 | type BadgeCount struct { 41 | Gold int 42 | Silver int 43 | Bronze int 44 | } 45 | 46 | type Comment struct { 47 | Comment_id int 48 | Post_id int 49 | Creation_date int64 50 | Post_type string //one of question, or answer 51 | Score int 52 | Edited bool 53 | Body string 54 | Owner ShallowUser 55 | Reply_to_user ShallowUser 56 | Link string 57 | } 58 | 59 | type Error struct { 60 | Error_id int 61 | Error_name string 62 | Error_message string 63 | } 64 | 65 | type Event struct { 66 | Event_type string //one of question_posted, answer_posted, comment_posted, post_edited, or user_created 67 | Event_id int //refers to an event 68 | Creation_date int64 69 | Link string 70 | Excerpt string 71 | } 72 | 73 | type Filter struct { 74 | Filter string 75 | Included_fields []string 76 | Filter_type string //one of safe, unsafe, or invalid 77 | } 78 | 79 | type InboxItem struct { 80 | Item_type string //one of comment, chat_message, new_answer, careers_message, careers_invitations, or meta_question 81 | Question_id int // refers to a question 82 | Answer_id int // refers to an answer 83 | Comment_id int //refers to a comment 84 | Title string 85 | Creation_date int64 86 | Is_unread bool 87 | Site Site 88 | Body string 89 | Link string 90 | } 91 | 92 | type Info struct { 93 | Total_questions int 94 | Total_unanswered int 95 | Total_accepted int 96 | Total_answers int 97 | Questions_per_minute float32 98 | Answers_per_minute float32 99 | Total_comments int 100 | Total_votes int 101 | Total_badges int 102 | Badges_per_minute float32 103 | Total_users int 104 | New_active_users int 105 | Api_revision string 106 | } 107 | 108 | type NetworkUser struct { 109 | Site_name string 110 | Site_url string 111 | User_id int 112 | Reputation int 113 | Account_id int 114 | Creation_date int64 115 | User_type string //one of unregistered, registered, moderator, or does_not_exist 116 | Badge_counts BadgeCount 117 | Last_access_date int64 118 | Answer_count int 119 | Question_count int 120 | } 121 | 122 | type MetaInfo struct { 123 | Backoff int 124 | Has_more bool 125 | Page int 126 | Page_size int 127 | Quota_max int 128 | Quota_remaining int 129 | Total int 130 | Type string 131 | } 132 | 133 | type Migration_info struct { 134 | Question_id int 135 | Other_site Site 136 | On_date int64 137 | } 138 | 139 | type Post struct { 140 | Post_id int 141 | Post_type string 142 | Body string 143 | Owner ShallowUser 144 | Creation_date int64 145 | Last_activity_date int64 146 | Last_edit_date int64 147 | Score int 148 | Up_vote_count int 149 | Down_vote_count int 150 | Comments []Comment 151 | } 152 | 153 | type Privilege struct { 154 | Short_description string 155 | Description string 156 | Reputation int 157 | } 158 | 159 | type Question struct { 160 | Question_id int 161 | Last_edit_date int64 162 | Creation_date int64 163 | Last_activity_date int64 164 | Locked_date int64 165 | Community_owned_date int64 166 | Score int 167 | Answer_count int 168 | Accepted_answer_id int 169 | Migrated_to Migration_info 170 | Migrated_from Migration_info 171 | Bounty_closes_date int64 172 | Bounty_amount int 173 | Closed_date int64 174 | Protected_date int64 175 | Body string 176 | Title string 177 | Tags []string 178 | Closed_reason string 179 | Up_vote_count int 180 | Down_vote_count int 181 | Favorite_count int 182 | View_count int 183 | Owner ShallowUser 184 | Comments []Comment 185 | Answers []Answer 186 | Link string 187 | Is_answered bool 188 | } 189 | 190 | type QuestionTimeline struct { 191 | Timeline_type string //one of question, answer, comment, unaccepted_answer, accepted_answer, vote_aggregate, revision, or post_state_changed 192 | Question_id int 193 | Post_id int 194 | Comment_id int 195 | Revision_guid string 196 | Up_vote_count int 197 | Down_vote_count int 198 | Creation_date int64 199 | User ShallowUser 200 | Owner ShallowUser 201 | } 202 | 203 | type Reputation struct { 204 | User_id int 205 | Post_id int 206 | Post_type string //one of question, or answer 207 | Vote_type string //one of accepts, up_votes, down_votes, bounties_offered, bounties_won, spam, or suggested_edits 208 | Title string 209 | Link string 210 | Reputation_change int 211 | On_date int64 212 | } 213 | 214 | type Revision struct { 215 | Revision_guid string 216 | Revision_number int 217 | Revision_type string //one of single_user, or vote_based 218 | Post_type string //one of question, or answer 219 | Post_id int 220 | Comment string 221 | Creation_date int64 222 | Is_rollback bool 223 | Last_body string 224 | Last_title string 225 | Last_tags []string 226 | Body string 227 | Title string 228 | Tags []string 229 | Set_community_wiki bool 230 | User ShallowUser 231 | } 232 | 233 | type RelatedSite struct { 234 | Name string 235 | Site_url string 236 | Relation string //one of parent, meta, chat, or other 237 | Api_site_parameter string 238 | } 239 | 240 | type ShallowUser struct { 241 | User_id int 242 | Display_name string 243 | Reputation int 244 | User_type string //one of unregistered, registered, moderator, or does_not_exist 245 | Profile_image string 246 | Link string 247 | } 248 | 249 | type Site struct { 250 | Site_type string 251 | Name string 252 | Logo_url string 253 | Api_site_parameter string 254 | Site_url string 255 | Audience string 256 | Icon_url string 257 | Aliases []string 258 | Site_state string //one of normal, closed_beta, open_beta, or linked_meta 259 | Styling Styling 260 | Closed_beta_date int64 261 | Open_beta_date int64 262 | Launch_date int64 263 | Favicon_url string 264 | Related_sites []RelatedSite 265 | Twitter_account string 266 | Markdown_extensions []string 267 | } 268 | 269 | type Styling struct { 270 | Link_color string 271 | Tag_foreground_color string 272 | Tag_background_color string 273 | } 274 | 275 | type SuggestedEdit struct { 276 | Suggested_edit_id int 277 | Post_id int 278 | Post_type string //one of question, or answer 279 | Body string 280 | Title string 281 | Tags []string 282 | Comment string 283 | Creation_date int64 284 | Approval_date int64 285 | Rejection_date int64 286 | Proposing_user ShallowUser 287 | } 288 | 289 | type Tag struct { 290 | Name string 291 | Count int 292 | Is_required bool 293 | Is_moderator_only bool 294 | User_id int 295 | Has_synonyms bool 296 | Last_activity_date int64 297 | } 298 | 299 | type TagScore struct { 300 | User ShallowUser 301 | Score int 302 | Post_count int 303 | } 304 | 305 | type TagSynonym struct { 306 | From_tag string 307 | To_tag string 308 | Applied_count int 309 | Last_applied_date int64 310 | Creation_date int64 311 | } 312 | 313 | type TagWiki struct { 314 | Tag_name string 315 | Body string 316 | Excerpt string 317 | Body_last_edit_date int64 318 | Excerpt_last_edit_date int64 319 | Last_body_editor ShallowUser 320 | Last_excerpt_editor ShallowUser 321 | } 322 | 323 | type TopTag struct { 324 | Tag_name string 325 | Question_score int 326 | Question_count int 327 | Answer_score int 328 | Answer_count int 329 | } 330 | 331 | type User struct { 332 | User_id int 333 | User_type string //one of unregistered, registered, moderator, or does_not_exist 334 | Creation_date int64 335 | Display_name string 336 | Profile_image string 337 | Reputation int 338 | Reputation_change_day int 339 | Reputation_change_week int 340 | Reputation_change_month int 341 | Reputation_change_quarter int 342 | Reputation_change_year int 343 | Age int 344 | Last_access_date int64 345 | Last_modified_date int64 346 | Is_employee bool 347 | Link string 348 | Website_url string 349 | Location string 350 | Account_id int 351 | Timed_penalty_date int64 352 | Badge_counts BadgeCount 353 | Question_count int 354 | Answer_count int 355 | Up_vote_count int 356 | Down_vote_count int 357 | About_me string 358 | View_count int 359 | Accept_rate int 360 | } 361 | 362 | type UserTimeline struct { 363 | Creation_date int64 364 | Post_type string //one of question, or answer 365 | Timeline_type string //one of commented, asked, answered, badge, revision, accepted, reviewed, or suggested 366 | User_id int 367 | Post_id int 368 | Comment_id int 369 | Suggested_edit_id int 370 | Badge_id int 371 | Title string 372 | Detail string 373 | Link string 374 | } 375 | -------------------------------------------------------------------------------- /stackongo/revisions.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Revisions returns edit revisions identified by given ids 9 | func (session Session) Revisions(ids []int, params map[string]string) (output *Revisions, error error) { 10 | string_ids := []string{} 11 | for _, v := range ids { 12 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 13 | } 14 | request_path := strings.Join([]string{"revisions", strings.Join(string_ids, ";")}, "/") 15 | 16 | output = new(Revisions) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | 21 | // RevisionsForPosts returns the revisions for the posts identified with given ids 22 | func (session Session) RevisionsForPosts(ids []int, params map[string]string) (output *Revisions, error error) { 23 | string_ids := []string{} 24 | for _, v := range ids { 25 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 26 | } 27 | request_path := strings.Join([]string{"posts", strings.Join(string_ids, ";"), "revisions"}, "/") 28 | 29 | output = new(Revisions) 30 | error = session.get(request_path, params, output) 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /stackongo/revisions_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRevisions(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/revisions/1;2;3", dummyRevisionsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | revisions, err := session.Revisions([]int{1, 2, 3}, map[string]string{}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(revisions.Items) != 3 { 19 | t.Error("Number of items are wrong.") 20 | } 21 | 22 | if revisions.Items[0].Revision_guid != "C8AF433A-8DFE-4906-9418-EB7D5B4522EA" { 23 | t.Error("guid is invalid.") 24 | } 25 | 26 | if revisions.Items[0].Creation_date != 1220598880 { 27 | t.Error("Creation date is invalid.") 28 | } 29 | 30 | if revisions.Items[0].User.Display_name != "JasonMichael" { 31 | t.Error("User is invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestRevisionsForPosts(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/posts/1;2;3/revisions", dummyRevisionsResponse, t) 38 | defer closeDummyServer(dummy_server) 39 | 40 | session := NewSession("stackoverflow") 41 | _, err := session.RevisionsForPosts([]int{1, 2, 3}, map[string]string{}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | } 48 | 49 | //Test Data 50 | 51 | var dummyRevisionsResponse string = ` 52 | { 53 | "items": [ 54 | { 55 | "revision_guid": "C8AF433A-8DFE-4906-9418-EB7D5B4522EA", 56 | "revision_number": 0, 57 | "revision_type": "vote_based", 58 | "post_type": "answer", 59 | "post_id": 16321, 60 | "comment": "<b>Post Deleted</b> by <a href="/users/1935/jasonmichael">JasonMichael</a>", 61 | "creation_date": 1220598880, 62 | "is_rollback": false, 63 | "set_community_wiki": false, 64 | "user": { 65 | "user_id": 1935, 66 | "display_name": "JasonMichael", 67 | "reputation": 645, 68 | "user_type": "registered", 69 | "profile_image": "http://www.gravatar.com/avatar/16fdddee3335cb859fe7bb300cf02cdb?d=identicon&r=PG", 70 | "link": "http://stackoverflow.com/users/1935/jasonmichael" 71 | } 72 | }, 73 | { 74 | "revision_guid": "F7658FFB-6F1C-412F-A6D9-18B17B560381", 75 | "revision_number": 0, 76 | "revision_type": "vote_based", 77 | "post_type": "answer", 78 | "post_id": 16445, 79 | "comment": "<b>Post Deleted</b> by <a href="/users/1444/humpton">Humpton</a>", 80 | "creation_date": 1219590393, 81 | "is_rollback": false, 82 | "set_community_wiki": false, 83 | "user": { 84 | "user_id": 1444, 85 | "display_name": "Humpton", 86 | "reputation": 327, 87 | "user_type": "registered", 88 | "profile_image": "http://www.gravatar.com/avatar/15c189676f875b5a245f3bd6e87744f1?d=identicon&r=PG", 89 | "link": "http://stackoverflow.com/users/1444/humpton" 90 | } 91 | }, 92 | { 93 | "revision_guid": "CA1797BD-E4C4-4649-AC1D-EB2C5B69B4BF", 94 | "revision_number": 1, 95 | "revision_type": "single_user", 96 | "post_type": "answer", 97 | "post_id": 16545, 98 | "creation_date": 1219165179, 99 | "is_rollback": false, 100 | "set_community_wiki": false, 101 | "user": { 102 | "display_name": "mauro", 103 | "user_type": "does_not_exist" 104 | } 105 | } 106 | ] 107 | } 108 | ` 109 | -------------------------------------------------------------------------------- /stackongo/session.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | var host string = "http://api.stackexchange.com" 13 | var transport http.RoundTripper 14 | 15 | // UseSecure set to communicate using SSL 16 | var UseSSL bool = true 17 | 18 | type Session struct { 19 | Site string 20 | } 21 | 22 | func NewSession(site string) *Session { 23 | return &Session{Site: site} 24 | } 25 | 26 | func getTransport() http.RoundTripper { 27 | if transport != nil { 28 | return transport 29 | } 30 | return http.DefaultTransport 31 | } 32 | 33 | func SetTransport(t http.RoundTripper) { 34 | transport = t 35 | } 36 | 37 | // construct the endpoint URL 38 | func setupEndpoint(path string, params map[string]string) *url.URL { 39 | base_url, _ := url.Parse(host) 40 | 41 | if UseSSL { 42 | base_url.Scheme = "https" 43 | } else { 44 | base_url.Scheme = "http" 45 | } 46 | 47 | endpoint, _ := base_url.Parse("/2.0/" + path) 48 | 49 | query := endpoint.Query() 50 | for key, value := range params { 51 | query.Set(key, value) 52 | } 53 | 54 | endpoint.RawQuery = query.Encode() 55 | 56 | return endpoint 57 | } 58 | 59 | // parse the response 60 | func parseResponse(response *http.Response, result interface{}) (error error) { 61 | // close the body when done reading 62 | defer response.Body.Close() 63 | 64 | //read the response 65 | bytes, error := ioutil.ReadAll(response.Body) 66 | 67 | if error != nil { 68 | return 69 | } 70 | 71 | //parse JSON 72 | error = json.Unmarshal(bytes, result) 73 | 74 | if error != nil { 75 | print(error.Error()) 76 | } 77 | 78 | //check whether the response is a bad request 79 | if response.StatusCode == 400 { 80 | error = errors.New(fmt.Sprintf("Bad Request: %s", string(bytes))) 81 | } 82 | 83 | return 84 | } 85 | 86 | func (session Session) get(section string, params map[string]string, collection interface{}) (error error) { 87 | //set parameters for querystring 88 | params["site"] = session.Site 89 | 90 | return get(section, params, collection) 91 | } 92 | 93 | func get(section string, params map[string]string, collection interface{}) (error error) { 94 | client := &http.Client{Transport: getTransport()} 95 | 96 | response, error := client.Get(setupEndpoint(section, params).String()) 97 | 98 | if error != nil { 99 | return 100 | } 101 | 102 | error = parseResponse(response, collection) 103 | 104 | return 105 | 106 | } 107 | -------------------------------------------------------------------------------- /stackongo/session_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func closeDummyServer(dummy_server *httptest.Server) { 11 | transport = nil 12 | dummy_server.Close() 13 | } 14 | 15 | func createDummyServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server { 16 | dummy_server := httptest.NewServer(http.HandlerFunc(handler)) 17 | 18 | //change the host to use the test server 19 | SetTransport(&http.Transport{Proxy: func(*http.Request) (*url.URL, error) { return url.Parse(dummy_server.URL) }}) 20 | 21 | //turn off SSL 22 | UseSSL = false 23 | 24 | return dummy_server 25 | } 26 | 27 | func returnDummyResponseForPath(path string, dummy_response string, t *testing.T) *httptest.Server { 28 | //serve dummy responses 29 | dummy_data := []byte(dummy_response) 30 | 31 | return createDummyServer(func(w http.ResponseWriter, r *http.Request) { 32 | if r.URL.Path != path { 33 | t.Error("Path doesn't match") 34 | } 35 | w.Write(dummy_data) 36 | }) 37 | } 38 | 39 | func returnDummyResponseForPathAndParams(path string, params map[string]string, dummy_response string, t *testing.T) *httptest.Server { 40 | //serve dummy responses 41 | dummy_data := []byte(dummy_response) 42 | 43 | return createDummyServer(func(w http.ResponseWriter, r *http.Request) { 44 | if r.URL.Path != path { 45 | t.Error("Path doesn't match") 46 | } 47 | 48 | for key, value := range params { 49 | if r.URL.Query().Get(key) != value { 50 | t.Error("Expected " + key + " to equal " + value + ". Got " + r.URL.Query().Get(key)) 51 | } 52 | } 53 | w.Write(dummy_data) 54 | }) 55 | } 56 | 57 | func returnDummyErrorResponseForPath(path string, dummy_response string, t *testing.T) *httptest.Server { 58 | //serve dummy responses 59 | dummy_data := []byte(dummy_response) 60 | 61 | return createDummyServer(func(w http.ResponseWriter, r *http.Request) { 62 | if r.URL.Path != path { 63 | t.Error("Path doesn't match") 64 | } 65 | w.WriteHeader(400) 66 | w.Write(dummy_data) 67 | }) 68 | } 69 | 70 | func TestMetaInfo(t *testing.T) { 71 | dummy_server := returnDummyResponseForPath("/2.0/questions", dummyMetaInfoResponse, t) 72 | defer closeDummyServer(dummy_server) 73 | 74 | session := NewSession("stackoverflow") 75 | results, err := session.AllQuestions(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 76 | 77 | if err != nil { 78 | t.Error(err.Error()) 79 | } 80 | 81 | if results.Has_more != true { 82 | t.Error("Has more field invalid.") 83 | } 84 | 85 | if results.Quota_remaining != 9989 { 86 | t.Error("Quota remaining invalid.") 87 | } 88 | 89 | if results.Quota_max != 10000 { 90 | t.Error("Quota max invalid.") 91 | } 92 | 93 | } 94 | 95 | func TestRequestError(t *testing.T) { 96 | dummy_server := returnDummyErrorResponseForPath("/2.0/questions", dummyErrorResponse, t) 97 | defer closeDummyServer(dummy_server) 98 | 99 | session := NewSession("stackoverflow") 100 | result, err := session.AllQuestions(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 101 | 102 | if err != nil && result.Error_name != "no_method" && result.Error_message != "simulated" { 103 | t.Error("Y U GAVE NO ERROR?") 104 | } 105 | 106 | } 107 | 108 | //test data 109 | 110 | var dummyMetaInfoResponse string = ` 111 | { 112 | "items": [], 113 | "quota_remaining": 9989, 114 | "quota_max": 10000, 115 | "has_more": true 116 | } 117 | ` 118 | -------------------------------------------------------------------------------- /stackongo/sites.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | // AllSites returns all sites available in StackExchange network 4 | func AllSites(params map[string]string) (output *Sites, error error) { 5 | output = new(Sites) 6 | error = get("sites", params, output) 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /stackongo/sites_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllSites(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/sites", dummySitesResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | sites, err := AllSites(map[string]string{"page": "1"}) 12 | 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | 17 | if len(sites.Items) != 3 { 18 | t.Error("Number of items wrong.") 19 | } 20 | 21 | if sites.Items[0].Site_type != "main_site" { 22 | t.Error("Site type invalid.") 23 | } 24 | 25 | if sites.Items[0].Aliases[0] != "http://www.stackoverflow.com" { 26 | t.Error("alias invalid.") 27 | } 28 | 29 | if sites.Items[0].Related_sites[0].Name != "Stack Overflow Chat" { 30 | t.Error("related site invalid.") 31 | } 32 | 33 | } 34 | 35 | //Test Data 36 | 37 | var dummySitesResponse string = ` 38 | { 39 | "items": [ 40 | { 41 | "site_type": "main_site", 42 | "name": "Stack Overflow", 43 | "logo_url": "http://sstatic.net/stackoverflow/img/logo.png", 44 | "api_site_parameter": "stackoverflow", 45 | "site_url": "http://stackoverflow.com", 46 | "audience": "professional and enthusiast programmers", 47 | "icon_url": "http://sstatic.net/stackoverflow/img/apple-touch-icon.png", 48 | "aliases": [ 49 | "http://www.stackoverflow.com" 50 | ], 51 | "site_state": "normal", 52 | "styling": { 53 | "link_color": "#0077CC", 54 | "tag_foreground_color": "#3E6D8E", 55 | "tag_background_color": "#E0EAF1" 56 | }, 57 | "launch_date": 1221436800, 58 | "favicon_url": "http://sstatic.net/stackoverflow/img/favicon.ico", 59 | "related_sites": [ 60 | { 61 | "name": "Stack Overflow Chat", 62 | "site_url": "http://chat.stackoverflow.com", 63 | "relation": "chat" 64 | } 65 | ], 66 | "markdown_extensions": [ 67 | "Prettify" 68 | ] 69 | }, 70 | { 71 | "site_type": "main_site", 72 | "name": "Server Fault", 73 | "logo_url": "http://sstatic.net/serverfault/img/logo.png", 74 | "api_site_parameter": "serverfault", 75 | "site_url": "http://serverfault.com", 76 | "audience": "system administrators and desktop support professionals", 77 | "icon_url": "http://sstatic.net/serverfault/img/apple-touch-icon.png", 78 | "site_state": "normal", 79 | "styling": { 80 | "link_color": "#10456A", 81 | "tag_foreground_color": "#444444", 82 | "tag_background_color": "#F3F1D9" 83 | }, 84 | "launch_date": 1243296000, 85 | "favicon_url": "http://sstatic.net/serverfault/img/favicon.ico", 86 | "related_sites": [ 87 | { 88 | "name": "Meta Server Fault", 89 | "site_url": "http://meta.serverfault.com", 90 | "relation": "meta", 91 | "api_site_parameter": "meta.serverfault" 92 | }, 93 | { 94 | "name": "Chat Stack Exchange", 95 | "site_url": "http://chat.stackexchange.com", 96 | "relation": "chat" 97 | } 98 | ], 99 | "twitter_account": "ServerFault" 100 | }, 101 | { 102 | "site_type": "main_site", 103 | "name": "Super User", 104 | "logo_url": "http://sstatic.net/superuser/img/logo.png", 105 | "api_site_parameter": "superuser", 106 | "site_url": "http://superuser.com", 107 | "audience": "computer enthusiasts and power users", 108 | "icon_url": "http://sstatic.net/superuser/img/apple-touch-icon.png", 109 | "site_state": "normal", 110 | "styling": { 111 | "link_color": "#1086A4", 112 | "tag_foreground_color": "#1087A4", 113 | "tag_background_color": "#FFFFFF" 114 | }, 115 | "launch_date": 1250553600, 116 | "favicon_url": "http://sstatic.net/superuser/img/favicon.ico", 117 | "related_sites": [ 118 | { 119 | "name": "Meta Super User", 120 | "site_url": "http://meta.superuser.com", 121 | "relation": "meta", 122 | "api_site_parameter": "meta.superuser" 123 | }, 124 | { 125 | "name": "Chat Stack Exchange", 126 | "site_url": "http://chat.stackexchange.com", 127 | "relation": "chat" 128 | } 129 | ], 130 | "twitter_account": "StackSuper_User" 131 | } 132 | ] 133 | } 134 | ` 135 | -------------------------------------------------------------------------------- /stackongo/suggested_edits.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllSuggestedEdits returns all the suggested edits in the systems. 9 | func (session Session) AllSuggestedEdits(params map[string]string) (output *SuggestedEdits, error error) { 10 | output = new(SuggestedEdits) 11 | error = session.get("suggested-edits", params, output) 12 | return 13 | } 14 | 15 | // SuggestedEdits returns suggested edits identified in ids. 16 | func (session Session) GetSuggestedEdits(ids []int, params map[string]string) (output *SuggestedEdits, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"suggested-edits", strings.Join(string_ids, ";")}, "/") 22 | 23 | output = new(SuggestedEdits) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // SuggestedEditsForPosts returns the suggested edits for the posts identified with given ids 29 | func (session Session) SuggestedEditsForPosts(ids []int, params map[string]string) (output *SuggestedEdits, error error) { 30 | string_ids := []string{} 31 | for _, v := range ids { 32 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 33 | } 34 | request_path := strings.Join([]string{"posts", strings.Join(string_ids, ";"), "suggested-edits"}, "/") 35 | 36 | output = new(SuggestedEdits) 37 | error = session.get(request_path, params, output) 38 | return 39 | } 40 | 41 | // SuggestedEditsFromUsers returns the suggested edits submitted by users with given ids. 42 | func (session Session) SuggestedEditsFromUsers(ids []int, params map[string]string) (output *SuggestedEdits, error error) { 43 | string_ids := []string{} 44 | for _, v := range ids { 45 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 46 | } 47 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "suggested-edits"}, "/") 48 | 49 | output = new(SuggestedEdits) 50 | error = session.get(request_path, params, output) 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /stackongo/suggested_edits_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | "strings" 6 | ) 7 | 8 | func TestAllSuggestedEdits(t *testing.T) { 9 | dummy_server := returnDummyResponseForPath("/2.0/suggested-edits", dummySuggestedEditsResponse, t) 10 | defer closeDummyServer(dummy_server) 11 | 12 | session := NewSession("stackoverflow") 13 | suggested_edits, err := session.AllSuggestedEdits(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 14 | 15 | if err != nil { 16 | t.Error(err.Error()) 17 | } 18 | 19 | if len(suggested_edits.Items) != 2 { 20 | t.Error("Number of items wrong.") 21 | } 22 | 23 | if suggested_edits.Items[0].Suggested_edit_id != 190741 { 24 | t.Error("ID invalid.") 25 | } 26 | 27 | if suggested_edits.Items[0].Proposing_user.Display_name != "Natali" { 28 | t.Error("Owner invalid.") 29 | } 30 | 31 | if suggested_edits.Items[0].Creation_date != 1327922327 { 32 | t.Error("Date invalid.") 33 | } 34 | 35 | if strings.Join(suggested_edits.Items[1].Tags, ",") != "c#,jquery,asp.net,html" { 36 | t.Error("Tags invalid.") 37 | } 38 | 39 | } 40 | 41 | func TestGetSuggestedEdits(t *testing.T) { 42 | dummy_server := returnDummyResponseForPath("/2.0/suggested-edits/1;2;3", dummySuggestedEditsResponse, t) 43 | defer closeDummyServer(dummy_server) 44 | 45 | session := NewSession("stackoverflow") 46 | _, err := session.GetSuggestedEdits([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 47 | 48 | if err != nil { 49 | t.Error(err.Error()) 50 | } 51 | 52 | } 53 | 54 | func TestSuggestedEditsForPosts(t *testing.T) { 55 | dummy_server := returnDummyResponseForPath("/2.0/posts/1;2;3/suggested-edits", dummySuggestedEditsResponse, t) 56 | defer closeDummyServer(dummy_server) 57 | 58 | session := NewSession("stackoverflow") 59 | _, err := session.SuggestedEditsForPosts([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 60 | 61 | if err != nil { 62 | t.Error(err.Error()) 63 | } 64 | } 65 | 66 | func TestSuggestedEditsFromUsers(t *testing.T) { 67 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/suggested-edits", dummySuggestedEditsResponse, t) 68 | defer closeDummyServer(dummy_server) 69 | 70 | session := NewSession("stackoverflow") 71 | _, err := session.SuggestedEditsFromUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 72 | 73 | if err != nil { 74 | t.Error(err.Error()) 75 | } 76 | 77 | } 78 | 79 | //Test Data 80 | 81 | var dummySuggestedEditsResponse string = ` 82 | { 83 | "items": [ 84 | { 85 | "suggested_edit_id": 190741, 86 | "post_id": 9062968, 87 | "post_type": "question", 88 | "comment": "improveing formating", 89 | "creation_date": 1327922327, 90 | "proposing_user": { 91 | "user_id": 601868, 92 | "display_name": "Natali", 93 | "reputation": 405, 94 | "user_type": "registered", 95 | "profile_image": "http://www.gravatar.com/avatar/b15f339505879a64a8ca7b7eaa6f8585?d=identicon&r=PG", 96 | "link": "http://stackoverflow.com/users/601868/natali" 97 | } 98 | }, 99 | { 100 | "suggested_edit_id": 190740, 101 | "post_id": 9052990, 102 | "post_type": "question", 103 | "title": "How to get values in hidden fields without clicking the submit button", 104 | "tags": [ 105 | "c#", 106 | "jquery", 107 | "asp.net", 108 | "html" 109 | ], 110 | "comment": "changed tags, spelling correction, changed title to be more relevent and improved formating", 111 | "creation_date": 1327922303, 112 | "proposing_user": { 113 | "user_id": 27922, 114 | "display_name": "TheAlbear", 115 | "reputation": 1265, 116 | "user_type": "registered", 117 | "profile_image": "http://www.gravatar.com/avatar/e46f50e7b35efdcf2945f6e40b10df7f?d=identicon&r=PG", 118 | "link": "http://stackoverflow.com/users/27922/thealbear" 119 | } 120 | } 121 | ] 122 | } 123 | ` 124 | -------------------------------------------------------------------------------- /stackongo/tag_scores.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "strings" 4 | 5 | // TopAnswerers returns the top 30 answerers active in a single tag, for the given period 6 | // period should be either `all_time` or `month` 7 | func (session Session) TopAnswerers(tag string, period string, params map[string]string) (output *TagScores, error error) { 8 | request_path := strings.Join([]string{"tags", tag, "top-answerers", period}, "/") 9 | 10 | output = new(TagScores) 11 | error = session.get(request_path, params, output) 12 | return 13 | } 14 | 15 | // TopAskers returns the top 30 askers active in a single tag, for the given period 16 | // period should be either `all_time` or `month` 17 | func (session Session) TopAskers(tag string, period string, params map[string]string) (output *TagScores, error error) { 18 | request_path := strings.Join([]string{"tags", tag, "top-askers", period}, "/") 19 | 20 | output = new(TagScores) 21 | error = session.get(request_path, params, output) 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /stackongo/tag_scores_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTopAnswerers(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/tags/test/top-answerers/all_time", dummyTagScoresResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | tag_scores, err := session.TopAnswerers("test", "all_time", map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(tag_scores.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if tag_scores.Items[0].Score != 45 { 23 | t.Error("Score invalid.") 24 | } 25 | 26 | if tag_scores.Items[0].Post_count != 1 { 27 | t.Error("Post count invalid.") 28 | } 29 | 30 | if tag_scores.Items[0].User.Display_name != "user208987" { 31 | t.Error("User invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestTopAskers(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/tags/test/top-askers/all_time", dummyTagScoresResponse, t) 38 | defer closeDummyServer(dummy_server) 39 | 40 | session := NewSession("stackoverflow") 41 | _, err := session.TopAskers("test", "all_time", map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | } 48 | 49 | //Test Data 50 | 51 | var dummyTagScoresResponse string = ` 52 | { 53 | "items": [ 54 | { 55 | "user": { 56 | "user_id": 208987, 57 | "display_name": "user208987", 58 | "reputation": 234, 59 | "user_type": "registered", 60 | "profile_image": "http://www.gravatar.com/avatar/449b9a01c4cba54b275435cfabc54e30?d=identicon&r=PG", 61 | "link": "http://stackoverflow.com/users/208987/user208987" 62 | }, 63 | "score": 45, 64 | "post_count": 1 65 | }, 66 | { 67 | "user": { 68 | "user_id": 7743, 69 | "display_name": "Jeroen Dirks", 70 | "reputation": 1661, 71 | "user_type": "registered", 72 | "profile_image": "http://www.gravatar.com/avatar/2d3c00ac7272c33d84be8e310b95e93c?d=identicon&r=PG", 73 | "link": "http://stackoverflow.com/users/7743/jeroen-dirks", 74 | "accept_rate": 97 75 | }, 76 | "score": 41, 77 | "post_count": 6 78 | }, 79 | { 80 | "user": { 81 | "user_id": 211210, 82 | "display_name": "Malte Schledjewski", 83 | "reputation": 330, 84 | "user_type": "registered", 85 | "profile_image": "http://www.gravatar.com/avatar/9c9a69a4563e030a3a0cbed8c9ca19ee?d=identicon&r=PG", 86 | "link": "http://stackoverflow.com/users/211210/malte-schledjewski" 87 | }, 88 | "score": 41, 89 | "post_count": 1 90 | } 91 | ] 92 | } 93 | ` 94 | -------------------------------------------------------------------------------- /stackongo/tag_synonyms.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "strings" 4 | 5 | // AllTagSynonyms returns all tag synonyms in site 6 | func (session Session) AllTagSynonyms(params map[string]string) (output *TagSynonyms, error error) { 7 | output = new(TagSynonyms) 8 | error = session.get("tags/synonyms", params, output) 9 | return 10 | } 11 | 12 | // SynonymsForTags returns all the synonyms that point to the given tags 13 | func (session Session) SynonymsForTags(tags []string, params map[string]string) (output *TagSynonyms, error error) { 14 | request_path := strings.Join([]string{"tags", strings.Join(tags, ";"), "synonyms"}, "/") 15 | 16 | output = new(TagSynonyms) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/tag_synonyms_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllTagSynonyms(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/tags/synonyms", dummyTagSynonymsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | tag_synonyms, err := session.AllTagSynonyms(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(tag_synonyms.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if tag_synonyms.Items[0].From_tag != "acoustic-echo-cancellatio" { 23 | t.Error("From tag invalid.") 24 | } 25 | 26 | if tag_synonyms.Items[0].To_tag != "aec" { 27 | t.Error("To tag invalid.") 28 | } 29 | 30 | if tag_synonyms.Items[0].Applied_count != 0 { 31 | t.Error("Applied count invalid.") 32 | } 33 | 34 | if tag_synonyms.Items[0].Creation_date != 1327953917 { 35 | t.Error("Creation date invalid.") 36 | } 37 | 38 | } 39 | 40 | func TestSynonymsForTags(t *testing.T) { 41 | dummy_server := returnDummyResponseForPath("/2.0/tags/tag1;tag2;tag3/synonyms", dummyTagSynonymsResponse, t) 42 | defer closeDummyServer(dummy_server) 43 | 44 | session := NewSession("stackoverflow") 45 | _, err := session.SynonymsForTags([]string{"tag1", "tag2", "tag3"}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 46 | 47 | if err != nil { 48 | t.Error(err.Error()) 49 | } 50 | 51 | } 52 | 53 | //Test Data 54 | 55 | var dummyTagSynonymsResponse string = ` 56 | { 57 | "items": [ 58 | { 59 | "from_tag": "acoustic-echo-cancellatio", 60 | "to_tag": "aec", 61 | "applied_count": 0, 62 | "creation_date": 1327953917 63 | }, 64 | { 65 | "from_tag": "validate", 66 | "to_tag": "validation", 67 | "applied_count": 3, 68 | "last_applied_date": 1328085114, 69 | "creation_date": 1327411904 70 | }, 71 | { 72 | "from_tag": "assets-pipeline", 73 | "to_tag": "asset-pipeline", 74 | "applied_count": 0, 75 | "creation_date": 1327698162 76 | } 77 | ] 78 | } 79 | ` 80 | -------------------------------------------------------------------------------- /stackongo/tag_wikis.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import "strings" 4 | 5 | // WikisForTags returns the wikis that go with the given set of tags 6 | func (session Session) WikisForTags(tags []string, params map[string]string) (output *TagWikis, error error) { 7 | request_path := strings.Join([]string{"tags", strings.Join(tags, ";"), "wikis"}, "/") 8 | 9 | output = new(TagWikis) 10 | error = session.get(request_path, params, output) 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /stackongo/tag_wikis_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWikisForTags(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/tags/tag1;tag2;tag3/wikis", dummyTagWikisResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | tag_wikis, err := session.WikisForTags([]string{"tag1", "tag2", "tag3"}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(tag_wikis.Items) != 1 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if tag_wikis.Items[0].Tag_name != "go" { 23 | t.Error("Tag name invalid.") 24 | } 25 | 26 | if tag_wikis.Items[0].Body_last_edit_date != 1322081597 { 27 | t.Error("last edit date invalid.") 28 | } 29 | 30 | } 31 | 32 | //Test Data 33 | 34 | var dummyTagWikisResponse string = ` 35 | { 36 | "items": [ 37 | { 38 | "tag_name": "go", 39 | "excerpt": "Go is a general-purpose programming language designed by Google.", 40 | "body_last_edit_date": 1322081597, 41 | "excerpt_last_edit_date": 1322081452 42 | } 43 | ] 44 | } 45 | ` 46 | -------------------------------------------------------------------------------- /stackongo/tags.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // AllTags returns all tags in site 9 | func (session Session) AllTags(params map[string]string) (output *Tags, error error) { 10 | output = new(Tags) 11 | error = session.get("tags", params, output) 12 | return 13 | } 14 | 15 | // TagsForUsers returns the tags the users identified with given ids have been active in. 16 | func (session Session) TagsForUsers(ids []int, params map[string]string) (output *Tags, error error) { 17 | string_ids := []string{} 18 | for _, v := range ids { 19 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 20 | } 21 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "tags"}, "/") 22 | 23 | output = new(Tags) 24 | error = session.get(request_path, params, output) 25 | return 26 | } 27 | 28 | // RelatedTags returns the tags that are most related to those in given tags. 29 | func (session Session) RelatedTags(tags []string, params map[string]string) (output *Tags, error error) { 30 | request_path := strings.Join([]string{"tags", strings.Join(tags, ";"), "tags"}, "/") 31 | 32 | output = new(Tags) 33 | error = session.get(request_path, params, output) 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /stackongo/tags_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllTags(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/tags", dummyTagsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | tags, err := session.AllTags(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(tags.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if tags.Items[0].Name != "c#" { 23 | t.Error("Name invalid.") 24 | } 25 | 26 | if tags.Items[0].Count != 261768 { 27 | t.Error("Tag count invalid.") 28 | } 29 | 30 | if tags.Items[0].Has_synonyms != true { 31 | t.Error("boolean invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestTagsForUsers(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/tags", dummyTagsResponse, t) 38 | defer closeDummyServer(dummy_server) 39 | 40 | session := NewSession("stackoverflow") 41 | _, err := session.TagsForUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | } 48 | 49 | func TestRelatedTags(t *testing.T) { 50 | dummy_server := returnDummyResponseForPath("/2.0/tags/tag1;tag2;tag3/tags", dummyTagsResponse, t) 51 | defer closeDummyServer(dummy_server) 52 | 53 | session := NewSession("stackoverflow") 54 | _, err := session.RelatedTags([]string{"tag1", "tag2", "tag3"}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 55 | 56 | if err != nil { 57 | t.Error(err.Error()) 58 | } 59 | 60 | } 61 | 62 | //Test Data 63 | 64 | var dummyTagsResponse string = ` 65 | { 66 | "items": [ 67 | { 68 | "name": "c#", 69 | "count": 261768, 70 | "is_required": false, 71 | "is_moderator_only": false, 72 | "has_synonyms": true 73 | }, 74 | { 75 | "name": "java", 76 | "count": 202323, 77 | "is_required": false, 78 | "is_moderator_only": false, 79 | "has_synonyms": true 80 | }, 81 | { 82 | "name": "php", 83 | "count": 187267, 84 | "is_required": false, 85 | "is_moderator_only": false, 86 | "has_synonyms": true 87 | } 88 | ] 89 | } 90 | ` 91 | -------------------------------------------------------------------------------- /stackongo/top_tags.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // TopTagsByAnswerForUser returns a single user's top tags by answer score. 9 | func (session Session) TopTagsByAnswerForUser(id int, params map[string]string) (output *TopTags, error error) { 10 | request_path := strings.Join([]string{"users", fmt.Sprintf("%v", id), "top-answer-tags"}, "/") 11 | 12 | output = new(TopTags) 13 | error = session.get(request_path, params, output) 14 | return 15 | } 16 | 17 | // TopTagsByQuestionForUser returns a single user's top tags by question score. 18 | func (session Session) TopTagsByQuestionForUser(id int, params map[string]string) (output *TopTags, error error) { 19 | request_path := strings.Join([]string{"users", fmt.Sprintf("%v", id), "top-question-tags"}, "/") 20 | 21 | output = new(TopTags) 22 | error = session.get(request_path, params, output) 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /stackongo/top_tags_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTopTagsByAnswerForUser(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/users/1/top-answer-tags", dummyTopTagsResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | tags, err := session.TopTagsByAnswerForUser(1, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(tags.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if tags.Items[0].Tag_name != "sql-server" { 23 | t.Error("Name invalid.") 24 | } 25 | 26 | if tags.Items[0].Answer_score != 89 { 27 | t.Error("Answer score invalid.") 28 | } 29 | 30 | if tags.Items[0].Answer_count != 8 { 31 | t.Error("Answer count invalid.") 32 | } 33 | 34 | } 35 | 36 | func TestTopTagsByQuestionForUser(t *testing.T) { 37 | dummy_server := returnDummyResponseForPath("/2.0/users/1/top-question-tags", dummyTopTagsResponse, t) 38 | defer closeDummyServer(dummy_server) 39 | 40 | session := NewSession("stackoverflow") 41 | tags, err := session.TopTagsByQuestionForUser(1, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 42 | 43 | if err != nil { 44 | t.Error(err.Error()) 45 | } 46 | 47 | if len(tags.Items) != 3 { 48 | t.Error("Number of items wrong.") 49 | } 50 | 51 | if tags.Items[0].Tag_name != "sql-server" { 52 | t.Error("Name invalid.") 53 | } 54 | 55 | if tags.Items[0].Question_score != 369 { 56 | t.Error("Question score invalid.") 57 | } 58 | 59 | if tags.Items[0].Question_count != 4 { 60 | t.Error("Question count invalid.") 61 | } 62 | 63 | } 64 | 65 | //Test Data 66 | 67 | var dummyTopTagsResponse string = ` 68 | { 69 | "items": [ 70 | { 71 | "tag_name": "sql-server", 72 | "question_score": 369, 73 | "question_count": 4, 74 | "answer_score": 89, 75 | "answer_count": 8 76 | }, 77 | { 78 | "tag_name": "sql", 79 | "question_score": 292, 80 | "question_count": 2, 81 | "answer_score": 60, 82 | "answer_count": 4 83 | }, 84 | { 85 | "tag_name": "parameters", 86 | "question_score": 196, 87 | "question_count": 1, 88 | "answer_score": 0, 89 | "answer_count": 0 90 | } 91 | ] 92 | } 93 | ` 94 | -------------------------------------------------------------------------------- /stackongo/user_timelines.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // TimelineForUsers returns a subset of the actions the users with given ids have taken on the site. 9 | func (session Session) TimelineForUsers(ids []int, params map[string]string) (output *UserTimelines, error error) { 10 | string_ids := []string{} 11 | for _, v := range ids { 12 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 13 | } 14 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";"), "timeline"}, "/") 15 | 16 | output = new(UserTimelines) 17 | error = session.get(request_path, params, output) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /stackongo/user_timelines_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTimelineForUsers(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3/timeline", dummyUserTimelinesResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | user_timelines, err := session.TimelineForUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(user_timelines.Items) != 3 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if user_timelines.Items[0].User_id != 22656 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if user_timelines.Items[0].Post_type != "answer" { 27 | t.Error("Post type invalid.") 28 | } 29 | 30 | if user_timelines.Items[0].Creation_date != 1328047513 { 31 | t.Error("Date invalid.") 32 | } 33 | 34 | if user_timelines.Items[0].Title != "Thread concurrency issue even within one single command?" { 35 | t.Error("Title invalid.") 36 | } 37 | 38 | } 39 | 40 | //Test Data 41 | 42 | var dummyUserTimelinesResponse string = ` 43 | { 44 | "items": [ 45 | { 46 | "creation_date": 1328047513, 47 | "post_type": "answer", 48 | "timeline_type": "answered", 49 | "user_id": 22656, 50 | "post_id": 9087675, 51 | "title": "Thread concurrency issue even within one single command?" 52 | }, 53 | { 54 | "creation_date": 1328044996, 55 | "post_type": "question", 56 | "timeline_type": "commented", 57 | "user_id": 22656, 58 | "post_id": 9087138, 59 | "comment_id": 11410707, 60 | "title": "Display varchar Date as D/M/Y?", 61 | "detail": "Why is your date field in the database as a varchar in the first place? If you can possibly fix the schema, that would be the best approach." 62 | }, 63 | { 64 | "creation_date": 1328044954, 65 | "post_type": "answer", 66 | "timeline_type": "commented", 67 | "user_id": 22656, 68 | "post_id": 9086766, 69 | "comment_id": 11410691, 70 | "title": "Joining two tables and returning multiple records as one row using LINQ", 71 | "detail": "@Zajn: You might want to use officeNames = offices.Select(o => o.OfficeName).ToList() or something like that - it's hard to say without knowing more about what you're doing. Iterating over the office names should work anyway though." 72 | } 73 | ] 74 | } 75 | ` 76 | -------------------------------------------------------------------------------- /stackongo/users.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // AllUsers returns all users in site 10 | func (session Session) AllUsers(params map[string]string) (output *Users, error error) { 11 | output = new(Users) 12 | error = session.get("users", params, output) 13 | return 14 | } 15 | 16 | // Users returns the users with the given ids 17 | func (session Session) GetUsers(ids []int, params map[string]string) (output *Users, error error) { 18 | string_ids := []string{} 19 | for _, v := range ids { 20 | string_ids = append(string_ids, fmt.Sprintf("%v", v)) 21 | } 22 | request_path := strings.Join([]string{"users", strings.Join(string_ids, ";")}, "/") 23 | 24 | output = new(Users) 25 | error = session.get(request_path, params, output) 26 | return 27 | } 28 | 29 | // AuthenticatedUser returns the user associated with the passed auth_token. 30 | func (session Session) AuthenticatedUser(params map[string]string, auth map[string]string) (output User, error error) { 31 | //add auth params 32 | for key, value := range auth { 33 | params[key] = value 34 | } 35 | 36 | collection := new(Users) 37 | error = session.get("me", params, collection) 38 | 39 | if error != nil { 40 | error = errors.New(collection.Error_name + ": " + collection.Error_message) 41 | return 42 | } 43 | 44 | if len(collection.Items) > 0 { 45 | output = collection.Items[0] 46 | } else { 47 | error = errors.New("User not found") 48 | } 49 | 50 | return 51 | 52 | } 53 | 54 | // Moderators returns those users on a site who can exercise moderation powers. 55 | func (session Session) Moderators(params map[string]string) (output *Users, error error) { 56 | output = new(Users) 57 | error = session.get("users/moderators", params, output) 58 | return 59 | } 60 | 61 | // ElectedModerators returns those users on a site who both have moderator powers, and were actually elected. 62 | func (session Session) ElectedModerators(params map[string]string) (output *Users, error error) { 63 | output = new(Users) 64 | error = session.get("users/moderators/elected", params, output) 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /stackongo/users_test.go: -------------------------------------------------------------------------------- 1 | package stackongo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllUsers(t *testing.T) { 8 | dummy_server := returnDummyResponseForPath("/2.0/users", dummyUsersResponse, t) 9 | defer closeDummyServer(dummy_server) 10 | 11 | session := NewSession("stackoverflow") 12 | users, err := session.AllUsers(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 13 | 14 | if err != nil { 15 | t.Error(err.Error()) 16 | } 17 | 18 | if len(users.Items) != 1 { 19 | t.Error("Number of items wrong.") 20 | } 21 | 22 | if users.Items[0].User_id != 22656 { 23 | t.Error("ID invalid.") 24 | } 25 | 26 | if users.Items[0].User_type != "registered" { 27 | t.Error("User type invalid.") 28 | } 29 | 30 | if users.Items[0].Creation_date != 1222430705 { 31 | t.Error("Date invalid.") 32 | } 33 | 34 | if users.Items[0].Is_employee != false { 35 | t.Error("Boolean doesn't match.") 36 | } 37 | 38 | if users.Items[0].Badge_counts.Gold != 105 { 39 | t.Error("Badge count is invalid.") 40 | } 41 | 42 | } 43 | 44 | func TestGetUsers(t *testing.T) { 45 | dummy_server := returnDummyResponseForPath("/2.0/users/1;2;3", dummyUsersResponse, t) 46 | defer closeDummyServer(dummy_server) 47 | 48 | session := NewSession("stackoverflow") 49 | _, err := session.GetUsers([]int{1, 2, 3}, map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 50 | 51 | if err != nil { 52 | t.Error(err.Error()) 53 | } 54 | 55 | } 56 | 57 | func TestAuthenticatedUser(t *testing.T) { 58 | dummy_server := returnDummyResponseForPathAndParams("/2.0/me", map[string]string{"key": "app123", "access_token": "abc"}, dummyUsersResponse, t) 59 | defer closeDummyServer(dummy_server) 60 | 61 | session := NewSession("stackoverflow") 62 | user, err := session.AuthenticatedUser(map[string]string{}, map[string]string{"key": "app123", "access_token": "abc"}) 63 | 64 | if err != nil { 65 | t.Error(err.Error()) 66 | } 67 | 68 | if user.User_id != 22656 { 69 | t.Error("ID invalid.") 70 | } 71 | 72 | if user.User_type != "registered" { 73 | t.Error("User type invalid.") 74 | } 75 | 76 | if user.Creation_date != 1222430705 { 77 | t.Error("Date invalid.") 78 | } 79 | 80 | if user.Is_employee != false { 81 | t.Error("Boolean doesn't match.") 82 | } 83 | 84 | if user.Badge_counts.Gold != 105 { 85 | t.Error("Badge count is invalid.") 86 | } 87 | 88 | } 89 | 90 | func TestNoAuthenticatedUser(t *testing.T) { 91 | dummy_server := returnDummyResponseForPathAndParams("/2.0/me", map[string]string{"key": "app123", "access_token": "abc"}, dummyMetaInfoResponse, t) 92 | defer closeDummyServer(dummy_server) 93 | 94 | session := NewSession("stackoverflow") 95 | _, err := session.AuthenticatedUser(map[string]string{}, map[string]string{"key": "app123", "access_token": "abc"}) 96 | 97 | if err.Error() != "User not found" { 98 | t.Error("Error didn't match") 99 | } 100 | } 101 | 102 | func TestModerators(t *testing.T) { 103 | dummy_server := returnDummyResponseForPath("/2.0/users/moderators", dummyUsersResponse, t) 104 | defer closeDummyServer(dummy_server) 105 | 106 | session := NewSession("stackoverflow") 107 | _, err := session.Moderators(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 108 | 109 | if err != nil { 110 | t.Error(err.Error()) 111 | } 112 | 113 | } 114 | 115 | func TestElectedModerators(t *testing.T) { 116 | dummy_server := returnDummyResponseForPath("/2.0/users/moderators/elected", dummyUsersResponse, t) 117 | defer closeDummyServer(dummy_server) 118 | 119 | session := NewSession("stackoverflow") 120 | _, err := session.ElectedModerators(map[string]string{"sort": "votes", "order": "desc", "page": "1"}) 121 | 122 | if err != nil { 123 | t.Error(err.Error()) 124 | } 125 | 126 | } 127 | 128 | //Test Data 129 | 130 | var dummyUsersResponse string = ` 131 | { 132 | "items": [ 133 | { 134 | "user_id": 22656, 135 | "user_type": "registered", 136 | "creation_date": 1222430705, 137 | "display_name": "Jon Skeet", 138 | "profile_image": "http://www.gravatar.com/avatar/6d8ebb117e8d83d74ea95fbdd0f87e13?d=identicon&r=PG", 139 | "reputation": 397366, 140 | "reputation_change_day": 30, 141 | "reputation_change_week": 1135, 142 | "reputation_change_month": 30, 143 | "reputation_change_quarter": 10890, 144 | "reputation_change_year": 10890, 145 | "age": 35, 146 | "last_access_date": 1328051866, 147 | "last_modified_date": 1328017043, 148 | "is_employee": false, 149 | "link": "http://stackoverflow.com/users/22656/jon-skeet", 150 | "website_url": "http://csharpindepth.com", 151 | "location": "Reading, United Kingdom", 152 | "account_id": 11683, 153 | "badge_counts": { 154 | "gold": 105, 155 | "silver": 1672, 156 | "bronze": 2946 157 | }, 158 | "accept_rate": 95 159 | } 160 | ] 161 | } 162 | ` 163 | --------------------------------------------------------------------------------