├── .gitignore ├── LICENSE ├── README.md ├── bitbucket ├── bitbucket.go ├── bitbucket_test.go ├── brokers.go ├── brokers_test.go ├── contents.go ├── contents_test.go ├── emails.go ├── emails_test.go ├── http.go ├── keys.go ├── keys_test.go ├── repo_keys.go ├── repo_keys_test.go ├── repos.go ├── repos_test.go ├── teams.go ├── teams_test.go ├── users.go └── users_test.go └── oauth1 ├── LICENSE ├── consumer.go ├── token.go └── token_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | *~ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 drone.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-bitbucket 2 | ============ 3 | 4 | Go bindings for the Bitbucket API. Documentation coming soon! 5 | -------------------------------------------------------------------------------- /bitbucket/bitbucket.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrNilClient = errors.New("client is nil") 9 | ) 10 | 11 | // New creates an instance of the Bitbucket Client 12 | func New(consumerKey, consumerSecret, accessToken, tokenSecret string) *Client { 13 | c := &Client{} 14 | c.ConsumerKey = consumerKey 15 | c.ConsumerSecret = consumerSecret 16 | c.AccessToken = accessToken 17 | c.TokenSecret = tokenSecret 18 | 19 | c.Keys = &KeyResource{c} 20 | c.Repos = &RepoResource{c} 21 | c.Users = &UserResource{c} 22 | c.Emails = &EmailResource{c} 23 | c.Brokers = &BrokerResource{c} 24 | c.Teams = &TeamResource{c} 25 | c.RepoKeys = &RepoKeyResource{c} 26 | c.Sources = &SourceResource{c} 27 | return c 28 | } 29 | 30 | type Client struct { 31 | ConsumerKey string 32 | ConsumerSecret string 33 | AccessToken string 34 | TokenSecret string 35 | 36 | Repos *RepoResource 37 | Users *UserResource 38 | Emails *EmailResource 39 | Keys *KeyResource 40 | Brokers *BrokerResource 41 | Teams *TeamResource 42 | Sources *SourceResource 43 | RepoKeys *RepoKeyResource 44 | } 45 | 46 | // Guest Client that can be used to access 47 | // public APIs that do not require authentication. 48 | var Guest = New("", "", "", "") 49 | -------------------------------------------------------------------------------- /bitbucket/bitbucket_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | // Instance of the Bitbucket client that we'll use for our unit tests 9 | var client *Client 10 | 11 | var ( 12 | // Dummy user that we'll use to run integration tests 13 | testUser string 14 | 15 | // Dummy repo that we'll use to run integration tests 16 | testRepo string 17 | ) 18 | 19 | var ( 20 | // OAuth Consumer Key registered with Bitbucket 21 | consumerKey string 22 | 23 | // OAuth Consumer Secret registered with Bitbucket 24 | consumerSecret string 25 | 26 | // A valid access token issues for the `testUser` and `consumerKey` 27 | accessToken string 28 | tokenSecret string 29 | ) 30 | 31 | func init() { 32 | consumerKey = os.Getenv("BB_CONSUMER_KEY") 33 | consumerSecret = os.Getenv("BB_CONSUMER_SECRET") 34 | accessToken = os.Getenv("BB_ACCESS_TOKEN") 35 | tokenSecret = os.Getenv("BB_TOKEN_SECRET") 36 | testUser = os.Getenv("BB_USER") 37 | testRepo = os.Getenv("BB_REPO") 38 | 39 | switch { 40 | case len(consumerKey) == 0: 41 | panic(errors.New("must set the BB_CONSUMER_KEY environment variable")) 42 | case len(consumerSecret) == 0: 43 | panic(errors.New("must set the BB_CONSUMER_SECRET environment variable")) 44 | case len(accessToken) == 0: 45 | panic(errors.New("must set the BB_ACCESS_TOKEN environment variable")) 46 | case len(tokenSecret) == 0: 47 | panic(errors.New("must set the BB_TOKEN_SECRET environment variable")) 48 | case len(testUser) == 0: 49 | panic(errors.New("must set the BB_USER environment variable")) 50 | case len(testRepo) == 0: 51 | panic(errors.New("must set the BB_REPO environment variable")) 52 | } 53 | 54 | client = New(consumerKey, consumerSecret, accessToken, tokenSecret) 55 | } 56 | -------------------------------------------------------------------------------- /bitbucket/brokers.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | // Bitbucket POSTs to the service URL you specify. The service 11 | // receives an POST whenever user pushes to the repository. 12 | const BrokerTypePost = "POST" 13 | 14 | const BrokerTypePullRequestPost = "Pull Request POST" 15 | 16 | type Broker struct { 17 | // A Bitbucket assigned integer representing a unique 18 | // identifier for the service. 19 | Id int `json:"id"` 20 | 21 | // A profile describing the service. 22 | Profile *BrokerProfile `json:"service"` 23 | } 24 | 25 | type BrokerProfile struct { 26 | // One of the supported services. The type is a 27 | // case-insensitive value. 28 | Type string `json:"type"` 29 | 30 | // A parameter array containing a name and value pair 31 | // for each parameter associated with the service. 32 | Fields []*BrokerField `json:"fields"` 33 | } 34 | 35 | type BrokerField struct { 36 | Name string `json:"name"` 37 | Value string `json:"value"` 38 | } 39 | 40 | // Bitbucket offers integration with external services by allowing a set of 41 | // services, or brokers, to run at certain events. 42 | // 43 | // The Bitbucket services resource provides functionality for adding, 44 | // removing, and configuring brokers on your repositories. All the methods 45 | // on this resource require the caller to authenticate. 46 | // 47 | // https://confluence.atlassian.com/display/BITBUCKET/services+Resource 48 | type BrokerResource struct { 49 | client *Client 50 | } 51 | 52 | func (r *BrokerResource) List(owner, slug string) ([]*Broker, error) { 53 | brokers := []*Broker{} 54 | path := fmt.Sprintf("/repositories/%s/%s/services", owner, slug) 55 | 56 | if err := r.client.do("GET", path, nil, nil, &brokers); err != nil { 57 | return nil, err 58 | } 59 | 60 | return brokers, nil 61 | } 62 | 63 | func (r *BrokerResource) Find(owner, slug string, id int) (*Broker, error) { 64 | brokers := []*Broker{} 65 | path := fmt.Sprintf("/repositories/%s/%s/services/%v", owner, slug, id) 66 | 67 | if err := r.client.do("GET", path, nil, nil, &brokers); err != nil { 68 | return nil, err 69 | } 70 | 71 | if len(brokers) == 0 { 72 | return nil, ErrNotFound 73 | } 74 | 75 | return brokers[0], nil 76 | } 77 | 78 | func (r *BrokerResource) FindUrl(owner, slug, link, brokerType string) (*Broker, error) { 79 | brokers, err := r.List(owner, slug) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | //fmt.Println("Total borkers:", len(brokers)) 85 | 86 | // iterate though list of brokers 87 | for _, broker := range brokers { 88 | if broker.Profile != nil && broker.Profile.Fields != nil { 89 | //fmt.Printf("Compare: %v == %v is %v\n", broker.Profile.Type, brokerType, broker.Profile.Type == brokerType) 90 | if broker.Profile.Type != brokerType { 91 | continue 92 | } 93 | // iterate through list of fields 94 | for _, field := range broker.Profile.Fields { 95 | //fmt.Printf("-->%v==%v\n", field.Name, field.Value) 96 | if field.Name == "URL" && field.Value == link { 97 | // fmt.Println("Found match") 98 | return broker, nil 99 | } 100 | } 101 | //fmt.Println("Skipping...") 102 | } 103 | } 104 | 105 | //fmt.Println("Not match found") 106 | return nil, ErrNotFound 107 | } 108 | 109 | func (r *BrokerResource) Create(owner, slug, link, brokerType string) (*Broker, error) { 110 | values := url.Values{} 111 | values.Add("type", brokerType) 112 | values.Add("URL", link) 113 | 114 | if brokerType == BrokerTypePullRequestPost { 115 | values.Add("comments", "off") 116 | // TODO: figure out how to only set the hook for pull request create and update 117 | // This is the sample string from the docs...it does not work becuase of 118 | // the stupid slashes 119 | // Check out the docs: https://confluence.atlassian.com/display/BITBUCKET/Pull+Request+POST+hook+management 120 | // Sample string: 121 | // -data "type=POST&URL=https://www.test.comcreate%2Fedit%2Fmerge%2Fdecline=on&comments=on&approve%2Funapprove=on" 122 | } 123 | 124 | b := Broker{} 125 | path := fmt.Sprintf("/repositories/%s/%s/services", owner, slug) 126 | if err := r.client.do("POST", path, nil, values, &b); err != nil { 127 | return nil, err 128 | } 129 | 130 | return &b, nil 131 | } 132 | 133 | func (r *BrokerResource) Update(owner, slug, link, brokerType string, id int) (*Broker, error) { 134 | values := url.Values{} 135 | values.Add("type", brokerType) 136 | values.Add("URL", link) 137 | 138 | if brokerType == BrokerTypePullRequestPost { 139 | values.Add("comments", "off") 140 | // TODO: figure out how to also shutoff other events! 141 | } 142 | 143 | b := Broker{} 144 | path := fmt.Sprintf("/repositories/%s/%s/services/%v", owner, slug, id) 145 | if err := r.client.do("PUT", path, nil, values, &b); err != nil { 146 | return nil, err 147 | } 148 | 149 | return &b, nil 150 | } 151 | 152 | // CreateUpdate will attempt to Create a Broker (Server Hook) if 153 | // it doesn't already exist in the Bitbucket. 154 | func (r *BrokerResource) CreateUpdate(owner, slug, link, brokerType string) (*Broker, error) { 155 | if found, err := r.FindUrl(owner, slug, link, brokerType); err == nil { 156 | // if the Broker already exists, just return it 157 | // ... not need to re-create 158 | //fmt.Println("Broker already found, skipping!", brokerType) 159 | return found, nil 160 | } 161 | 162 | return r.Create(owner, slug, link, brokerType) 163 | } 164 | 165 | func (r *BrokerResource) Delete(owner, slug string, id int) error { 166 | path := fmt.Sprintf("/repositories/%s/%s/services/%v", owner, slug, id) 167 | return r.client.do("DELETE", path, nil, nil, nil) 168 | } 169 | 170 | func (r *BrokerResource) DeleteUrl(owner, slug, url, brokerType string) error { 171 | broker, err := r.FindUrl(owner, slug, url, brokerType) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | return r.Delete(owner, slug, broker.Id) 177 | } 178 | 179 | // patch := bitbucket.GetPatch(repo, p.Id, u.BitbucketToken, u.BitbucketSecret) 180 | func (r *BrokerResource) GetPatch(owner, slug string, id int) (string, error) { 181 | data := []byte{} 182 | // uri, err := url.Parse("https://api.bitbucket.org/1.0" + path) 183 | // https://bitbucket.org/!api/2.0/repositories/tdburke/test_mymysql/pullrequests/1/patch 184 | 185 | path := fmt.Sprintf("/repositories/tdburke/test_mymysql/pullrequests/1/patch") 186 | 187 | fmt.Println(path) 188 | 189 | if err := r.client.do("GET", path, nil, nil, &data); err != nil { 190 | fmt.Println("Get error:", err) 191 | return "", err 192 | } 193 | 194 | fmt.Println(data) 195 | 196 | if len(data) == 0 { 197 | return "", ErrNotFound 198 | } 199 | 200 | return "", nil 201 | } 202 | 203 | // ----------------------------------------------------------------------------- 204 | // Post Receive Hook Functions 205 | 206 | // ----------------------------------------------------------------------------- 207 | 208 | var ErrInvalidPostReceiveHook = errors.New("Invalid Post Receive Hook") 209 | 210 | type PullRequestHook struct { 211 | Id string `json:"id"` 212 | } 213 | 214 | type PostReceiveHook struct { 215 | Repo *Repo `json:"repository"` 216 | User string `json:"user"` 217 | Url string `json:"canon_url"` 218 | Commits []*Commit `json:"commits"` 219 | } 220 | 221 | type Commit struct { 222 | Message string `json:"message"` 223 | Author string `json:"author"` 224 | RawAuthor string `json:"raw_author"` 225 | Branch string `json:"branch"` 226 | Hash string `json:"raw_node"` 227 | Files []*File `json:"files"` 228 | } 229 | 230 | type File struct { 231 | Name string `json:"file"` 232 | Type string `json:"type"` 233 | } 234 | 235 | func ParseHook(raw []byte) (*PostReceiveHook, error) { 236 | hook := PostReceiveHook{} 237 | if err := json.Unmarshal(raw, &hook); err != nil { 238 | return nil, err 239 | } 240 | 241 | // it is possible the JSON was parsed, however, 242 | // was not from Bitbucket (maybe was from Google Code) 243 | // So we'll check to be sure certain key fields 244 | // were populated 245 | switch { 246 | case hook.Repo == nil: 247 | return nil, ErrInvalidPostReceiveHook 248 | case hook.Commits == nil: 249 | return nil, ErrInvalidPostReceiveHook 250 | case len(hook.User) == 0: 251 | return nil, ErrInvalidPostReceiveHook 252 | case len(hook.Commits) == 0: 253 | return nil, ErrInvalidPostReceiveHook 254 | } 255 | 256 | return &hook, nil 257 | } 258 | 259 | func ParsePullRequestHook(raw []byte) (*PullRequestHook, error) { 260 | data := make(map[string]map[string]interface{}) 261 | if err := json.Unmarshal(raw, &data); err != nil { 262 | return nil, err 263 | } 264 | 265 | hook := PullRequestHook{} 266 | 267 | if p, ok := data["pullrequest_created"]; ok { 268 | fmt.Println(p["id"]) 269 | hook.Id = fmt.Sprintf("%v", p["id"]) 270 | if hook.Id == "" { 271 | return nil, errors.New("Could not parse bitbucket pullrequest_created message") 272 | } 273 | 274 | /* 275 | brokers := []*Broker{} 276 | path := fmt.Sprintf("/repositories/%s/%s/services/%v", owner, slug, id) 277 | 278 | if err := r.client.do("GET", path, nil, nil, &brokers); err != nil { 279 | return nil, err 280 | } 281 | 282 | */ 283 | 284 | return &hook, nil 285 | 286 | } 287 | 288 | // 289 | 290 | // How do we get the diff file? 291 | return nil, errors.New("Could not parse bitbucket pull request hook") 292 | } 293 | 294 | var ips = map[string]bool{ 295 | "63.246.22.222": true, 296 | } 297 | 298 | // Check's to see if the Post-Receive Build Hook is coming 299 | // from a valid sender (IP Address) 300 | func IsValidSender(ip string) bool { 301 | return ips[ip] 302 | } 303 | -------------------------------------------------------------------------------- /bitbucket/brokers_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | /* 8 | func Test_Brokers(t *testing.T) { 9 | 10 | // CREATE a broker 11 | s, err := client.Brokers.Create(testUser, testRepo, "https://bitbucket.org/post") 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | // DELETE (deferred) 17 | defer client.Brokers.Delete(testUser, testRepo, s.Id) 18 | 19 | // FIND the broker by id 20 | broker, err := client.Brokers.Find(testUser, testRepo, s.Id) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | 25 | // verify we get back the correct data 26 | if broker.Id != s.Id { 27 | t.Errorf("broker id [%v]; want [%v]", broker.Id, s.Id) 28 | } 29 | if broker.Profile.Type != s.Profile.Type { 30 | t.Errorf("broker type [%v]; want [%v]", broker.Profile.Type, s.Profile.Type) 31 | } 32 | if broker.Profile.Fields[0].Value != "https://bitbucket.org/post" { 33 | t.Errorf("broker url [%v]; want [%v]", broker.Profile.Fields[0].Value, "https://bitbucket.org/post") 34 | } 35 | 36 | // UPDATE the broker 37 | _, err = client.Brokers.Update(testUser, testRepo, "https://bitbucket.org/post2", s.Id) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | // LIST the brokers 43 | brokers, err := client.Brokers.List(testUser, testRepo) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | if len(brokers) == 0 { 49 | t.Errorf("List of brokers returned empty set") 50 | } 51 | } 52 | */ 53 | func Test_PostReceiveHooks(t *testing.T) { 54 | hook, err := ParseHook([]byte(sampleHook)) 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | 60 | if hook.Commits[0].Branch != "master" { 61 | t.Errorf("expected branch [%v]; got [%v]", "master", hook.Commits[0].Branch) 62 | } 63 | 64 | if hook.Repo.Owner != "marcus" { 65 | t.Errorf("expected branch [%v]; got [%v]", "marcus", hook.Repo.Owner) 66 | } 67 | 68 | if hook.Repo.Slug != "project-x" { 69 | t.Errorf("expected slug [%v]; got [%v]", "project-x", hook.Repo.Slug) 70 | } 71 | 72 | // What happens if we get a hook from, say, Google Code? 73 | hook, err = ParseHook([]byte(invalidHook)) 74 | if err == nil { 75 | t.Errorf("Expected error parsing Google Code hook") 76 | return 77 | } 78 | } 79 | 80 | func Test_IsValidSender(t *testing.T) { 81 | str := map[string]bool{ 82 | "63.246.22.222": true, 83 | "127.0.0.1": false, 84 | "localhost": false, 85 | "1.2.3.4": false, 86 | } 87 | 88 | for k, v := range str { 89 | if IsValidSender(k) != v { 90 | t.Errorf("expected IP address [%v] validation [%v]", k, v) 91 | } 92 | } 93 | } 94 | 95 | var sampleHook = ` 96 | { 97 | "canon_url": "https://bitbucket.org", 98 | "commits": [ 99 | { 100 | "author": "marcus", 101 | "branch": "master", 102 | "files": [ 103 | { 104 | "file": "somefile.py", 105 | "type": "modified" 106 | } 107 | ], 108 | "message": "Added some more things to somefile.py\n", 109 | "node": "620ade18607a", 110 | "parents": [ 111 | "702c70160afc" 112 | ], 113 | "raw_author": "Marcus Bertrand ", 114 | "raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9", 115 | "revision": null, 116 | "size": -1, 117 | "timestamp": "2012-05-30 05:58:56", 118 | "utctimestamp": "2012-05-30 03:58:56+00:00" 119 | } 120 | ], 121 | "repository": { 122 | "absolute_url": "/marcus/project-x/", 123 | "fork": false, 124 | "is_private": true, 125 | "name": "Project X", 126 | "owner": "marcus", 127 | "scm": "git", 128 | "slug": "project-x", 129 | "website": "https://atlassian.com/" 130 | }, 131 | "user": "marcus" 132 | } 133 | ` 134 | 135 | var invalidHook = ` 136 | { 137 | "repository_path": "https:\/\/code.google.com\/p\/rydzewski-hg\/", 138 | "project_name": "rydzewski-hg", 139 | "revisions": [ 140 | { 141 | "added": [ 142 | "\/README" 143 | ], 144 | "parents": [ 145 | 146 | ], 147 | "author": "John Doe ", 148 | "url": "http:\/\/rydzewski-hg.googlecode.com\/hg-history\/be12639f52f33b0861e647d3a795f863061395bf\/", 149 | "timestamp": 1345764974, 150 | "message": "testing ...", 151 | "path_count": 1, 152 | "removed": [ 153 | 154 | ], 155 | "modified": [ 156 | 157 | ], 158 | "revision": "be12639f52f33b0861e647d3a795f863061395bf" 159 | } 160 | ], 161 | "revision_count": 1 162 | } 163 | ` 164 | -------------------------------------------------------------------------------- /bitbucket/contents.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Source struct { 8 | Node string `json:"node"` 9 | Path string `json:"path"` 10 | Data string `json:"data"` 11 | Size int64 `json:"size"` 12 | } 13 | 14 | // Use the Bitbucket src resource to browse directories and view files. 15 | // This is a read-only resource. 16 | // 17 | // https://confluence.atlassian.com/display/BITBUCKET/src+Resources 18 | type SourceResource struct { 19 | client *Client 20 | } 21 | 22 | // Gets information about an individual file in a repository 23 | func (r *SourceResource) Find(owner, slug, revision, path string) (*Source, error) { 24 | src := Source{} 25 | url_path := fmt.Sprintf("/repositories/%s/%s/src/%s/%s", owner, slug, revision, path) 26 | 27 | if err := r.client.do("GET", url_path, nil, nil, &src); err != nil { 28 | return nil, err 29 | } 30 | 31 | return &src, nil 32 | } 33 | 34 | // Gets a list of the src in a repository. 35 | func (r *SourceResource) List(owner, slug, revision, path string) ([]*Source, error) { 36 | src := []*Source{} 37 | url_path := fmt.Sprintf("/repositories/%s/%s/src/%s/%s", owner, slug, revision, path) 38 | if err := r.client.do("GET", url_path, nil, nil, &src); err != nil { 39 | return nil, err 40 | } 41 | 42 | return src, nil 43 | } 44 | -------------------------------------------------------------------------------- /bitbucket/contents_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_Contents(t *testing.T) { 9 | 10 | const testFile = "readme.rst" 11 | const testRev = "8f0fe25998516f460ce2a2a867b7298b3628dd23" 12 | 13 | // GET the latest revision for the repo 14 | 15 | // GET the README file for the repo & revision 16 | src, err := client.Sources.Find("atlassian", "jetbrains-bitbucket-connector", testRev, testFile) 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | 22 | fmt.Println(src) 23 | } 24 | -------------------------------------------------------------------------------- /bitbucket/emails.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // An account can have one or more email addresses associated with it. 9 | // Use this end point to list, change, or create an email address. 10 | // 11 | // https://confluence.atlassian.com/display/BITBUCKET/emails+Resource 12 | type EmailResource struct { 13 | client *Client 14 | } 15 | 16 | type Email struct { 17 | // Indicates the user confirmed the email address (true). 18 | Active bool `json:"active"` 19 | 20 | // The email address. 21 | Email string `json:"email"` 22 | 23 | // Indicates the email is the main contact email address for the account. 24 | Primary bool `json:"primary"` 25 | } 26 | 27 | // Gets the email addresses associated with the account. This call requires 28 | // authentication. 29 | func (r *EmailResource) List(account string) ([]*Email, error) { 30 | emails := []*Email{} 31 | path := fmt.Sprintf("/users/%s/emails", account) 32 | 33 | if err := r.client.do("GET", path, nil, nil, &emails); err != nil { 34 | return nil, err 35 | } 36 | 37 | return emails, nil 38 | } 39 | 40 | // Gets an individual email address associated with an account. 41 | // This call requires authentication. 42 | func (r *EmailResource) Find(account, address string) (*Email, error) { 43 | email := Email{} 44 | path := fmt.Sprintf("/users/%s/emails/%s", account, address) 45 | 46 | if err := r.client.do("GET", path, nil, nil, &email); err != nil { 47 | return nil, err 48 | } 49 | 50 | return &email, nil 51 | } 52 | 53 | // Gets an individual's primary email address. 54 | func (r *EmailResource) FindPrimary(account string) (*Email, error) { 55 | emails, err := r.List(account) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | for _, email := range emails { 61 | if email.Primary { 62 | return email, nil 63 | } 64 | } 65 | 66 | return nil, ErrNotFound 67 | } 68 | 69 | // Adds additional email addresses to an account. This call requires 70 | // authentication. 71 | func (r *EmailResource) Create(account, address string) (*Email, error) { 72 | 73 | values := url.Values{} 74 | values.Add("email", address) 75 | 76 | e := Email{} 77 | path := fmt.Sprintf("/users/%s/emails/%s", account, address) 78 | if err := r.client.do("POST", path, nil, values, &e); err != nil { 79 | return nil, err 80 | } 81 | 82 | return &e, nil 83 | } 84 | -------------------------------------------------------------------------------- /bitbucket/emails_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Emails(t *testing.T) { 8 | 9 | const dummyEmail = "dummy@localhost.com" 10 | 11 | // CREATE an email entry 12 | if _, err := client.Emails.Find(testUser, dummyEmail); err != nil { 13 | _, cerr := client.Emails.Create(testUser, dummyEmail) 14 | if cerr != nil { 15 | t.Error(cerr) 16 | return 17 | } 18 | } 19 | 20 | // FIND the email 21 | _, err := client.Emails.Find(testUser, dummyEmail) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | // LIST the email addresses 27 | emails, err := client.Emails.List(testUser) 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | 32 | if len(emails) == 0 { 33 | t.Errorf("List of emails returned empty set") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bitbucket/http.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/drone/go-bitbucket/oauth1" 12 | ) 13 | 14 | var ( 15 | // Returned if the specified resource does not exist. 16 | ErrNotFound = errors.New("Not Found") 17 | 18 | // Returned if the caller attempts to make a call or modify a resource 19 | // for which the caller is not authorized. 20 | // 21 | // The request was a valid request, the caller's authentication credentials 22 | // succeeded but those credentials do not grant the caller permission to 23 | // access the resource. 24 | ErrForbidden = errors.New("Forbidden") 25 | 26 | // Returned if the call requires authentication and either the credentials 27 | // provided failed or no credentials were provided. 28 | ErrNotAuthorized = errors.New("Unauthorized") 29 | 30 | // Returned if the caller submits a badly formed request. For example, 31 | // the caller can receive this return if you forget a required parameter. 32 | ErrBadRequest = errors.New("Bad Request") 33 | ) 34 | 35 | // DefaultClient uses DefaultTransport, and is used internall to execute 36 | // all http.Requests. This may be overriden for unit testing purposes. 37 | // 38 | // IMPORTANT: this is not thread safe and should not be touched with 39 | // the exception overriding for mock unit testing. 40 | var DefaultClient = http.DefaultClient 41 | 42 | func (c *Client) do(method string, path string, params url.Values, values url.Values, v interface{}) error { 43 | 44 | // if this is the guest client then we don't need 45 | // to sign the request ... we will execute just 46 | // a simple http request. 47 | if c == Guest { 48 | return c.guest(method, path, params, values, v) 49 | } 50 | 51 | // create the client 52 | var client = oauth1.Consumer{ 53 | ConsumerKey: c.ConsumerKey, 54 | ConsumerSecret: c.ConsumerSecret, 55 | } 56 | 57 | // create the URI 58 | uri, err := url.Parse("https://api.bitbucket.org/1.0" + path) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if params != nil && len(params) > 0 { 64 | uri.RawQuery = params.Encode() 65 | } 66 | 67 | // create the access token 68 | token := oauth1.NewAccessToken(c.AccessToken, c.TokenSecret, nil) 69 | 70 | // create the request 71 | req := &http.Request{ 72 | URL: uri, 73 | Method: method, 74 | ProtoMajor: 1, 75 | ProtoMinor: 1, 76 | Close: true, 77 | } 78 | 79 | if values != nil && len(values) > 0 { 80 | body := []byte(values.Encode()) 81 | buf := bytes.NewBuffer(body) 82 | req.Body = ioutil.NopCloser(buf) 83 | } 84 | 85 | // add the Form data to the request 86 | // (we'll need this in order to sign the request) 87 | req.Form = values 88 | 89 | // sign the request 90 | if err := client.Sign(req, token); err != nil { 91 | return err 92 | } 93 | 94 | // make the request using the default http client 95 | resp, err := DefaultClient.Do(req) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | // Read the bytes from the body (make sure we defer close the body) 101 | defer resp.Body.Close() 102 | body, err := ioutil.ReadAll(resp.Body) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // Check for an http error status (ie not 200 StatusOK) 108 | switch resp.StatusCode { 109 | case 404: 110 | return ErrNotFound 111 | case 403: 112 | return ErrForbidden 113 | case 401: 114 | return ErrNotAuthorized 115 | case 400: 116 | return ErrBadRequest 117 | } 118 | 119 | // Unmarshall the JSON response 120 | if v != nil { 121 | return json.Unmarshal(body, v) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (c *Client) guest(method string, path string, params url.Values, values url.Values, v interface{}) error { 128 | 129 | // create the URI 130 | uri, err := url.Parse("https://api.bitbucket.org/1.0" + path) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | if params != nil && len(params) > 0 { 136 | uri.RawQuery = params.Encode() 137 | } 138 | 139 | // create the request 140 | req := &http.Request{ 141 | URL: uri, 142 | Method: method, 143 | ProtoMajor: 1, 144 | ProtoMinor: 1, 145 | Close: true, 146 | } 147 | 148 | // add the Form values to the body 149 | if values != nil && len(values) > 0 { 150 | body := []byte(values.Encode()) 151 | buf := bytes.NewBuffer(body) 152 | req.Body = ioutil.NopCloser(buf) 153 | } 154 | 155 | // make the request using the default http client 156 | resp, err := DefaultClient.Do(req) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // Read the bytes from the body (make sure we defer close the body) 162 | defer resp.Body.Close() 163 | body, err := ioutil.ReadAll(resp.Body) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | // Check for an http error status (ie not 200 StatusOK) 169 | switch resp.StatusCode { 170 | case 404: 171 | return ErrNotFound 172 | case 403: 173 | return ErrForbidden 174 | case 401: 175 | return ErrNotAuthorized 176 | case 400: 177 | return ErrBadRequest 178 | } 179 | 180 | // Unmarshall the JSON response 181 | if v != nil { 182 | return json.Unmarshal(body, v) 183 | } 184 | 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /bitbucket/keys.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | type Key struct { 9 | Id int `json:"pk"` // The key identifier (ID). 10 | Key string `json:"key"` // Public key value. 11 | Label string `json:"label"` // The user-visible label on the key 12 | } 13 | 14 | // Use the ssh-keys resource to manipulate the ssh-keys on an individual 15 | // or team account. 16 | // 17 | // https://confluence.atlassian.com/display/BITBUCKET/ssh-keys+Resource 18 | type KeyResource struct { 19 | client *Client 20 | } 21 | 22 | // Gets a list of the keys associated with an account. 23 | // This call requires authentication. 24 | func (r *KeyResource) List(account string) ([]*Key, error) { 25 | keys := []*Key{} 26 | path := fmt.Sprintf("/users/%s/ssh-keys", account) 27 | 28 | if err := r.client.do("GET", path, nil, nil, &keys); err != nil { 29 | return nil, err 30 | } 31 | 32 | return keys, nil 33 | } 34 | 35 | // Gets the content of the specified key_id. 36 | // This call requires authentication. 37 | func (r *KeyResource) Find(account string, id int) (*Key, error) { 38 | key := Key{} 39 | path := fmt.Sprintf("/users/%s/ssh-keys/%v", account, id) 40 | if err := r.client.do("GET", path, nil, nil, &key); err != nil { 41 | return nil, err 42 | } 43 | 44 | return &key, nil 45 | } 46 | 47 | // Gets the content of the specified key with the 48 | // given label. 49 | func (r *KeyResource) FindName(account, label string) (*Key, error) { 50 | keys, err := r.List(account) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | for _, key := range keys { 56 | if key.Label == label { 57 | return key, nil 58 | } 59 | } 60 | 61 | return nil, ErrNotFound 62 | } 63 | 64 | // Creates a key on the specified account. You must supply a valid key 65 | // that is unique across the Bitbucket service. 66 | func (r *KeyResource) Create(account, key, label string) (*Key, error) { 67 | 68 | values := url.Values{} 69 | values.Add("key", key) 70 | values.Add("label", label) 71 | 72 | k := Key{} 73 | path := fmt.Sprintf("/users/%s/ssh-keys", account) 74 | if err := r.client.do("POST", path, nil, values, &k); err != nil { 75 | return nil, err 76 | } 77 | 78 | return &k, nil 79 | } 80 | 81 | // Creates a key on the specified account. You must supply a valid key 82 | // that is unique across the Bitbucket service. 83 | func (r *KeyResource) Update(account, key, label string, id int) (*Key, error) { 84 | 85 | values := url.Values{} 86 | values.Add("key", key) 87 | values.Add("label", label) 88 | 89 | k := Key{} 90 | path := fmt.Sprintf("/users/%s/ssh-keys/%v", account, id) 91 | if err := r.client.do("PUT", path, nil, values, &k); err != nil { 92 | return nil, err 93 | } 94 | 95 | return &k, nil 96 | } 97 | 98 | func (r *KeyResource) CreateUpdate(account, key, label string) (*Key, error) { 99 | if found, err := r.FindName(account, label); err == nil { 100 | // if the public keys are different we should update 101 | if found.Key != key { 102 | return r.Update(account, key, label, found.Id) 103 | } 104 | 105 | // otherwise we should just return the key, since there 106 | // is nothing to update 107 | return found, nil 108 | } 109 | 110 | return r.Create(account, key, label) 111 | } 112 | 113 | // Deletes the key specified by the key_id value. 114 | // This call requires authentication 115 | func (r *KeyResource) Delete(account string, id int) error { 116 | path := fmt.Sprintf("/users/%s/ssh-keys/%v", account, id) 117 | return r.client.do("DELETE", path, nil, nil, nil) 118 | } 119 | -------------------------------------------------------------------------------- /bitbucket/keys_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Keys(t *testing.T) { 8 | 9 | // Test Public key that we'll add to the account 10 | public := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkRHDtJljvvZudiXxLt+JoHEQ4olLX6vZrVkm4gRVZEC7llKs9lXubHAwzIm+odIWZnoqNKjh0tSQYd5UAlSsrzn9YVvp0Lc2eJo0N1AWuyMzb9na+lfhT/YdM3Htkm14v7OZNdX4fqff/gCuLBIv9Bc9XH0jfEliOmfaDMQsbzcDi4usRoXBrJQQiu6M0A9FF0ruBdpKp0q08XSteGh5cMn1LvOS+vLrkHXi3bOXWvv7YXoVoI5OTUQGJjxmEehRssYiMfwD58cv7v2+PMLR3atGVCnoxxu/zMkXQlBKmEyN9VS7Cr8WOoZcNsCd9C6CCrbP5HZnjiE8F0R9d1zjP test@localhost" 11 | title := "test@localhost" 12 | 13 | // create a new public key 14 | key, err := client.Keys.Create(testUser, public, title) 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | 20 | // cleanup after ourselves & delete this dummy key 21 | defer client.Keys.Delete(testUser, key.Id) 22 | 23 | // Get the new key we recently created 24 | find, err := client.Keys.Find(testUser, key.Id) 25 | if title != find.Label { 26 | t.Errorf("key label [%v]; want [%v]", find.Label, title) 27 | } 28 | 29 | // Get a list of SSH keys for the user 30 | keys, err := client.Keys.List(testUser) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | if len(keys) == 0 { 36 | t.Errorf("List of keys returned empty set") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /bitbucket/repo_keys.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | 9 | // Use the ssh-keys resource to manipulate the ssh-keys associated 10 | // with a repository 11 | // 12 | // https://confluence.atlassian.com/display/BITBUCKET/deploy-keys+Resource 13 | type RepoKeyResource struct { 14 | client *Client 15 | } 16 | 17 | // Gets a list of the keys associated with a repository. 18 | func (r *RepoKeyResource) List(owner, slug string) ([]*Key, error) { 19 | keys := []*Key{} 20 | path := fmt.Sprintf("/repositories/%s/%s/deploy-keys", owner, slug) 21 | 22 | if err := r.client.do("GET", path, nil, nil, &keys); err != nil { 23 | return nil, err 24 | } 25 | 26 | return keys, nil 27 | } 28 | 29 | // Gets the content of the specified key_id. 30 | // This call requires authentication. 31 | func (r *RepoKeyResource) Find(owner, slug string, id int) (*Key, error) { 32 | key := Key{} 33 | path := fmt.Sprintf("/repositories/%s/%s/deploy-keys/%v", owner, slug, id) 34 | if err := r.client.do("GET", path, nil, nil, &key); err != nil { 35 | return nil, err 36 | } 37 | 38 | return &key, nil 39 | } 40 | 41 | // Gets the content of the specified key with the 42 | // given label. 43 | func (r *RepoKeyResource) FindName(owner, slug, label string) (*Key, error) { 44 | keys, err := r.List(owner, slug) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | for _, key := range keys { 50 | if key.Label == label { 51 | return key, nil 52 | } 53 | } 54 | 55 | return nil, ErrNotFound 56 | } 57 | 58 | // Creates a key on the specified repo. You must supply a valid key 59 | // that is unique across the Bitbucket service. 60 | func (r *RepoKeyResource) Create(owner, slug, key, label string) (*Key, error) { 61 | 62 | values := url.Values{} 63 | values.Add("key", key) 64 | values.Add("label", label) 65 | 66 | k := Key{} 67 | path := fmt.Sprintf("/repositories/%s/%s/deploy-keys", owner, slug) 68 | if err := r.client.do("POST", path, nil, values, &k); err != nil { 69 | return nil, err 70 | } 71 | 72 | return &k, nil 73 | } 74 | 75 | // Creates a key on the specified account. You must supply a valid key 76 | // that is unique across the Bitbucket service. 77 | func (r *RepoKeyResource) Update(owner, slug, key, label string, id int) (*Key, error) { 78 | // There is actually no API to update an existing key 79 | r.Delete(owner, slug, id) 80 | return r.Create(owner, slug, key, label) 81 | } 82 | 83 | func (r *RepoKeyResource) CreateUpdate(owner, slug, key, label string) (*Key, error) { 84 | if found, err := r.FindName(owner, slug, label); err == nil { 85 | // if the public keys are different we should update 86 | if found.Key != key { 87 | return r.Update(owner, slug, key, label, found.Id) 88 | } 89 | 90 | // otherwise we should just return the key, since there 91 | // is nothing to update 92 | return found, nil 93 | } 94 | 95 | return r.Create(owner, slug, key, label) 96 | } 97 | 98 | // Deletes the key specified by the key_id value. 99 | // This call requires authentication 100 | func (r *RepoKeyResource) Delete(owner, slug string, id int) error { 101 | path := fmt.Sprintf("/repositories/%s/%s/deploy-keys/%v", owner, slug, id) 102 | return r.client.do("DELETE", path, nil, nil, nil) 103 | } 104 | 105 | // Deletes the named key. 106 | // This call requires authentication 107 | func (r *RepoKeyResource) DeleteName(owner, slug, label string) error { 108 | key, err := r.FindName(owner, slug, label) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | return r.Delete(owner, slug, key.Id) 114 | } 115 | -------------------------------------------------------------------------------- /bitbucket/repo_keys_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_RepoKeys(t *testing.T) { 8 | 9 | // Test Public key that we'll add to the account 10 | public := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkRHDtJljvvZudiXxLt+JoHEQ4olLX6vZrVkm4gRVZEC7llKs9lXubHAwzIm+odIWZnoqNKjh0tSQYd5UAlSsrzn9YVvp0Lc2eJo0N1AWuyMzb9na+lfhT/YdM3Htkm14v7OZNdX4fqff/gCuLBIv9Bc9XH0jfEliOmfaDMQsbzcDi4usRoXBrJQQiu6M0A9FF0ruBdpKp0q08XSteGh5cMn1LvOS+vLrkHXi3bOXWvv7YXoVoI5OTUQGJjxmEehRssYiMfwD58cv7v2+PMLR3atGVCnoxxu/zMkXQlBKmEyN9VS7Cr8WOoZcNsCd9C6CCrbP5HZnjiE8F0R9d1zjP test@localhost" 11 | title := "test@localhost" 12 | 13 | // create a new public key 14 | key, err := client.RepoKeys.Create(testUser, testRepo, public, title) 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | 20 | // cleanup after ourselves & delete this dummy key 21 | defer client.RepoKeys.Delete(testUser, testRepo, key.Id) 22 | 23 | // Get the new key we recently created 24 | find, err := client.RepoKeys.Find(testUser, testRepo, key.Id) 25 | if title != find.Label { 26 | t.Errorf("key label [%v]; want [%v]", find.Label, title) 27 | } 28 | 29 | // Get a list of SSH keys for the user 30 | keys, err := client.RepoKeys.List(testUser, testRepo) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | if len(keys) == 0 { 36 | t.Errorf("List of keys returned empty set") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /bitbucket/repos.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Repo struct { 8 | Name string `json:"name"` 9 | Slug string `json:"slug"` 10 | Owner string `json:"owner"` 11 | Scm string `json:"scm"` 12 | Logo string `json:"logo"` 13 | Language string `json:"language"` 14 | Private bool `json:"is_private"` 15 | IsFork bool `json:"is_fork"` 16 | ForkOf *Repo `json:"fork_of"` 17 | } 18 | 19 | type Branch struct { 20 | Branch string `json:"branch"` 21 | Message string `json:"message"` 22 | Author string `json:"author"` 23 | RawAuthor string `json:"raw_author"` 24 | Node string `json:"node"` 25 | RawNode string `json:"raw_node"` 26 | Files []*BranchFile `json:"files"` 27 | } 28 | 29 | type BranchFile struct { 30 | File string `json:"file"` 31 | Type string `json:"type"` 32 | } 33 | 34 | type RepoResource struct { 35 | client *Client 36 | } 37 | 38 | // Gets the repositories owned by the individual or team account. 39 | func (r *RepoResource) List() ([]*Repo, error) { 40 | repos := []*Repo{} 41 | const path = "/user/repositories" 42 | 43 | if err := r.client.do("GET", path, nil, nil, &repos); err != nil { 44 | return nil, err 45 | } 46 | 47 | return repos, nil 48 | } 49 | 50 | // Gets the repositories list from the account's dashboard. 51 | func (r *RepoResource) ListDashboard() ([]*Account, error) { 52 | var m [][]interface{} 53 | const path = "/user/repositories/dashboard" 54 | 55 | if err := r.client.do("GET", path, nil, nil, &m); err != nil { 56 | return nil, err 57 | } 58 | 59 | return unmarshalAccounts(m), nil 60 | } 61 | 62 | // Gets the repositories list from the account's dashboard, and 63 | // converts the response to a list of Repos, instead of a 64 | // list of Accounts. 65 | func (r *RepoResource) ListDashboardRepos() ([]*Repo, error) { 66 | accounts, err := r.ListDashboard() 67 | if err != nil { 68 | return nil, nil 69 | } 70 | 71 | repos := []*Repo{} 72 | for _, acct := range accounts { 73 | repos = append(repos, acct.Repos...) 74 | } 75 | 76 | return repos, nil 77 | } 78 | 79 | // Gets the list of Branches for the repository 80 | func (r *RepoResource) ListBranches(owner, slug string) ([]*Branch, error) { 81 | branchMap := map[string]*Branch{} 82 | path := fmt.Sprintf("/repositories/%s/%s/branches", owner, slug) 83 | 84 | if err := r.client.do("GET", path, nil, nil, &branchMap); err != nil { 85 | return nil, err 86 | } 87 | 88 | // The list is returned in a map ... 89 | // we really want a slice 90 | branches := []*Branch{} 91 | for _, branch := range branchMap { 92 | branches = append(branches, branch) 93 | } 94 | 95 | return branches, nil 96 | } 97 | 98 | // Gets the repositories list for the named user. 99 | func (r *RepoResource) ListUser(owner string) ([]*Repo, error) { 100 | repos := []*Repo{} 101 | path := fmt.Sprintf("/repositories/%s", owner) 102 | 103 | if err := r.client.do("GET", path, nil, nil, &repos); err != nil { 104 | return nil, err 105 | } 106 | 107 | return repos, nil 108 | } 109 | 110 | // Gets the named repository. 111 | func (r *RepoResource) Find(owner, slug string) (*Repo, error) { 112 | repo := Repo{} 113 | path := fmt.Sprintf("/repositories/%s/%s", owner, slug) 114 | 115 | if err := r.client.do("GET", path, nil, nil, &repo); err != nil { 116 | return nil, err 117 | } 118 | 119 | return &repo, nil 120 | } 121 | 122 | // ----------------------------------------------------------------------------- 123 | // Helper Functions to parse odd Bitbucket JSON structure 124 | 125 | func unmarshalAccounts(m [][]interface{}) []*Account { 126 | 127 | accts := []*Account{} 128 | for i := range m { 129 | a := Account{} 130 | for j := range m[i] { 131 | switch v := m[i][j].(type) { 132 | case []interface{}: 133 | a.Repos = unmarshalRepos(v) 134 | case map[string]interface{}: 135 | a.User = unmarshalUser(v) 136 | default: // Unknown...return error? 137 | } 138 | } 139 | accts = append(accts, &a) 140 | } 141 | 142 | return accts 143 | } 144 | 145 | func unmarshalUser(m map[string]interface{}) *User { 146 | u := User{} 147 | for k, v := range m { 148 | switch k { 149 | case "username": 150 | u.Username = v.(string) 151 | case "first_name": 152 | u.FirstName = v.(string) 153 | case "last_name": 154 | u.LastName = v.(string) 155 | case "display_name": 156 | u.DisplayName = v.(string) 157 | case "avatar": 158 | u.Avatar = v.(string) 159 | case "is_team": 160 | u.IsTeam = v.(bool) 161 | } 162 | } 163 | return &u 164 | } 165 | 166 | func unmarshalRepo(m map[string]interface{}) *Repo { 167 | r := Repo{} 168 | for k, v := range m { 169 | // make sure v.(type) is correct type each time? 170 | switch k { 171 | case "name": 172 | r.Name = v.(string) 173 | case "slug": 174 | r.Slug = v.(string) 175 | case "owner": 176 | r.Owner = v.(string) 177 | case "scm": 178 | r.Scm = v.(string) 179 | case "logo": 180 | r.Logo = v.(string) 181 | case "language": 182 | r.Language = v.(string) 183 | case "is_private": 184 | r.Private = v.(bool) 185 | } 186 | 187 | } 188 | return &r 189 | } 190 | 191 | func unmarshalRepos(m []interface{}) []*Repo { 192 | r := []*Repo{} 193 | for i := range m { 194 | switch v := m[i].(type) { 195 | case map[string]interface{}: 196 | r = append(r, unmarshalRepo(v)) 197 | //default: fmt.Println("BAD") 198 | } 199 | } 200 | return r 201 | } 202 | -------------------------------------------------------------------------------- /bitbucket/repos_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Repos(t *testing.T) { 8 | 9 | // LIST of repositories 10 | repos, err := client.Repos.List() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | if len(repos) == 0 { 16 | t.Errorf("List of /user repositories returned empty set") 17 | } 18 | 19 | // LIST dashboard repositories 20 | accts, err := client.Repos.ListDashboard() 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | 25 | if len(accts) == 0 { 26 | t.Errorf("List of dashboard repositories returned empty set") 27 | } 28 | 29 | // FIND the named repo 30 | repo, err := client.Repos.Find(testUser, testRepo) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | if repo.Slug != testRepo { 36 | t.Errorf("repo slug [%v]; want [%v]", repo.Slug, testRepo) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bitbucket/teams.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | const ( 4 | TeamRoleAdmin = "admin" 5 | TeamRoleCollab = "collaborator" 6 | ) 7 | 8 | // Use this resource to manage privilege settings for a team account. Team 9 | // accounts can grant groups account privileges as well as repository access. 10 | // Groups with account privileges are those with can administer this account 11 | // (admin rights) or can create repositories in this account (collaborator 12 | // rights) checked. 13 | // 14 | // https://confluence.atlassian.com/display/BITBUCKET/privileges+Resource 15 | type TeamResource struct { 16 | client *Client 17 | } 18 | 19 | type Team struct { 20 | // The team or individual account name. 21 | Name string 22 | 23 | // The group's slug. 24 | Role string 25 | 26 | } 27 | 28 | // Gets the groups with account privileges defined for a team account. 29 | func (r *TeamResource) List() ([]*Team, error) { 30 | 31 | // we'll get the data in a key/value struct 32 | data := struct { 33 | Teams map[string]string 34 | }{ } 35 | 36 | data.Teams = map[string]string{} 37 | teams := []*Team{} 38 | 39 | if err := r.client.do("GET", "/user/privileges", nil, nil, &data); err != nil { 40 | return nil, err 41 | } 42 | 43 | for k,v := range data.Teams { 44 | team := &Team{ k, v } 45 | teams = append(teams, team) 46 | } 47 | 48 | return teams, nil 49 | } 50 | -------------------------------------------------------------------------------- /bitbucket/teams_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Teams(t *testing.T) { 8 | 9 | teams, err := client.Teams.List() 10 | if err != nil { 11 | t.Error(err) 12 | return 13 | } 14 | 15 | if len(teams) == 0 { 16 | t.Errorf("Returned an empty list of teams. Expected at least one result") 17 | return 18 | } 19 | 20 | if len(teams) == 1 { 21 | if teams[0].Name != testUser { 22 | t.Errorf("expected team name [%s], got [%s]", testUser, teams[0].Name) 23 | } 24 | if teams[0].Role != TeamRoleAdmin { 25 | t.Errorf("expected team role [%s], got [%s]", TeamRoleAdmin, teams[0].Role) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bitbucket/users.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Account struct { 8 | User *User `json:"user"` 9 | Repos []*Repo `json:"repositories"` 10 | } 11 | 12 | type User struct { 13 | Username string `json:"username"` // The name associated with the account. 14 | FirstName string `json:"first_name"` // The first name associated with account. 15 | LastName string `json:"last_name"` // The last name associated with the account. For a team account, this value is always empty. 16 | DisplayName string `json:"display_name"` 17 | Avatar string `json:"avatar"` // An avatar associated with the account. 18 | IsTeam bool `json:"is_team"` // Indicates if this is a Team account. 19 | 20 | } 21 | 22 | // Use the /user endpoints to gets information related to a user 23 | // or team account 24 | // 25 | // https://confluence.atlassian.com/display/BITBUCKET/user+Endpoint 26 | type UserResource struct { 27 | client *Client 28 | } 29 | 30 | // Gets the basic information associated with an account and a list 31 | // of all its repositories both public and private. 32 | func (r *UserResource) Current() (*Account, error) { 33 | user := Account{} 34 | if err := r.client.do("GET", "/user", nil, nil, &user); err != nil { 35 | return nil, err 36 | } 37 | 38 | return &user, nil 39 | } 40 | 41 | // Gets the basic information associated with the specified user 42 | // account. 43 | func (r *UserResource) Find(username string) (*Account, error) { 44 | user := Account{} 45 | path := fmt.Sprintf("/users/%s", username) 46 | 47 | if err := r.client.do("GET", path, nil, nil, &user); err != nil { 48 | return nil, err 49 | } 50 | 51 | return &user, nil 52 | } 53 | 54 | /* TODO 55 | // Updates the basic information associated with an account. 56 | // It operates on the currently authenticated user. 57 | func (r *UserResource) Update(user *User) (*User, error) { 58 | return nil, nil 59 | } 60 | */ 61 | -------------------------------------------------------------------------------- /bitbucket/users_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Users(t *testing.T) { 8 | 9 | // FIND the currently authenticated user 10 | curr, err := client.Users.Current() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | // Find the user by Id 16 | user, err := client.Users.Find(curr.User.Username) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | // verify we get back the correct data 22 | if user.User.Username != curr.User.Username { 23 | t.Errorf("username [%v]; want [%v]", user.User.Username, curr.User.Username) 24 | } 25 | 26 | } 27 | 28 | func Test_UsersGuest(t *testing.T) { 29 | 30 | // FIND the currently authenticated user 31 | user, err := Guest.Users.Find(testUser) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | // verify we get back the correct data 37 | if user.User.Username != testUser { 38 | t.Errorf("username [%v]; want [%v]", user.User.Username, testUser) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /oauth1/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Brad Rydzewski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /oauth1/consumer.go: -------------------------------------------------------------------------------- 1 | package oauth1 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // Out-Of-Band mode, used for applications that do not have 18 | // a callback URL, such as mobile phones or command-line 19 | // utilities. 20 | const OOB = "oob" 21 | 22 | // Consumer represents a website or application that uses the 23 | // OAuth 1.0a protocol to access protected resources on behalf 24 | // of a User. 25 | type Consumer struct { 26 | // A value used by the Consumer to identify itself 27 | // to the Service Provider. 28 | ConsumerKey string 29 | 30 | // A secret used by the Consumer to establish 31 | // ownership of the Consumer Key. 32 | ConsumerSecret string 33 | 34 | // An absolute URL to which the Service Provider will redirect 35 | // the User back when the Obtaining User Authorization step 36 | // is completed. 37 | // 38 | // If the Consumer is unable to receive callbacks or a callback 39 | // URL has been established via other means, the parameter 40 | // value MUST be set to oob (case sensitive), to indicate 41 | // an out-of-band configuration. 42 | CallbackURL string 43 | 44 | // The URL used to obtain an unauthorized 45 | // Request Token. 46 | RequestTokenURL string 47 | 48 | // The URL used to obtain User authorization 49 | // for Consumer access. 50 | AccessTokenURL string 51 | 52 | // The URL used to exchange the User-authorized 53 | // Request Token for an Access Token. 54 | AuthorizationURL string 55 | } 56 | 57 | func (c *Consumer) RequestToken() (*RequestToken, error) { 58 | 59 | // create the http request to fetch a Request Token. 60 | requestTokenUrl, _ := url.Parse(c.RequestTokenURL) 61 | req := http.Request{ 62 | URL: requestTokenUrl, 63 | Method: "POST", 64 | ProtoMajor: 1, 65 | ProtoMinor: 1, 66 | Close: true, 67 | } 68 | 69 | // sign the request 70 | err := c.SignParams(&req, nil, map[string]string{ "oauth_callback":c.CallbackURL }) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | // make the http request and get the response 76 | resp, err := http.DefaultClient.Do(&req) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | // parse the Request's Body 82 | requestToken, err := ParseRequestToken(resp.Body) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return requestToken, nil 88 | } 89 | 90 | // AuthorizeRedirect constructs the request URL that should be used 91 | // to redirect the User to verify User identify and consent. 92 | func (c *Consumer) AuthorizeRedirect(t *RequestToken) (string, error) { 93 | redirect, err := url.Parse(c.AuthorizationURL) 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | params := make(url.Values) 99 | params.Add("oauth_token", t.token) 100 | redirect.RawQuery = params.Encode() 101 | 102 | u := redirect.String() 103 | if strings.HasPrefix(u, "https://bitbucket.org/%21api/") { 104 | u = strings.Replace(u, "/%21api/", "/!api/", -1) 105 | } 106 | 107 | return u, nil 108 | } 109 | 110 | func (c *Consumer) AuthorizeToken(t *RequestToken, verifier string) (*AccessToken, error) { 111 | 112 | // create the http request to fetch a Request Token. 113 | accessTokenUrl, _ := url.Parse(c.AccessTokenURL) 114 | req := http.Request{ 115 | URL: accessTokenUrl, 116 | Method: "POST", 117 | ProtoMajor: 1, 118 | ProtoMinor: 1, 119 | Close: true, 120 | } 121 | 122 | // sign the request 123 | err := c.SignParams(&req, t, map[string]string{ "oauth_verifier":verifier }) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | // make the http request and get the response 129 | resp, err := http.DefaultClient.Do(&req) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | // parse the Request's Body 135 | accessToken, err := ParseAccessToken(resp.Body) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | return accessToken, nil 141 | } 142 | 143 | // Sign will sign an http.Request using the provided token. 144 | func (c *Consumer) Sign(req *http.Request, token Token) error { 145 | return c.SignParams(req, token, nil) 146 | } 147 | 148 | // Sign will sign an http.Request using the provided token, and additional 149 | // parameters. 150 | func (c *Consumer) SignParams(req *http.Request, token Token, params map[string]string) error { 151 | 152 | // ensure the parameter map is not nil 153 | if params == nil { 154 | params = map[string]string{} 155 | } 156 | 157 | // ensure default parameters are set 158 | //params["oauth_token"] = token.Token() 159 | params["oauth_consumer_key"] = c.ConsumerKey 160 | params["oauth_nonce"] = nonce() 161 | params["oauth_signature_method"] = "HMAC-SHA1" 162 | params["oauth_timestamp"] = timestamp() 163 | params["oauth_version"] = "1.0" 164 | 165 | // we'll need to sign any form values? 166 | if req.Form != nil { 167 | for k, _ := range req.Form { 168 | params[k] = req.Form.Get(k) 169 | } 170 | } 171 | 172 | // we'll also need to sign any URL parameter 173 | queryParams := req.URL.Query() 174 | for k, _ := range queryParams { 175 | params[k] = queryParams.Get(k) 176 | } 177 | 178 | var tokenSecret string 179 | if token != nil { 180 | tokenSecret = token.Secret() 181 | params["oauth_token"] = token.Token() 182 | } 183 | 184 | // create the oauth signature 185 | key := escape(c.ConsumerSecret) + "&" + escape(tokenSecret) 186 | base := requestString(req.Method, req.URL.String(), params) 187 | params["oauth_signature"] = sign(base, key) 188 | 189 | //HACK: we were previously including params in the Authorization 190 | // header that shouldn't be. so for now, we'll filter 191 | //authStringParams := map[string]string{} 192 | //for k,v := range params { 193 | // if strings.HasPrefix(k, "oauth_") { 194 | // authStringParams[k]=v 195 | // } 196 | //} 197 | 198 | // ensure the http.Request's Header is not nil 199 | if req.Header == nil { 200 | req.Header = http.Header{} 201 | } 202 | 203 | // add the authorization header string 204 | req.Header.Add("Authorization", authorizationString(params))//params)) 205 | 206 | // ensure the appropriate content-type is set for POST, 207 | // assuming the field is not populated 208 | if (req.Method == "POST" || req.Method == "PUT") && len(req.Header.Get("Content-Type")) == 0 { 209 | req.Header.Set("Content-Type","application/x-www-form-urlencoded") 210 | } 211 | 212 | return nil 213 | } 214 | 215 | // ----------------------------------------------------------------------------- 216 | // Private Helper Functions 217 | 218 | // Nonce generator, seeded with current time 219 | var nonceGenerator = rand.New(rand.NewSource(time.Now().Unix())) 220 | 221 | // Nonce generates a random string. Nonce's are uniquely generated 222 | // for each request. 223 | func nonce() string { 224 | return strconv.FormatInt(nonceGenerator.Int63(), 10) 225 | } 226 | 227 | // Timestamp generates a timestamp, expressed in the number of seconds 228 | // since January 1, 1970 00:00:00 GMT. 229 | func timestamp() string { 230 | return strconv.FormatInt(time.Now().Unix(), 10) 231 | } 232 | 233 | // Generates an HMAC Signature for an OAuth1.0a request. 234 | func sign(message, key string) string { 235 | hashfun := hmac.New(sha1.New, []byte(key)) 236 | hashfun.Write([]byte(message)) 237 | rawsignature := hashfun.Sum(nil) 238 | base64signature := make([]byte, base64.StdEncoding.EncodedLen(len(rawsignature))) 239 | base64.StdEncoding.Encode(base64signature, rawsignature) 240 | 241 | return string(base64signature) 242 | } 243 | 244 | // Gets the default set of OAuth1.0a headers. 245 | func headers(consumerKey string) map[string]string { 246 | return map[string]string{ 247 | "oauth_consumer_key" : consumerKey, 248 | "oauth_nonce" : nonce(), 249 | "oauth_signature_method" : "HMAC-SHA1", 250 | "oauth_timestamp" : timestamp(), 251 | "oauth_version" : "1.0", 252 | } 253 | } 254 | 255 | func requestString(method string, uri string, params map[string]string) string { 256 | 257 | // loop through params, add keys to map 258 | var keys []string 259 | for key, _ := range params { 260 | keys = append(keys, key) 261 | } 262 | 263 | // sort the array of header keys 264 | sort.StringSlice(keys).Sort() 265 | 266 | // create the signed string 267 | result := method + "&" + escape(uri) 268 | 269 | // loop through sorted params and append to the string 270 | for pos, key := range keys { 271 | if pos == 0 { 272 | result += "&" 273 | } else { 274 | result += escape("&") 275 | } 276 | 277 | result += escape(fmt.Sprintf("%s=%s", key, escape(params[key]))) 278 | } 279 | 280 | return result 281 | } 282 | 283 | func authorizationString(params map[string]string) string { 284 | 285 | // loop through params, add keys to map 286 | var keys []string 287 | for key, _ := range params { 288 | keys = append(keys, key) 289 | } 290 | 291 | // sort the array of header keys 292 | sort.StringSlice(keys).Sort() 293 | 294 | // create the signed string 295 | var str string 296 | var cnt = 0 297 | 298 | // loop through sorted params and append to the string 299 | for _, key := range keys { 300 | 301 | // we previously encoded all params (url params, form data & oauth params) 302 | // but for the authorization string we should only encode the oauth params 303 | if !strings.HasPrefix(key, "oauth_") { 304 | continue 305 | } 306 | 307 | if cnt > 0 { 308 | str += "," 309 | } 310 | 311 | str += fmt.Sprintf("%s=%q", key, escape(params[key])) 312 | cnt++ 313 | } 314 | 315 | return fmt.Sprintf("OAuth %s", str) 316 | } 317 | 318 | 319 | func escape(s string) string { 320 | t := make([]byte, 0, 3*len(s)) 321 | for i := 0; i < len(s); i++ { 322 | c := s[i] 323 | if isEscapable(c) { 324 | t = append(t, '%') 325 | t = append(t, "0123456789ABCDEF"[c>>4]) 326 | t = append(t, "0123456789ABCDEF"[c&15]) 327 | } else { 328 | t = append(t, s[i]) 329 | } 330 | } 331 | return string(t) 332 | } 333 | 334 | func isEscapable(b byte) bool { 335 | return !('A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' || b == '-' || b == '.' || b == '_' || b == '~') 336 | 337 | } 338 | 339 | -------------------------------------------------------------------------------- /oauth1/token.go: -------------------------------------------------------------------------------- 1 | package oauth1 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "net/url" 8 | "strconv" 9 | ) 10 | 11 | type Token interface { 12 | Token() string // Gets the oauth_token value. 13 | Secret() string // Gets the oauth_token_secret. 14 | Encode() string // Encode encodes the token into “URL encoded” form. 15 | } 16 | 17 | // AccessToken represents a value used by the Consumer to gain access 18 | // to the Protected Resources on behalf of the User, instead of using 19 | // the User's Service Provider credentials. 20 | type AccessToken struct { 21 | token string // the oauth_token value 22 | secret string // the oauth_token_secret value 23 | params map[string]string // additional params, as defined by the Provider. 24 | } 25 | 26 | // NewAccessToken returns a new instance of AccessToken with the specified 27 | // token, secret and additional parameters. 28 | func NewAccessToken(token, secret string, params map[string]string) *AccessToken { 29 | return &AccessToken { 30 | token : token, 31 | secret : secret, 32 | params : params, 33 | } 34 | } 35 | 36 | // ParseAccessToken parses the URL-encoded query string from the Reader 37 | // and returns an AccessToken. 38 | func ParseAccessToken(reader io.ReadCloser) (*AccessToken, error) { 39 | body, err := ioutil.ReadAll(reader) 40 | reader.Close() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return ParseAccessTokenStr(string(body)) 46 | } 47 | 48 | // ParseAccessTokenStr parses the URL-encoded query string and returns 49 | // an AccessToken. 50 | func ParseAccessTokenStr(str string) (*AccessToken, error) { 51 | token := AccessToken{} 52 | token.params = map[string]string{} 53 | 54 | //parse the request token from the body 55 | parts, err := url.ParseQuery(str) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | //loop through parts to create Token 61 | for key, val := range parts { 62 | switch key { 63 | case "oauth_token" : token.token = val[0] 64 | case "oauth_token_secret" : token.secret = val[0] 65 | default : token.params[key] = val[0] 66 | } 67 | } 68 | 69 | //some error checking ... 70 | switch { 71 | case len(token.token) == 0 : return nil, errors.New(str) 72 | case len(token.secret) == 0 : return nil, errors.New(str) 73 | } 74 | 75 | return &token, nil 76 | } 77 | 78 | // Encode encodes the values into “URL encoded” form of the AccessToken. 79 | // e.g. "oauth_token=foo&oauth_token_secret=baz" 80 | func (a *AccessToken) Encode() string { 81 | values := url.Values{} 82 | values.Set("oauth_token", a.token) 83 | values.Set("oauth_token_secret", a.secret) 84 | if a.params != nil { 85 | for key, val := range a.params { 86 | values.Set(key, val) 87 | } 88 | } 89 | return values.Encode() 90 | } 91 | 92 | // Gets the oauth_token value 93 | func (a *AccessToken) Token() string { return a.token } 94 | 95 | // Gets the oauth_token_secret value 96 | func (a *AccessToken) Secret() string { return a.secret } 97 | 98 | // Gets any additional parameters, as defined by the Service Provider. 99 | func (a *AccessToken) Params() map[string]string { return a.params } 100 | 101 | 102 | // RequestToken represents a value used by the Consumer to obtain 103 | // authorization from the User, and exchanged for an Access Token. 104 | type RequestToken struct { 105 | token string // the oauth_token value 106 | secret string // the oauth_token_secret value 107 | callbackConfirmed bool 108 | } 109 | 110 | // ParseRequestToken parses the URL-encoded query string from the Reader 111 | // and returns a RequestToken. 112 | func ParseRequestToken(reader io.ReadCloser) (*RequestToken, error) { 113 | body, err := ioutil.ReadAll(reader) 114 | reader.Close() 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return ParseRequestTokenStr(string(body)) 120 | } 121 | 122 | // ParseRequestTokenStr parses the URL-encoded query string and returns 123 | // a RequestToken. 124 | func ParseRequestTokenStr(str string) (*RequestToken, error) { 125 | //parse the request token from the body 126 | parts, err := url.ParseQuery(str) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | token := RequestToken{} 132 | token.token = parts.Get("oauth_token") 133 | token.secret = parts.Get("oauth_token_secret") 134 | token.callbackConfirmed = parts.Get("oauth_callback_confirmed") == "true" 135 | 136 | //some error checking ... 137 | switch { 138 | case len(token.token) == 0 : return nil, errors.New(str) 139 | case len(token.secret) == 0 : return nil, errors.New(str) 140 | } 141 | 142 | return &token, nil 143 | } 144 | 145 | // Encode encodes the values into “URL encoded” form of the ReqeustToken. 146 | // e.g. "oauth_token=foo&oauth_token_secret=baz" 147 | func (r *RequestToken) Encode() string { 148 | values := url.Values{} 149 | values.Set("oauth_token", r.token) 150 | values.Set("oauth_token_secret", r.secret) 151 | values.Set("oauth_callback_confirmed", strconv.FormatBool(r.callbackConfirmed)) 152 | return values.Encode() 153 | } 154 | 155 | // Gets the oauth_token value 156 | func (r *RequestToken) Token() string { return r.token } 157 | 158 | // Gets the oauth_token_secret value 159 | func (r *RequestToken) Secret() string { return r.secret } 160 | -------------------------------------------------------------------------------- /oauth1/token_test.go: -------------------------------------------------------------------------------- 1 | package oauth1 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | // Test the ability to parse a URL query string and unmarshal to a RequestToken. 10 | func TestParseRequestTokenStr(t *testing.T) { 11 | oauth_token:="c0cf8793d39d46ab" 12 | oauth_token_secret:="FMMj3w7plPEyhK8ZZ9lBsp" 13 | oauth_callback_confirmed:=true 14 | 15 | values := url.Values{} 16 | values.Set("oauth_token", oauth_token) 17 | values.Set("oauth_token_secret", oauth_token_secret) 18 | values.Set("oauth_callback_confirmed", strconv.FormatBool(oauth_callback_confirmed)) 19 | 20 | token, err := ParseRequestTokenStr(values.Encode()) 21 | if err != nil { 22 | t.Errorf("Expected Request Token parsed, got Error %s", err.Error()) 23 | } 24 | if token.token != oauth_token { 25 | t.Errorf("Expected Request Token %v, got %v", oauth_token, token.token) 26 | } 27 | if token.secret != oauth_token_secret { 28 | t.Errorf("Expected Request Token Secret %v, got %v", oauth_token_secret, token.secret) 29 | } 30 | } 31 | 32 | // Test the ability to Encode a RequestToken to a URL query string. 33 | func TestEncodeRequestToken(t *testing.T) { 34 | token := RequestToken { 35 | token : "c0cf8793d39d46ab", 36 | secret : "FMMj3w7plPEyhK8ZZ9lBsp", 37 | callbackConfirmed : true, 38 | } 39 | 40 | tokenStr := token.Encode() 41 | expectedStr := "oauth_token_secret=FMMj3w7plPEyhK8ZZ9lBsp&oauth_token=c0cf8793d39d46ab&oauth_callback_confirmed=true" 42 | if tokenStr != expectedStr { 43 | t.Errorf("Expected Request Token Encoded as %v, got %v", expectedStr, tokenStr) 44 | } 45 | } 46 | 47 | // Test the ability to parse a URL query string and unmarshal to an AccessToken. 48 | func TestEncodeAccessTokenStr(t *testing.T) { 49 | oauth_token:="c0cf8793d39d46ab" 50 | oauth_token_secret:="FMMj3w7plPEyhK8ZZ9lBsp" 51 | oauth_callback_confirmed:=true 52 | 53 | values := url.Values{} 54 | values.Set("oauth_token", oauth_token) 55 | values.Set("oauth_token_secret", oauth_token_secret) 56 | values.Set("oauth_callback_confirmed", strconv.FormatBool(oauth_callback_confirmed)) 57 | 58 | token, err := ParseAccessTokenStr(values.Encode()) 59 | if err != nil { 60 | t.Errorf("Expected Access Token parsed, got Error %s", err.Error()) 61 | } 62 | if token.token != oauth_token { 63 | t.Errorf("Expected Access Token %v, got %v", oauth_token, token.token) 64 | } 65 | if token.secret != oauth_token_secret { 66 | t.Errorf("Expected Access Token Secret %v, got %v", oauth_token_secret, token.secret) 67 | } 68 | } 69 | 70 | // Test the ability to Encode an AccessToken to a URL query string. 71 | func TestEncodeAccessToken(t *testing.T) { 72 | token := AccessToken { 73 | token : "c0cf8793d39d46ab", 74 | secret : "FMMj3w7plPEyhK8ZZ9lBsp", 75 | params : map[string]string{ "user" : "dr_van_nostrand" }, 76 | } 77 | 78 | tokenStr := token.Encode() 79 | expectedStr := "user=dr_van_nostrand&oauth_token_secret=FMMj3w7plPEyhK8ZZ9lBsp&oauth_token=c0cf8793d39d46ab" 80 | if tokenStr != expectedStr { 81 | t.Errorf("Expected Access Token Encoded as %v, got %v", expectedStr, tokenStr) 82 | } 83 | } 84 | --------------------------------------------------------------------------------