├── .gitignore ├── go.mod ├── api.go ├── .pre-commit-config.yaml ├── examples ├── README.md ├── space │ └── space.go ├── user │ └── user.go ├── labels │ └── labels.go ├── search │ └── search.go ├── attributes │ └── attributes.go └── content │ └── content.go ├── auth.go ├── .github ├── workflows │ ├── gosec.yaml │ └── test.yaml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── magefile.go ├── attachment_test.go ├── user_test.go ├── doc.go ├── LICENSE ├── go.sum ├── space_test.go ├── search_test.go ├── template_test.go ├── auth_test.go ├── internal.go ├── attachment.go ├── user.go ├── space.go ├── internal_test.go ├── search.go ├── template.go ├── CODE_OF_CONDUCT.md ├── README.md ├── content_test.go ├── CONTRIBUTING.md ├── request.go ├── request_test.go └── content.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/test.go 2 | examples/epages.go 3 | coverage.out 4 | .idea 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/virtomize/confluence-go-api 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/magefile/mage v1.14.0 7 | github.com/stretchr/testify v1.3.0 8 | ) 9 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | // API is the main api data structure 9 | type API struct { 10 | endPoint *url.URL 11 | Client *http.Client 12 | username, token string 13 | } 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/TekWizely/pre-commit-golang 5 | rev: v0.8.3 # update to v1 after release from beta 6 | hooks: 7 | - id: golangci-lint-mod 8 | - id: go-sec-mod 9 | - id: go-test-mod 10 | 11 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # example use cases 2 | 3 | - content.go 4 | - get, create and update content 5 | 6 | - attributes.go 7 | - get comments, attachments, child pages, history and watchers 8 | 9 | - labels.go 10 | - get, add, delete labels 11 | 12 | - user.go 13 | - get currentUser, AnonymousUser or user by name/accountid 14 | 15 | - search.go 16 | - searching example using CQL 17 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Auth implements basic auth 8 | func (a *API) Auth(req *http.Request) { 9 | //Supports unauthenticated access to confluence: 10 | //if username and token are not set, do not add authorization header 11 | if a.username != "" && a.token != "" { 12 | req.SetBasicAuth(a.username, a.token) 13 | } else if a.token != "" { 14 | req.Header.Set("Authorization", "Bearer "+a.token) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yaml: -------------------------------------------------------------------------------- 1 | name: GoSec 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | gosec: 13 | runs-on: ubuntu-latest 14 | env: 15 | GO111MODULE: on 16 | steps: 17 | - name: Checkout Source 18 | uses: actions/checkout@v2 19 | - name: Run Gosec Security Scanner 20 | uses: securego/gosec@master 21 | with: 22 | args: "-nosec=false ./..." 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.18 21 | 22 | - name: Run Mage 23 | uses: magefile/mage-action@v2 24 | with: 25 | version: latest 26 | args: test 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ##### Checklist 11 | 12 | 13 | - [ ] `mage test` passes 14 | - [ ] unit tests are included and tested 15 | - [ ] documentation is added or changed 16 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //+build mage 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/magefile/mage/sh" 10 | ) 11 | 12 | // Test - mage run 13 | func Test() error { 14 | return sh.RunV("go", "test", "-v", "-cover", ".", "-coverprofile=coverage.out") 15 | } 16 | 17 | // Coverage - checking code coverage 18 | func Coverage() error { 19 | if _, err := os.Stat("./coverage.out"); err != nil { 20 | return fmt.Errorf("run mage test befor checking the code coverage") 21 | } 22 | return sh.RunV("go", "tool", "cover", "-html=coverage.out") 23 | } 24 | -------------------------------------------------------------------------------- /examples/space/space.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | spaces, err := api.GetAllSpaces(goconfluence.AllSpacesQuery{ 17 | Type: "global", 18 | Start: 0, 19 | Limit: 10, 20 | }) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | for _, space := range spaces.Results { 26 | fmt.Printf("Space Key: %s\n", space.Key) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Release version** 24 | The version tag of the used version. 25 | 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /attachment_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetAttachmentEndpoint(t *testing.T) { 10 | a, err := NewAPI("https://test.test", "username", "token") 11 | assert.Nil(t, err) 12 | 13 | url, err := a.getAttachmentEndpoint("test") 14 | assert.Nil(t, err) 15 | assert.Equal(t, "/attachments/test", url.Path) 16 | } 17 | 18 | func TestAttachmentGetter(t *testing.T) { 19 | server := confluenceRestAPIStub() 20 | defer server.Close() 21 | 22 | api, err := NewAPI(server.URL+"/wiki/api/v2", "userame", "token") 23 | assert.Nil(t, err) 24 | 25 | c, err := api.GetAttachmentById("2495990589") 26 | assert.Nil(t, err) 27 | assert.Equal(t, &Attachment{}, c) 28 | } 29 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUser(t *testing.T) { 10 | server := confluenceRestAPIStub() 11 | defer server.Close() 12 | 13 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 14 | assert.NoError(t, err) 15 | 16 | u, err := api.CurrentUser() 17 | assert.NoError(t, err) 18 | assert.Equal(t, &User{}, u) 19 | 20 | u, err = api.AnonymousUser() 21 | assert.NoError(t, err) 22 | assert.Equal(t, &User{}, u) 23 | 24 | u, err = api.User("42") 25 | assert.NoError(t, err) 26 | assert.Equal(t, &User{}, u) 27 | 28 | u, err = api.User(":42") 29 | assert.NoError(t, err) 30 | assert.Equal(t, &User{}, u) 31 | 32 | u, err = api.User("key:42") 33 | assert.NoError(t, err) 34 | assert.Equal(t, &User{}, u) 35 | } 36 | -------------------------------------------------------------------------------- /examples/user/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // get current user information 17 | currentUser, err := api.CurrentUser() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | fmt.Printf("%+v\n", currentUser) 22 | 23 | // get anonymous user information 24 | anonUser, err := api.AnonymousUser() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | fmt.Printf("%+v\n", anonUser) 29 | 30 | // get user by username or accountId 31 | user, err := api.User("someuser") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | fmt.Printf("%+v\n", user) 36 | } 37 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package goconfluence implementing atlassian's Confluence API 3 | 4 | Simple example: 5 | 6 | //Initialize a new API instance 7 | api, err := goconfluence.NewAPI( 8 | "https://.atlassian.net/wiki/rest/api", 9 | "", 10 | "", 11 | ) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // get current user information 17 | currentUser, err := api.CurrentUser() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | fmt.Printf("%+v\n", currentUser) 22 | 23 | supported features: 24 | - get user information 25 | - create, update, delete content 26 | - get comments, attachments, history, watchers and children of content objects 27 | - get, add, delete labels 28 | - search using CQL 29 | 30 | see https://github.com/virtomize/confluence-go-api/tree/master/examples for more information and usage examples 31 | */ 32 | package goconfluence 33 | -------------------------------------------------------------------------------- /examples/labels/labels.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // get label information 17 | labels, err := api.GetLabels("1234567") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | for _, v := range labels.Labels { 23 | fmt.Printf("%+v\n", v) 24 | } 25 | 26 | // add new label 27 | newlabels := []goconfluence.Label{ 28 | {Prefix: "global", Name: "test-label-api"}, 29 | } 30 | 31 | lres, err := api.AddLabels("1234567", &newlabels) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | for _, v := range lres.Labels { 37 | fmt.Printf("%+v\n", v) 38 | } 39 | 40 | // remove label 41 | _, err = api.DeleteLabel("1234567", "test-label-api") 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/search/search.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // define your Search parameters 17 | query := goconfluence.SearchQuery{ 18 | CQL: "space=SomeSpace", 19 | } 20 | 21 | // execute search 22 | result, err := api.Search(query) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | // loop over results 28 | for _, v := range result.Results { 29 | fmt.Printf("%+v\n", v) 30 | } 31 | 32 | // search example with paging using SearchWithNext and Links.Next 33 | next := "" 34 | for { 35 | resp, err := api.SearchWithNext(query, next) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | for _, v := range result.Results { 40 | fmt.Printf("%+v\n", v) 41 | } 42 | next = resp.Links.Next 43 | if next == "" { 44 | break 45 | } 46 | log.Printf("Using next page: %s", next) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Carsten Seeger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/magefile/mage v1.8.0 h1:mzL+xIopvPURVBwHG9A50JcjBO+xV3b5iZ7khFRI+5E= 4 | github.com/magefile/mage v1.8.0/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA= 5 | github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= 6 | github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 7 | github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= 8 | github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 13 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 14 | -------------------------------------------------------------------------------- /space_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAllSpacesQueryParams(t *testing.T) { 10 | query := AllSpacesQuery{ 11 | Expand: []string{"a", "b"}, 12 | Favourite: true, 13 | FavouriteUserKey: "key1", 14 | SpaceKey: "KEY", 15 | Status: "sta", 16 | Type: "global", 17 | Limit: 1, 18 | Start: 1, 19 | } 20 | p := addAllSpacesQueryParams(query) 21 | assert.Equal(t, p.Get("expand"), "a,b") 22 | assert.Equal(t, p.Get("favourite"), "true") 23 | assert.Equal(t, p.Get("favouriteUserKey"), "key1") 24 | assert.Equal(t, p.Get("spaceKey"), "KEY") 25 | assert.Equal(t, p.Get("status"), "sta") 26 | assert.Equal(t, p.Get("type"), "global") 27 | assert.Equal(t, p.Get("limit"), "1") 28 | assert.Equal(t, p.Get("start"), "1") 29 | } 30 | 31 | func TestGetAllSpacesQuery(t *testing.T) { 32 | server := confluenceRestAPIStub() 33 | defer server.Close() 34 | 35 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 36 | assert.Nil(t, err) 37 | 38 | s, err := api.GetAllSpaces(AllSpacesQuery{}) 39 | assert.Nil(t, err) 40 | assert.Equal(t, &AllSpaces{}, s) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSearchQueryParams(t *testing.T) { 10 | query := SearchQuery{ 11 | CQL: "test", 12 | CQLContext: "test", 13 | IncludeArchivedSpaces: true, 14 | Limit: 1, 15 | Start: 1, 16 | } 17 | p := addSearchQueryParams(query) 18 | assert.Equal(t, p.Get("cql"), "test") 19 | assert.Equal(t, p.Get("cqlcontext"), "test") 20 | assert.Equal(t, p.Get("includeArchivedSpaces"), "true") 21 | assert.Equal(t, p.Get("limit"), "1") 22 | assert.Equal(t, p.Get("start"), "1") 23 | } 24 | 25 | func TestSearch(t *testing.T) { 26 | server := confluenceRestAPIStub() 27 | defer server.Close() 28 | 29 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 30 | assert.Nil(t, err) 31 | 32 | s, err := api.Search(SearchQuery{}) 33 | assert.Nil(t, err) 34 | assert.Equal(t, &Search{}, s) 35 | 36 | } 37 | 38 | func TestSearchWithNext(t *testing.T) { 39 | server := confluenceRestAPIStub() 40 | defer server.Close() 41 | 42 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 43 | assert.Nil(t, err) 44 | 45 | s, err := api.SearchWithNext(SearchQuery{}, "") 46 | assert.Nil(t, err) 47 | assert.Equal(t, &Search{}, s) 48 | 49 | s, err = api.SearchWithNext(SearchQuery{}, "/rest/api/search?next=true&cursor=abc123") 50 | assert.Nil(t, err) 51 | assert.Equal(t, &Search{}, s) 52 | } 53 | -------------------------------------------------------------------------------- /examples/attributes/attributes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // get comments of a specific page 17 | res, err := api.GetComments("1234567") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | for _, v := range res.Results { 23 | fmt.Printf("%+v\n", v) 24 | } 25 | 26 | // get attachments of a specific page 27 | res, err = api.GetAttachments("1234567") 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | // loop over results 33 | for _, v := range res.Results { 34 | fmt.Printf("%+v\n", v) 35 | } 36 | 37 | // get child pages of a specific page 38 | res, err = api.GetChildPages("1234567") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // loop over results 44 | for _, v := range res.Results { 45 | fmt.Printf("%+v\n", v) 46 | } 47 | 48 | // get history information of a page 49 | hist, err := api.GetHistory("1234567") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | fmt.Printf("%+v\n", hist) 55 | 56 | // get information about watching users 57 | watchers, err := api.GetWatchers("1234567") 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | for _, v := range watchers.Watchers { 63 | fmt.Printf("%+v\n", v) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetContentTemplatesEndpoints(t *testing.T) { 11 | a, err := NewAPI("https://test.test", "", "") 12 | assert.Nil(t, err) 13 | 14 | ep, err := a.getContentTemplatesEndpoint() 15 | assert.Nil(t, err) 16 | uri, err := url.ParseRequestURI("https://test.test/template/page") 17 | assert.Nil(t, err) 18 | assert.Equal(t, ep, uri) 19 | } 20 | 21 | func TestGetBlueprintTemplatesEndpoints(t *testing.T) { 22 | a, err := NewAPI("https://test.test", "", "") 23 | assert.Nil(t, err) 24 | 25 | ep, err := a.getBlueprintTemplatesEndpoint() 26 | assert.Nil(t, err) 27 | uri, err := url.ParseRequestURI("https://test.test/template/blueprint") 28 | assert.Nil(t, err) 29 | assert.Equal(t, ep, uri) 30 | } 31 | 32 | func TestBlueprintTemplateGetter(t *testing.T) { 33 | server := confluenceRestAPIStub() 34 | defer server.Close() 35 | 36 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 37 | assert.Nil(t, err) 38 | 39 | b, err := api.GetBlueprintTemplates(TemplateQuery{}) 40 | assert.Nil(t, err) 41 | assert.Equal(t, &TemplateSearch{}, b) 42 | } 43 | 44 | func TestContentTemplateGetter(t *testing.T) { 45 | 46 | server := confluenceRestAPIStub() 47 | defer server.Close() 48 | 49 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 50 | assert.Nil(t, err) 51 | 52 | c, err := api.GetContentTemplates(TemplateQuery{}) 53 | assert.Nil(t, err) 54 | assert.Equal(t, &TemplateSearch{}, c) 55 | } 56 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAuth(t *testing.T) { 13 | 14 | t.Run("basic-auth", func(t *testing.T) { 15 | req := httptest.NewRequest("POST", "https://test.test", nil) 16 | api, err := NewAPI("https://test.test", "username", "token") 17 | 18 | assert.Nil(t, err) 19 | assert.Empty(t, req.Header) 20 | 21 | api.Auth(req) 22 | h := req.Header.Get("Authorization") 23 | assert.NotEmpty(t, h) 24 | 25 | split := strings.Split(h, " ") 26 | assert.Len(t, split, 2) 27 | 28 | b, err := base64.StdEncoding.DecodeString(split[1]) 29 | assert.Nil(t, err) 30 | 31 | auth := strings.Split(string(b), ":") 32 | assert.Len(t, auth, 2) 33 | assert.Equal(t, "username", auth[0]) 34 | assert.Equal(t, "token", auth[1]) 35 | }) 36 | 37 | t.Run("empty-auth", func(t *testing.T) { 38 | req := httptest.NewRequest("POST", "https://test.test", nil) 39 | 40 | api, err := NewAPI("https://test.test", "", "") 41 | 42 | assert.Nil(t, err) 43 | assert.Empty(t, req.Header) 44 | 45 | api.Auth(req) 46 | h := req.Header.Get("Authorization") 47 | assert.Empty(t, h) 48 | }) 49 | 50 | t.Run("token-auth", func(t *testing.T) { 51 | req := httptest.NewRequest("POST", "https://test.test", nil) 52 | 53 | api, err := NewAPI("https://test.test", "", "token") 54 | 55 | assert.Nil(t, err) 56 | assert.Empty(t, req.Header) 57 | 58 | api.Auth(req) 59 | h := req.Header.Get("Authorization") 60 | assert.Equal(t, "Bearer token", h) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /internal.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | var ( 12 | errEmptyHTTPClient = errors.New("empty http client") 13 | ) 14 | 15 | // NewAPI implements API constructor 16 | func NewAPI(location string, username string, token string) (*API, error) { 17 | if len(location) == 0 { 18 | return nil, errors.New("url empty") 19 | } 20 | 21 | u, err := url.ParseRequestURI(location) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | a := new(API) 28 | a.endPoint = u 29 | a.token = token 30 | a.username = username 31 | 32 | // #nosec G402 33 | tr := &http.Transport{ 34 | TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, 35 | } 36 | 37 | a.Client = &http.Client{Transport: tr} 38 | 39 | return a, nil 40 | } 41 | 42 | // NewAPIWithClient creates a new API instance using an existing HTTP client. 43 | // Useful when using oauth or other authentication methods. 44 | func NewAPIWithClient(location string, client *http.Client) (*API, error) { 45 | u, err := url.ParseRequestURI(location) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if client == nil { 52 | return nil, errEmptyHTTPClient 53 | } 54 | 55 | a := new(API) 56 | a.endPoint = u 57 | a.Client = client 58 | 59 | return a, nil 60 | } 61 | 62 | // VerifyTLS to enable disable certificate checks 63 | func (a *API) VerifyTLS(set bool) { 64 | // #nosec G402 65 | tr := &http.Transport{ 66 | TLSClientConfig: &tls.Config{InsecureSkipVerify: !set}, 67 | } 68 | a.Client = &http.Client{Transport: tr} 69 | } 70 | 71 | // DebugFlag is the global debugging variable 72 | var DebugFlag = false 73 | 74 | // SetDebug enables debug output 75 | func SetDebug(state bool) { 76 | DebugFlag = state 77 | } 78 | 79 | // Debug outputs debug messages 80 | func Debug(msg interface{}) { 81 | if DebugFlag { 82 | fmt.Printf("%+v\n", msg) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /attachment.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type Attachment struct { 11 | MediaTypeDescription string `json:"mediaTypeDescription"` 12 | WebuiLink string `json:"webuiLink"` 13 | DownloadLink string `json:"downloadLink"` 14 | CreatedAt interface{} `json:"createdAt"` 15 | ID string `json:"id"` 16 | Comment string `json:"comment"` 17 | Version struct { 18 | Number int `json:"number"` 19 | Message string `json:"message"` 20 | MinorEdit bool `json:"minorEdit"` 21 | AuthorID string `json:"authorId"` 22 | CreatedAt time.Time `json:"createdAt"` 23 | } `json:"version"` 24 | Title string `json:"title"` 25 | FileSize int `json:"fileSize"` 26 | Status string `json:"status"` 27 | PageID string `json:"pageId"` 28 | FileID string `json:"fileId"` 29 | MediaType string `json:"mediaType"` 30 | Links struct { 31 | Download string `json:"download"` 32 | Webui string `json:"webui"` 33 | } `json:"_links"` 34 | } 35 | 36 | // getAttachmentEndpoint creates the correct api endpoint by given id 37 | func (a *API) getAttachmentEndpoint(id string) (*url.URL, error) { 38 | return url.ParseRequestURI(a.endPoint.String() + "/attachments/" + id) 39 | } 40 | 41 | // Get info on specific attachment by id 42 | func (a *API) GetAttachmentById(id string) (*Attachment, error) { 43 | ep, err := a.getAttachmentEndpoint(id) 44 | if err != nil { 45 | return nil, err 46 | } 47 | req, err := http.NewRequest("GET", ep.String(), nil) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | res, err := a.Request(req) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | var attachment Attachment 58 | 59 | err = json.Unmarshal(res, &attachment) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return &attachment, nil 64 | } 65 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | // User defines user informations 9 | type User struct { 10 | Type string `json:"type"` 11 | Username string `json:"username"` 12 | UserKey string `json:"userKey"` 13 | AccountID string `json:"accountId"` 14 | DisplayName string `json:"displayName"` 15 | } 16 | 17 | // getUserEndpoint creates the correct api endpoint by given id 18 | func (a *API) getUserEndpoint(id string) (*url.URL, error) { 19 | return url.ParseRequestURI(a.endPoint.String() + "/user?accountId=" + id) 20 | } 21 | 22 | // getCurrentUserEndpoint creates the correct api endpoint by given id 23 | func (a *API) getCurrentUserEndpoint() (*url.URL, error) { 24 | return url.ParseRequestURI(a.endPoint.String() + "/user/current") 25 | } 26 | 27 | // getAnonymousUserEndpoint creates the correct api endpoint by given id 28 | func (a *API) getAnonymousUserEndpoint() (*url.URL, error) { 29 | return url.ParseRequestURI(a.endPoint.String() + "/user/anonymous") 30 | } 31 | 32 | // CurrentUser return current user information 33 | func (a *API) CurrentUser() (*User, error) { 34 | ep, err := a.getCurrentUserEndpoint() 35 | if err != nil { 36 | return nil, err 37 | } 38 | return a.SendUserRequest(ep, "GET") 39 | } 40 | 41 | // AnonymousUser return user information for anonymous user 42 | func (a *API) AnonymousUser() (*User, error) { 43 | ep, err := a.getAnonymousUserEndpoint() 44 | if err != nil { 45 | return nil, err 46 | } 47 | return a.SendUserRequest(ep, "GET") 48 | } 49 | 50 | // User returns user data for defined query 51 | // query can be accountID or username 52 | func (a *API) User(query string) (*User, error) { 53 | ep, err := a.getUserEndpoint("") 54 | if err != nil { 55 | return nil, err 56 | } 57 | data := url.Values{} 58 | if strings.Contains(query, ":") { 59 | lookupParams := strings.Split(query, ":") 60 | if lookupParams[0] == "key" { 61 | data.Set("key", lookupParams[1]) 62 | } else { 63 | data.Set("accountId", lookupParams[1]) 64 | } 65 | } else { 66 | data.Set("username", query) 67 | } 68 | 69 | ep.RawQuery = data.Encode() 70 | return a.SendUserRequest(ep, "GET") 71 | } 72 | -------------------------------------------------------------------------------- /examples/content/content.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | goconfluence "github.com/virtomize/confluence-go-api" 8 | ) 9 | 10 | func main() { 11 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | // get content by content id 17 | c, err := api.GetContentByID("12345678", goconfluence.ContentQuery{ 18 | SpaceKey: "IM", 19 | Expand: []string{"body.storage", "version"}, 20 | }) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | fmt.Printf("%+v\n", c) 25 | 26 | //get content by query 27 | res, err := api.GetContent(goconfluence.ContentQuery{ 28 | SpaceKey: "IM", 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | for _, v := range res.Results { 35 | fmt.Printf("%+v\n", v) 36 | } 37 | 38 | // create content 39 | data := &goconfluence.Content{ 40 | Type: "page", // can also be blogpost 41 | Title: "Some-Test-Page", // page title (mandatory) 42 | Ancestors: []goconfluence.Ancestor{ 43 | { 44 | ID: "123456", // ancestor-id optional if you want to create sub-pages 45 | }, 46 | }, 47 | Body: goconfluence.Body{ 48 | Storage: goconfluence.Storage{ 49 | Value: "#api-test\nnew sub\npage", // your page content here 50 | Representation: "storage", 51 | }, 52 | }, 53 | Version: &goconfluence.Version{ // mandatory 54 | Number: 1, 55 | }, 56 | Space: &goconfluence.Space{ 57 | Key: "SomeSpaceKey", // Space 58 | }, 59 | } 60 | 61 | c, err = api.CreateContent(data) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | fmt.Printf("%+v\n", c) 67 | 68 | // update content 69 | data = &goconfluence.Content{ 70 | ID: "1234567", 71 | Type: "page", 72 | Title: "updated-title", 73 | Ancestors: []goconfluence.Ancestor{ 74 | { 75 | ID: "2345678", 76 | }, 77 | }, 78 | Body: goconfluence.Body{ 79 | Storage: goconfluence.Storage{ 80 | Value: "#api-page\nnew\ncontent", 81 | Representation: "storage", 82 | }, 83 | }, 84 | Version: &goconfluence.Version{ 85 | Number: 2, 86 | }, 87 | Space: &goconfluence.Space{ 88 | Key: "SomeSpaceKey", 89 | }, 90 | } 91 | 92 | c, err = api.UpdateContent(data) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | 97 | fmt.Printf("%+v\n", c) 98 | 99 | } 100 | -------------------------------------------------------------------------------- /space.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // AllSpaces results 10 | type AllSpaces struct { 11 | Results []Space `json:"results"` 12 | Start int `json:"start,omitempty"` 13 | Limit int `json:"limit,omitempty"` 14 | Size int `json:"size,omitempty"` 15 | } 16 | 17 | // AllSpacesQuery defines the query parameters 18 | // Query parameter values https://developer.atlassian.com/cloud/confluence/rest/#api-space-get 19 | type AllSpacesQuery struct { 20 | Expand []string 21 | Favourite bool // Filter the results to the favourite spaces of the user specified by favouriteUserKey 22 | FavouriteUserKey string // The userKey of the user, whose favourite spaces are used to filter the results when using the favourite parameter. Leave blank for the current user 23 | Limit int // page limit 24 | SpaceKey string 25 | Start int // page start 26 | Status string // current, archived 27 | Type string // global, personal 28 | } 29 | 30 | // getSpaceEndpoint creates the correct api endpoint 31 | func (a *API) getSpaceEndpoint() (*url.URL, error) { 32 | return url.ParseRequestURI(a.endPoint.String() + "/space") 33 | } 34 | 35 | // GetAllSpaces queries content using a query parameters 36 | func (a *API) GetAllSpaces(query AllSpacesQuery) (*AllSpaces, error) { 37 | ep, err := a.getSpaceEndpoint() 38 | if err != nil { 39 | return nil, err 40 | } 41 | ep.RawQuery = addAllSpacesQueryParams(query).Encode() 42 | return a.SendAllSpacesRequest(ep, "GET") 43 | } 44 | 45 | // addAllSpacesQueryParams adds the defined query parameters 46 | func addAllSpacesQueryParams(query AllSpacesQuery) *url.Values { 47 | 48 | data := url.Values{} 49 | if len(query.Expand) != 0 { 50 | data.Set("expand", strings.Join(query.Expand, ",")) 51 | } 52 | if query.Favourite { 53 | data.Set("favourite", strconv.FormatBool(query.Favourite)) 54 | } 55 | if query.FavouriteUserKey != "" { 56 | data.Set("favouriteUserKey", query.FavouriteUserKey) 57 | } 58 | if query.Limit != 0 { 59 | data.Set("limit", strconv.Itoa(query.Limit)) 60 | } 61 | if query.SpaceKey != "" { 62 | data.Set("spaceKey", query.SpaceKey) 63 | } 64 | if query.Start != 0 { 65 | data.Set("start", strconv.Itoa(query.Start)) 66 | } 67 | if query.Status != "" { 68 | data.Set("status", query.Status) 69 | } 70 | if query.Type != "" { 71 | data.Set("type", query.Type) 72 | } 73 | return &data 74 | } 75 | -------------------------------------------------------------------------------- /internal_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type apiTestValue struct { 13 | Name string 14 | Input []string 15 | Error error 16 | } 17 | 18 | type apiClientTestValue struct { 19 | Name string 20 | Location string 21 | Client *http.Client 22 | Error error 23 | } 24 | 25 | func TestNewAPI(t *testing.T) { 26 | assert := assert.New(t) 27 | 28 | testValues := []apiTestValue{ 29 | {"empty-url", []string{"", "username", "token"}, fmt.Errorf("url empty")}, 30 | {"no-auth", []string{"https://test.test", "", ""}, nil}, 31 | {"basic-auth", []string{"https://test.test", "username", "token"}, nil}, 32 | {"invalid-url", []string{"test", "username", "token"}, fmt.Errorf("parse \"test\": invalid URI for request")}, 33 | } 34 | 35 | for _, test := range testValues { 36 | t.Run(test.Name, func(t *testing.T) { 37 | api, err := NewAPI(test.Input[0], test.Input[1], test.Input[2]) 38 | if err != nil { 39 | assert.Equal(test.Error.Error(), err.Error()) 40 | } else { 41 | assert.Equal(test.Input[0], api.endPoint.String()) 42 | assert.Equal(test.Input[1], api.username) 43 | assert.Equal(test.Input[2], api.token) 44 | } 45 | }) 46 | } 47 | 48 | testClientValues := []apiClientTestValue{ 49 | {"valid-client", "https://test.test", &http.Client{}, nil}, 50 | {"nil-client", "https://test.test", nil, errEmptyHTTPClient}, 51 | {"invalid-url", "no-url", &http.Client{}, fmt.Errorf("parse \"no-url\": invalid URI for request")}, 52 | } 53 | 54 | for _, test := range testClientValues { 55 | t.Run(test.Name, func(t *testing.T) { 56 | api, err := NewAPIWithClient(test.Location, test.Client) 57 | if err != nil { 58 | assert.Equal(test.Error.Error(), err.Error()) 59 | } else { 60 | assert.Equal(api.Client, test.Client) 61 | } 62 | 63 | }) 64 | } 65 | } 66 | 67 | func TestVerifyTLS(t *testing.T) { 68 | assert := assert.New(t) 69 | api, err := NewAPI("https://test.test", "", "") 70 | assert.NoError(err) 71 | 72 | t.Run("set-true", func(t *testing.T) { 73 | api.VerifyTLS(true) 74 | assert.Equal(&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}}, api.Client.Transport) 75 | }) 76 | 77 | t.Run("set-false", func(t *testing.T) { 78 | api.VerifyTLS(false) 79 | assert.Equal(&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, api.Client.Transport) 80 | }) 81 | 82 | } 83 | 84 | func TestSetDebug(t *testing.T) { 85 | assert.False(t, DebugFlag) 86 | SetDebug(true) 87 | assert.True(t, DebugFlag) 88 | SetDebug(false) 89 | assert.False(t, DebugFlag) 90 | } 91 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Search results 10 | type Search struct { 11 | Results []Results `json:"results"` 12 | Start int `json:"start,omitempty"` 13 | Limit int `json:"limit,omitempty"` 14 | Size int `json:"size,omitempty"` 15 | TotalSize int `json:"totalSize,omitempty"` 16 | Links SearchLinks `json:"_links,omitempty"` 17 | } 18 | 19 | // Parsing out the _links section to allow paging etc. 20 | type SearchLinks struct { 21 | Base string `json:"base,omitempty"` 22 | Context string `json:"content,omitempty"` 23 | Next string `json:"next,omitempty"` 24 | Self string `json:"self,omitempty"` 25 | } 26 | 27 | // SearchQuery defines query parameters used for searchng 28 | // Query parameter values https://developer.atlassian.com/cloud/confluence/rest/#api-search-get 29 | type SearchQuery struct { 30 | CQL string 31 | CQLContext string 32 | IncludeArchivedSpaces bool 33 | Limit int 34 | Start int 35 | Expand []string 36 | } 37 | 38 | // getContentEndpoint creates the correct api endpoint by given id 39 | func (a *API) getSearchEndpoint() (*url.URL, error) { 40 | return url.ParseRequestURI(a.endPoint.String() + "/search") 41 | } 42 | 43 | // Search querys confluence using CQL 44 | func (a *API) Search(query SearchQuery) (*Search, error) { 45 | ep, err := a.getSearchEndpoint() 46 | if err != nil { 47 | return nil, err 48 | } 49 | ep.RawQuery = addSearchQueryParams(query).Encode() 50 | return a.SendSearchRequest(ep, "GET") 51 | } 52 | 53 | // Search querys confluence using CQL, with the ability to pass in the Next header 54 | // Empty Next header will run search like normal 55 | func (a *API) SearchWithNext(query SearchQuery, next string) (*Search, error) { 56 | if next == "" { 57 | return a.Search(query) 58 | } 59 | ep, err := url.ParseRequestURI(a.endPoint.String() + strings.TrimPrefix(next, "/rest/api")) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return a.SendSearchRequest(ep, "GET") 64 | } 65 | 66 | // addSearchQueryParams adds the defined query parameters 67 | func addSearchQueryParams(query SearchQuery) *url.Values { 68 | 69 | data := url.Values{} 70 | if query.CQL != "" { 71 | data.Set("cql", query.CQL) 72 | } 73 | if query.CQLContext != "" { 74 | data.Set("cqlcontext", query.CQLContext) 75 | } 76 | if query.IncludeArchivedSpaces { 77 | data.Set("includeArchivedSpaces", "true") 78 | } 79 | if query.Limit != 0 { 80 | data.Set("limit", strconv.Itoa(query.Limit)) 81 | } 82 | if query.Start != 0 { 83 | data.Set("start", strconv.Itoa(query.Start)) 84 | } 85 | if len(query.Expand) != 0 { 86 | data.Set("expand", strings.Join(query.Expand, ",")) 87 | } 88 | return &data 89 | } 90 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Template contains blueprint data 12 | type Template struct { 13 | ID string `json:"templateId,omitempty"` 14 | Name string `json:"name,omitempty"` 15 | Type string `json:"templateType,omitempty"` 16 | Description string `json:"description"` 17 | Body Body `json:"body"` 18 | Space Space `json:"space"` 19 | } 20 | 21 | // TemplateQuery defines the query parameters 22 | type TemplateQuery struct { 23 | SpaceKey string 24 | Start int // page start 25 | Limit int // page limit 26 | Expand []string 27 | } 28 | 29 | // TemplateSearch contains blueprint search results 30 | type TemplateSearch struct { 31 | Results []Template `json:"results"` 32 | Start int `json:"start,omitempty"` 33 | Limit int `json:"limit,omitempty"` 34 | Size int `json:"size,omitempty"` 35 | } 36 | 37 | func (a *API) getBlueprintTemplatesEndpoint() (*url.URL, error) { 38 | return url.ParseRequestURI(a.endPoint.String() + "/template/blueprint") 39 | } 40 | 41 | func (a *API) getContentTemplatesEndpoint() (*url.URL, error) { 42 | return url.ParseRequestURI(a.endPoint.String() + "/template/page") 43 | } 44 | 45 | // GetBlueprintTemplates querys for content blueprints defined by TemplateQuery parameters 46 | func (a *API) GetBlueprintTemplates(query TemplateQuery) (*TemplateSearch, error) { 47 | ep, err := a.getBlueprintTemplatesEndpoint() 48 | if err != nil { 49 | return nil, err 50 | } 51 | ep.RawQuery = addTemplateQueryParams(query).Encode() 52 | 53 | req, err := http.NewRequest("GET", ep.String(), nil) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | res, err := a.Request(req) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | var search TemplateSearch 64 | 65 | err = json.Unmarshal(res, &search) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &search, nil 70 | } 71 | 72 | // GetContentTemplates querys for content templates 73 | func (a *API) GetContentTemplates(query TemplateQuery) (*TemplateSearch, error) { 74 | ep, err := a.getContentTemplatesEndpoint() 75 | if err != nil { 76 | return nil, err 77 | } 78 | ep.RawQuery = addTemplateQueryParams(query).Encode() 79 | 80 | req, err := http.NewRequest("GET", ep.String(), nil) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | res, err := a.Request(req) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | var search TemplateSearch 91 | 92 | err = json.Unmarshal(res, &search) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return &search, nil 97 | } 98 | 99 | func addTemplateQueryParams(query TemplateQuery) *url.Values { 100 | data := url.Values{} 101 | if len(query.Expand) != 0 { 102 | data.Set("expand", strings.Join(query.Expand, ",")) 103 | } 104 | if query.Limit != 0 { 105 | data.Set("limit", strconv.Itoa(query.Limit)) 106 | } 107 | if query.SpaceKey != "" { 108 | data.Set("spaceKey", query.SpaceKey) 109 | } 110 | if len(query.Expand) != 0 { 111 | data.Set("expand", strings.Join(query.Expand, ",")) 112 | } 113 | if query.Start != 0 { 114 | data.Set("start", strconv.Itoa(query.Start)) 115 | } 116 | return &data 117 | } 118 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@casee.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # confluence-go-api 2 | 3 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VBXHBYFU44T5W&source=url) 4 | [![GoDoc](https://img.shields.io/badge/godoc-reference-green.svg)](https://godoc.org/github.com/virtomize/confluence-go-api) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/virtomize/confluence-go-api)](https://goreportcard.com/report/github.com/virtomize/confluence-go-api) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/virtomize/confluence-go-api/blob/master/LICENSE) 7 | [![Built with Mage](https://magefile.org/badge.svg)](https://magefile.org) 8 | 9 | 10 | is a [Confluence](https://www.atlassian.com/software/confluence) REST API client implementation written in [GOLANG](https://golang.org). 11 | 12 | ## Supported Features 13 | 14 | - get, update, delete content 15 | - get, update, delete content templates and blueprints 16 | - get comments, attachments, children of content objects, history, watchers 17 | - get, add ,delete labels 18 | - get user information 19 | - search using [CQL](https://developer.atlassian.com/cloud/confluence/advanced-searching-using-cql/) 20 | 21 | If you miss some feature implementation, feel free to open an issue or send pull requests. I will take look as soon as possible. 22 | 23 | ## Donation 24 | If this project helps you, feel free to give us a cup of coffee :). 25 | 26 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VBXHBYFU44T5W&source=url) 27 | 28 | ## Installation 29 | 30 | If you already installed GO on your system and configured it properly than its simply: 31 | 32 | ``` 33 | go get github.com/virtomize/confluence-go-api 34 | ``` 35 | 36 | If not follow [these instructions](https://golang.org/doc/install) 37 | 38 | ## Usage 39 | 40 | ### Simple example 41 | 42 | ``` 43 | package main 44 | 45 | import ( 46 | "fmt" 47 | "log" 48 | 49 | "github.com/virtomize/confluence-go-api" 50 | ) 51 | 52 | func main() { 53 | 54 | // initialize a new api instance 55 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | // get current user information 61 | currentUser, err := api.CurrentUser() 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | fmt.Printf("%+v\n", currentUser) 66 | } 67 | ``` 68 | 69 | 70 | ### Using a Personal Access Token 71 | 72 | To generate a confluence personal access token (PAT) see this article: [using personal access tokens](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html). Only set the token in the NewAPI function 73 | 74 | ``` 75 | api, err := goconfluence.NewAPI("https://.atlassian.net/wiki/rest/api", "", "") 76 | ``` 77 | 78 | ### Advanced examples 79 | 80 | see [examples](https://github.com/virtomize/confluence-go-api/tree/master/examples) for some more usage examples 81 | 82 | ## Code Documentation 83 | 84 | You find the full [code documentation here](https://godoc.org/github.com/virtomize/confluence-go-api). 85 | 86 | The Confluence API documentation [can be found here](https://docs.atlassian.com/ConfluenceServer/rest/6.9.1/). 87 | 88 | ## Contribution 89 | 90 | Thank you for participating to this project. 91 | Please see our [Contribution Guidlines](https://github.com/virtomize/confluence-go-api/blob/master/CONTRIBUTING.md) for more information. 92 | 93 | ### Pre-Commit 94 | 95 | This repo uses [pre-commit hooks](https://pre-commit.com/). Please install pre-commit and do `pre-commit install` 96 | 97 | ### Conventional Commits 98 | 99 | Format commit messaged according to [Conventional Commits standard](https://www.conventionalcommits.org/en/v1.0.0/). 100 | 101 | ### Semantic Versioning 102 | 103 | Whenever you need to version something make use of [Semantic Versioning](https://semver.org). 104 | 105 | 106 | -------------------------------------------------------------------------------- /content_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetContentEndpoints(t *testing.T) { 11 | a, err := NewAPI("https://test.test", "username", "token") 12 | assert.Nil(t, err) 13 | 14 | url, err := a.getContentIDEndpoint("test") 15 | assert.Nil(t, err) 16 | assert.Equal(t, "/content/test", url.Path) 17 | 18 | url, err = a.getContentEndpoint() 19 | assert.Nil(t, err) 20 | assert.Equal(t, "/content/", url.Path) 21 | 22 | url, err = a.getContentChildEndpoint("test", "child") 23 | assert.Nil(t, err) 24 | assert.Equal(t, "/content/test/child/child", url.Path) 25 | 26 | url, err = a.getContentGenericEndpoint("test", "child") 27 | assert.Nil(t, err) 28 | assert.Equal(t, "/content/test/child", url.Path) 29 | } 30 | 31 | func TestContentGetter(t *testing.T) { 32 | server := confluenceRestAPIStub() 33 | defer server.Close() 34 | 35 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 36 | assert.Nil(t, err) 37 | 38 | c, err := api.GetContentByID("42", ContentQuery{}) 39 | assert.Nil(t, err) 40 | assert.Equal(t, &Content{}, c) 41 | 42 | s, err := api.GetContent(ContentQuery{}) 43 | assert.Nil(t, err) 44 | assert.Equal(t, &ContentSearch{}, s) 45 | 46 | p, err := api.GetChildPages("42") 47 | assert.Nil(t, err) 48 | assert.Equal(t, &Search{}, p) 49 | 50 | p, err = api.GetComments("42") 51 | assert.Nil(t, err) 52 | assert.Equal(t, &Search{}, p) 53 | 54 | p, err = api.GetAttachments("42") 55 | assert.Nil(t, err) 56 | assert.Equal(t, &Search{}, p) 57 | 58 | h, err := api.GetHistory("42") 59 | assert.Nil(t, err) 60 | assert.Equal(t, &History{}, h) 61 | 62 | l, err := api.GetLabels("42") 63 | assert.Nil(t, err) 64 | assert.Equal(t, &Labels{}, l) 65 | 66 | w, err := api.GetWatchers("42") 67 | assert.Nil(t, err) 68 | assert.Equal(t, &Watchers{}, w) 69 | } 70 | 71 | func TestAddLabels(t *testing.T) { 72 | server := confluenceRestAPIStub() 73 | defer server.Close() 74 | 75 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 76 | assert.Nil(t, err) 77 | 78 | l, err := api.AddLabels("42", &[]Label{}) 79 | assert.Nil(t, err) 80 | assert.Equal(t, &Labels{}, l) 81 | } 82 | 83 | func TestDeleteLabels(t *testing.T) { 84 | server := confluenceRestAPIStub() 85 | defer server.Close() 86 | 87 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 88 | assert.Nil(t, err) 89 | 90 | l, err := api.DeleteLabel("42", "test") 91 | assert.Nil(t, err) 92 | assert.Equal(t, &Labels{}, l) 93 | } 94 | 95 | func TestContent(t *testing.T) { 96 | server := confluenceRestAPIStub() 97 | defer server.Close() 98 | 99 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 100 | assert.Nil(t, err) 101 | 102 | c, err := api.CreateContent(&Content{}) 103 | assert.Nil(t, err) 104 | assert.Equal(t, &Content{}, c) 105 | 106 | s, err := api.UploadAttachment("43", "attachmentName", strings.NewReader("attachment content")) 107 | assert.Nil(t, err) 108 | assert.Equal(t, &Search{}, s) 109 | 110 | s, err = api.UpdateAttachment("43", "attachmentName", "123", strings.NewReader("attachment content")) 111 | assert.Nil(t, err) 112 | assert.Equal(t, &Search{}, s) 113 | 114 | c, err = api.UpdateContent(&Content{}) 115 | assert.Nil(t, err) 116 | assert.Equal(t, &Content{}, c) 117 | 118 | c, err = api.DelContent("42") 119 | assert.Nil(t, err) 120 | assert.Equal(t, &Content{}, c) 121 | } 122 | 123 | func TestAddContentQueryParams(t *testing.T) { 124 | query := ContentQuery{ 125 | Expand: []string{"foo", "bar"}, 126 | Limit: 1, 127 | OrderBy: "test", 128 | PostingDay: "test", 129 | SpaceKey: "test", 130 | Start: 1, 131 | Status: "test", 132 | Title: "test", 133 | Trigger: "test", 134 | Type: "test", 135 | } 136 | 137 | p := addContentQueryParams(query) 138 | 139 | assert.Equal(t, p.Get("expand"), "foo,bar") 140 | assert.Equal(t, p.Get("limit"), "1") 141 | assert.Equal(t, p.Get("orderby"), "test") 142 | assert.Equal(t, p.Get("postingDay"), "test") 143 | assert.Equal(t, p.Get("spaceKey"), "test") 144 | assert.Equal(t, p.Get("start"), "1") 145 | assert.Equal(t, p.Get("status"), "test") 146 | assert.Equal(t, p.Get("title"), "test") 147 | assert.Equal(t, p.Get("trigger"), "test") 148 | assert.Equal(t, p.Get("type"), "test") 149 | } 150 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to confluence-go-api 2 | 3 | - [Code of Conduct](#code-fo-conduct) 4 | - [Issues](#issues) 5 | - [Pull Requests](#pull-requests) 6 | - [Reviewing](#reviewing) 7 | 8 | ## [Code of Conduct](https://github.com/virtomize/confluence-go-api/blob/master/CODE_OF_CONDUCT.md) 9 | 10 | The confluence-go-api project follows the [Contributor Covenant Code of Conduct](https://github.com/virtomize/confluence-go-api/blob/master/CODE_OF_CONDUCT.md) 11 | 12 | ## Issues 13 | 14 | ### How to Contribute in Issues 15 | 16 | You can contribute: 17 | 18 | - By opening an issue for discussion: For example, if you found a bug, creating a bug report using the [template](https://github.com/virtomize/confluence-go-api/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) is the way to report it. 19 | - By providing supporting details to an existing issue: For example, additional testcases. 20 | - By helping to resolve an issue: For example by opening a [Pull Request](https://github.com/virtomize/confluence-go-api/pulls) 21 | 22 | ### Asking for Help 23 | 24 | Just open a [regular issue](https://github.com/virtomize/confluence-go-api/issues/new) and describe your problem. 25 | 26 | ## Pull Requests 27 | 28 | ### Dependencies 29 | 30 | This project uses [Mage](https://magefile.org/) which is a replacement for the classical make. 31 | 32 | ### 1. Fork 33 | 34 | Fork the project [on Github](https://github.com/virtomize/confluence-go-api/) and clone your fork 35 | 36 | ``` 37 | $ git clone git@github.com:username/confluence-go-api.git 38 | $ cd confluence-go-api 39 | $ git remote add upstream https://github.com/virtomize/confluence-go-api.git 40 | $ git feth upstream 41 | ``` 42 | 43 | Also configure git to know who you are: 44 | 45 | ``` 46 | $ git config user.name "Jane Doe" 47 | $ git config user.email "j.doe@example.com" 48 | ``` 49 | 50 | ### 2. Branch 51 | 52 | Best practice is to organize your development environment by creating local branches to work within. 53 | These should also be created directly off the `master` branch. 54 | 55 | ``` 56 | $ git checkout -b example-branch -t upstream/master 57 | ``` 58 | 59 | ### 3. Code 60 | 61 | Follow the [official code guidelines](https://golang.org/doc/effective_go.html). 62 | 63 | To make sure the code runs correct, test the code using: 64 | 65 | ``` 66 | mage test 67 | ``` 68 | 69 | also add unit tests to test your code contributions. 70 | 71 | For new features add a short description in the README.md. 72 | 73 | ### 4. Commit 74 | 75 | It is a recommended best practice to keep your changes as logically grouped as possible within individual commits. 76 | There is no limit to the number of commits any single Pull Request may have, and many contributors find it easier to review changes that are split across multiple commits. 77 | 78 | ``` 79 | $ git add files/changed 80 | $ git commit 81 | ``` 82 | 83 | **Commit message guidelines** 84 | 85 | A good commit message should contain a short description what changed and why 86 | 87 | - The first line should contain a short description (not more than 72 characters) 88 | - e.g.: `additional filter added to filter mails by whatever` 89 | - if you fix open issues, add a reference to the issue 90 | - e.g.: `issue-1337: fixed ...` 91 | - if you commit a breaking change (see [semantic versioning](https://semver.org/)), the message should contain an explanation about the reason of the breaking change, what triggers the change and what the exact change is 92 | 93 | ### 5. Rebase 94 | 95 | As a best practice, once you have committed your changes, it is a good idea to use `git rebase` (not `git merge`) to synchronize your work with the main repository. 96 | 97 | ``` 98 | $ git fetch upstream 99 | $ git rebase upstream/master 100 | ``` 101 | 102 | This ensures that your working branch has the latest changes from master. 103 | 104 | ### 6. Push 105 | 106 | If your commits are ready to go and passed all tests and linting, you can start creating a [Pull Requests](https://github.com/virtomize/confluence-go-api/pulls) by pushing your work branch to your fork on GitHub. 107 | 108 | ``` 109 | $ git push origin example-branch 110 | ``` 111 | 112 | ### 7. Open the Pull Request 113 | 114 | From within GitHub, by opening a new Pull Request will present you with a template that should be filled out: 115 | 116 | ``` 117 | 125 | 126 | ##### Checklist 127 | 128 | 129 | - [ ] `mage test` passes 130 | - [ ] unit tests are included and tested 131 | - [ ] documentation is added or changed 132 | ``` 133 | 134 | Please fill out all details, feel free to skip not nessesary parts or if you're not sure what to fill in. 135 | 136 | Once opened, the Pull Request is opend and will be reviewed. 137 | 138 | ### 8. Updates and discussion 139 | 140 | While reviewing you will probably get some feedback or requests for changes to your Pull Request. This is normal and a necessary part of the process to evaluate the changes and there correctness. 141 | 142 | To make changes to an existsing Pull Request, make the changes to your local branch. 143 | Add a new commit including those changes and push them to your fork. 144 | The Pull Requests will automatically updated by GitHub. 145 | 146 | ``` 147 | $ git add files/changed 148 | $ git commit 149 | $ git push origin example-branch 150 | ``` 151 | **Approvement and Changes** 152 | 153 | Whenever a contributor reviews a Pull Request they may find specific details that they would like to see changed or fixed. 154 | These may be as simple as fixing a typo, or may involve substantive changes to the code you have written. 155 | While such requests are intended to be helpful, they may come across as abrupt or unhelpful, especially requests to change things that do not include concrete suggestions on how to change them. 156 | 157 | Try not to be discouraged. 158 | If you feel that a particular review is unfair, say so, or contact one of the other contributors in the project and seek their input. 159 | Often such comments are the result of the reviewer having only taken a short amount of time to review and are not ill-intended. 160 | Such issues can often be resolved with a bit of patience. 161 | That said, reviewers should be expected to be helpful in their feedback, and feedback that is simply vague, dismissive and unhelpful is likely safe to ignore. 162 | 163 | ## Reviewing 164 | 165 | Reviews and feedback must be helpful, insightful, and geared towards improving the contribution as opposed to simply blocking it. 166 | If there are reasons why you feel the PR should not be accepted, explain what those are. 167 | Be open to having your mind changed. 168 | Be open to working with the contributor to make the Pull Request better. 169 | 170 | Also follow the [Code of Conduct](https://github.com/virtomize/confluence-go-api/blob/master/CODE_OF_CONDUCT.md) 171 | 172 | When reviewing a Pull Request, the primary goals are for the codebase to improve and for the person submitting the request to succeed. 173 | Even if a Pull Request is not accepted, the submitters should come away from the experience feeling like their effort was not wasted or unappreciated. 174 | Every Pull Request from a new contributor is an opportunity to grow the community. 175 | 176 | Be aware that **how** you communicate requests and reviews in your feedback can have a significant impact on the success of the Pull Request. 177 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | "strings" 14 | ) 15 | 16 | // Request implements the basic Request function 17 | func (a *API) Request(req *http.Request) ([]byte, error) { 18 | req.Header.Add("Accept", "application/json, */*") 19 | 20 | // only auth if we can auth 21 | if (a.username != "") || (a.token != "") { 22 | a.Auth(req) 23 | } 24 | 25 | Debug("====== Request ======") 26 | Debug(req) 27 | Debug("====== Request Body ======") 28 | if DebugFlag { 29 | requestDump, err := httputil.DumpRequest(req, true) 30 | if err != nil { 31 | fmt.Println(err) 32 | } 33 | fmt.Println(string(requestDump)) 34 | } 35 | Debug("====== /Request Body ======") 36 | Debug("====== /Request ======") 37 | 38 | resp, err := a.Client.Do(req) 39 | if err != nil { 40 | return nil, err 41 | } 42 | Debug(fmt.Sprintf("====== Response Status Code: %d ======", resp.StatusCode)) 43 | 44 | res, err := ioutil.ReadAll(resp.Body) 45 | if err != nil { 46 | return nil, err 47 | } 48 | err = resp.Body.Close() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | Debug("====== Response Body ======") 54 | Debug(string(res)) 55 | Debug("====== /Response Body ======") 56 | 57 | switch resp.StatusCode { 58 | case http.StatusOK, http.StatusCreated, http.StatusPartialContent: 59 | return res, nil 60 | case http.StatusNoContent, http.StatusResetContent: 61 | return nil, nil 62 | case http.StatusUnauthorized: 63 | return nil, fmt.Errorf("authentication failed") 64 | case http.StatusServiceUnavailable: 65 | return nil, fmt.Errorf("service is not available: %s", resp.Status) 66 | case http.StatusInternalServerError: 67 | return nil, fmt.Errorf("internal server error: %s", resp.Status) 68 | case http.StatusConflict: 69 | return nil, fmt.Errorf("conflict: %s", resp.Status) 70 | } 71 | 72 | return nil, fmt.Errorf("unknown response status: %s", resp.Status) 73 | } 74 | 75 | // SendContentRequest sends content related requests 76 | // this function is used for getting, updating and deleting content 77 | func (a *API) SendContentRequest(ep *url.URL, method string, c *Content) (*Content, error) { 78 | 79 | var body io.Reader 80 | if c != nil { 81 | js, err := json.Marshal(c) 82 | if err != nil { 83 | return nil, err 84 | } 85 | body = strings.NewReader(string(js)) 86 | } 87 | 88 | req, err := http.NewRequest(method, ep.String(), body) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | if body != nil { 94 | req.Header.Add("Content-Type", "application/json") 95 | } 96 | 97 | res, err := a.Request(req) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | var content Content 103 | if len(res) != 0 { 104 | err = json.Unmarshal(res, &content) 105 | if err != nil { 106 | return nil, err 107 | } 108 | } 109 | return &content, nil 110 | } 111 | 112 | // SendContentAttachmentRequest sends a multipart/form-data attachment create/update request to a content 113 | func (a *API) SendContentAttachmentRequest(ep *url.URL, attachmentName string, attachment io.Reader, params map[string]string) (*Search, error) { 114 | // setup body for mulitpart file, adding minorEdit option 115 | body := &bytes.Buffer{} 116 | writer := multipart.NewWriter(body) 117 | err := writer.WriteField("minorEdit", "true") 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | part, err := writer.CreateFormFile("file", attachmentName) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | // add attachment to body 128 | _, err = io.Copy(part, attachment) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | // add any other params 134 | for key, val := range params { 135 | _ = writer.WriteField(key, val) 136 | } 137 | 138 | //clean up multipart form writer 139 | err = writer.Close() 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | req, err := http.NewRequest("POST", ep.String(), body) // will always be put 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | req.Header.Set("X-Atlassian-Token", "nocheck") // required by api 150 | req.Header.Set("Content-Type", writer.FormDataContentType()) 151 | // https://developer.atlassian.com/cloud/confluence/rest/#api-api-content-id-child-attachment-put 152 | 153 | res, err := a.Request(req) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | var search Search 159 | 160 | err = json.Unmarshal(res, &search) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return &search, nil 166 | } 167 | 168 | // SendUserRequest sends user related requests 169 | func (a *API) SendUserRequest(ep *url.URL, method string) (*User, error) { 170 | 171 | req, err := http.NewRequest(method, ep.String(), nil) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | res, err := a.Request(req) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | var user User 182 | 183 | err = json.Unmarshal(res, &user) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | return &user, nil 189 | } 190 | 191 | // SendSearchRequest sends search related requests 192 | func (a *API) SendSearchRequest(ep *url.URL, method string) (*Search, error) { 193 | 194 | req, err := http.NewRequest(method, ep.String(), nil) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | res, err := a.Request(req) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | var search Search 205 | 206 | err = json.Unmarshal(res, &search) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | return &search, nil 212 | } 213 | 214 | // SendHistoryRequest requests history 215 | func (a *API) SendHistoryRequest(ep *url.URL, method string) (*History, error) { 216 | 217 | req, err := http.NewRequest(method, ep.String(), nil) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | res, err := a.Request(req) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | var history History 228 | 229 | err = json.Unmarshal(res, &history) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return &history, nil 235 | } 236 | 237 | // SendLabelRequest requests history 238 | func (a *API) SendLabelRequest(ep *url.URL, method string, labels *[]Label) (*Labels, error) { 239 | 240 | var body io.Reader 241 | if labels != nil { 242 | js, err := json.Marshal(labels) 243 | if err != nil { 244 | return nil, err 245 | } 246 | body = strings.NewReader(string(js)) 247 | } 248 | 249 | req, err := http.NewRequest(method, ep.String(), body) 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | if body != nil { 255 | req.Header.Add("Content-Type", "application/json") 256 | } 257 | 258 | res, err := a.Request(req) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | if res != nil { 264 | var l Labels 265 | 266 | err = json.Unmarshal(res, &l) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | return &l, nil 272 | } 273 | 274 | return &Labels{}, nil 275 | } 276 | 277 | // SendWatcherRequest requests watchers 278 | func (a *API) SendWatcherRequest(ep *url.URL, method string) (*Watchers, error) { 279 | 280 | req, err := http.NewRequest(method, ep.String(), nil) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | res, err := a.Request(req) 286 | if err != nil { 287 | return nil, err 288 | } 289 | var watchers Watchers 290 | 291 | err = json.Unmarshal(res, &watchers) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | return &watchers, nil 297 | } 298 | 299 | // SendAllSpacesRequest sends a request for all spaces 300 | func (a *API) SendAllSpacesRequest(ep *url.URL, method string) (*AllSpaces, error) { 301 | 302 | req, err := http.NewRequest(method, ep.String(), nil) 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | res, err := a.Request(req) 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | var allSpaces AllSpaces 313 | 314 | err = json.Unmarshal(res, &allSpaces) 315 | if err != nil { 316 | return nil, err 317 | } 318 | 319 | return &allSpaces, nil 320 | } 321 | 322 | // SendContentVersionRequest requests a version of a specific content 323 | func (a *API) SendContentVersionRequest(ep *url.URL, method string) (*ContentVersionResult, error) { 324 | 325 | req, err := http.NewRequest(method, ep.String(), nil) 326 | if err != nil { 327 | return nil, err 328 | } 329 | 330 | res, err := a.Request(req) 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | var versionResult ContentVersionResult 336 | 337 | err = json.Unmarshal(res, &versionResult) 338 | if err != nil { 339 | return nil, err 340 | } 341 | 342 | return &versionResult, nil 343 | } 344 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | type testValuesRequest struct { 16 | Endpoint string 17 | Body string 18 | Error error 19 | } 20 | 21 | func TestRequest(t *testing.T) { 22 | server := confluenceRestAPIStub() 23 | defer server.Close() 24 | 25 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 26 | assert.Nil(t, err) 27 | 28 | testValues := []testValuesRequest{ 29 | {"/test", "\"test\"", nil}, 30 | {"/nocontent", "", nil}, 31 | {"/noauth", "", fmt.Errorf("authentication failed")}, 32 | {"/noservice", "", fmt.Errorf("service is not available: 503 Service Unavailable")}, 33 | {"/internalerror", "", fmt.Errorf("internal server error: 500 Internal Server Error")}, 34 | {"/unknown", "", fmt.Errorf("unknown response status: 408 Request Timeout")}, 35 | } 36 | 37 | for _, test := range testValues { 38 | 39 | req, err := http.NewRequest("GET", api.endPoint.String()+test.Endpoint, nil) 40 | assert.Nil(t, err) 41 | 42 | b, err := api.Request(req) 43 | if test.Error == nil { 44 | assert.Nil(t, err) 45 | } else { 46 | assert.Equal(t, test.Error.Error(), err.Error()) 47 | } 48 | 49 | assert.Equal(t, string(b), test.Body) 50 | } 51 | } 52 | 53 | type testValuesContentRequest struct { 54 | Content *Content 55 | Method string 56 | Body *Content 57 | Error error 58 | } 59 | 60 | func TestSendContentRequest(t *testing.T) { 61 | server := confluenceRestAPIStub() 62 | defer server.Close() 63 | 64 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 65 | assert.Nil(t, err) 66 | 67 | ep, err := api.getContentEndpoint() 68 | assert.Nil(t, err) 69 | 70 | testValues := []testValuesContentRequest{ 71 | {nil, "GET", &Content{}, nil}, 72 | {&Content{}, "GET", &Content{}, nil}, 73 | } 74 | 75 | for _, test := range testValues { 76 | b, err := api.SendContentRequest(ep, test.Method, test.Content) 77 | if test.Error == nil { 78 | assert.Nil(t, err) 79 | } else { 80 | assert.Equal(t, test.Error.Error(), err.Error) 81 | } 82 | assert.Equal(t, test.Body, b) 83 | } 84 | } 85 | 86 | func TestSendContentRequestToken(t *testing.T) { 87 | server := confluenceRestAPIStub() 88 | defer server.Close() 89 | 90 | api, err := NewAPI(server.URL+"/wiki/rest/api", "", "token") 91 | assert.Nil(t, err) 92 | 93 | ep, err := api.getContentEndpoint() 94 | assert.Nil(t, err) 95 | 96 | testValues := []testValuesContentRequest{ 97 | {nil, "GET", &Content{}, nil}, 98 | {&Content{}, "GET", &Content{}, nil}, 99 | } 100 | 101 | for _, test := range testValues { 102 | b, err := api.SendContentRequest(ep, test.Method, test.Content) 103 | if test.Error == nil { 104 | assert.Nil(t, err) 105 | } else { 106 | assert.Equal(t, test.Error.Error(), err.Error) 107 | } 108 | assert.Equal(t, test.Body, b) 109 | } 110 | } 111 | 112 | type testValuesAttachmentRequest struct { 113 | AttachmentName string 114 | Attachment io.Reader 115 | Params map[string]string 116 | Error error 117 | } 118 | 119 | func TestSendContentAttachmentRequest(t *testing.T) { 120 | server := confluenceRestAPIStub() 121 | defer server.Close() 122 | 123 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 124 | assert.Nil(t, err) 125 | 126 | ep, err := api.getContentChildEndpoint("43", "attachment") 127 | assert.Nil(t, err) 128 | 129 | r1 := strings.NewReader("some test file attachment, normally this would come from a file") 130 | p1 := map[string]string{"comment": "some witty comment"} 131 | r2 := strings.NewReader("an even better attachment\nwith new lines in it") 132 | p2 := map[string]string{"comment": "some other witty comment"} 133 | testValues := []testValuesAttachmentRequest{ 134 | {"my awesome attachment", r1, p1, nil}, 135 | {"is awesome", r2, p2, nil}, 136 | } 137 | 138 | for _, test := range testValues { 139 | _, err := api.SendContentAttachmentRequest(ep, test.AttachmentName, test.Attachment, test.Params) 140 | if test.Error == nil { 141 | assert.Nil(t, err) 142 | } else { 143 | assert.Equal(t, test.Error.Error(), err.Error) 144 | } 145 | } 146 | } 147 | 148 | type testValuesUserRequest struct { 149 | Method string 150 | Body *User 151 | Error error 152 | } 153 | 154 | func TestSendUserRequest(t *testing.T) { 155 | server := confluenceRestAPIStub() 156 | defer server.Close() 157 | 158 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 159 | assert.Nil(t, err) 160 | 161 | ep, err := api.getUserEndpoint("42") 162 | assert.Nil(t, err) 163 | 164 | testValues := []testValuesUserRequest{ 165 | {"GET", &User{}, nil}, 166 | } 167 | 168 | for _, test := range testValues { 169 | b, err := api.SendUserRequest(ep, test.Method) 170 | if test.Error == nil { 171 | assert.Nil(t, err) 172 | } else { 173 | assert.Equal(t, test.Error.Error(), err.Error) 174 | } 175 | assert.Equal(t, test.Body, b) 176 | } 177 | } 178 | 179 | type testValuesSearchRequest struct { 180 | Method string 181 | Body *Search 182 | Error error 183 | } 184 | 185 | func TestSendSearchRequest(t *testing.T) { 186 | server := confluenceRestAPIStub() 187 | defer server.Close() 188 | 189 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 190 | assert.Nil(t, err) 191 | 192 | ep, err := api.getSearchEndpoint() 193 | assert.Nil(t, err) 194 | 195 | testValues := []testValuesSearchRequest{ 196 | {"GET", &Search{}, nil}, 197 | } 198 | 199 | for _, test := range testValues { 200 | b, err := api.SendSearchRequest(ep, test.Method) 201 | if test.Error == nil { 202 | assert.Nil(t, err) 203 | } else { 204 | assert.Equal(t, test.Error.Error(), err.Error) 205 | } 206 | assert.Equal(t, test.Body, b) 207 | } 208 | } 209 | 210 | type testValuesHistoryRequest struct { 211 | Method string 212 | Body *History 213 | Error error 214 | } 215 | 216 | func TestSendHistoryRequest(t *testing.T) { 217 | server := confluenceRestAPIStub() 218 | defer server.Close() 219 | 220 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 221 | assert.Nil(t, err) 222 | 223 | ep, err := api.getContentGenericEndpoint("42", "history") 224 | assert.Nil(t, err) 225 | 226 | testValues := []testValuesHistoryRequest{ 227 | {"GET", &History{}, nil}, 228 | } 229 | 230 | for _, test := range testValues { 231 | b, err := api.SendHistoryRequest(ep, test.Method) 232 | if test.Error == nil { 233 | assert.Nil(t, err) 234 | } else { 235 | assert.Equal(t, test.Error.Error(), err.Error) 236 | } 237 | assert.Equal(t, test.Body, b) 238 | } 239 | } 240 | 241 | type testValuesLabelRequest struct { 242 | Method string 243 | Body *Labels 244 | Error error 245 | Labels *[]Label 246 | } 247 | 248 | func TestSendLabelRequest(t *testing.T) { 249 | server := confluenceRestAPIStub() 250 | defer server.Close() 251 | 252 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 253 | assert.Nil(t, err) 254 | 255 | ep, err := api.getContentGenericEndpoint("42", "label") 256 | assert.Nil(t, err) 257 | 258 | testValues := []testValuesLabelRequest{ 259 | {"GET", &Labels{}, nil, &[]Label{}}, 260 | } 261 | 262 | for _, test := range testValues { 263 | b, err := api.SendLabelRequest(ep, test.Method, test.Labels) 264 | if test.Error == nil { 265 | assert.Nil(t, err) 266 | } else { 267 | assert.Equal(t, test.Error.Error(), err.Error) 268 | } 269 | assert.Equal(t, test.Body, b) 270 | } 271 | } 272 | 273 | type testValuesWatcherRequest struct { 274 | Method string 275 | Body *Watchers 276 | Error error 277 | } 278 | 279 | func TestSendWatcherRequest(t *testing.T) { 280 | server := confluenceRestAPIStub() 281 | defer server.Close() 282 | 283 | api, err := NewAPI(server.URL+"/wiki/rest/api", "userame", "token") 284 | assert.Nil(t, err) 285 | 286 | ep, err := api.getContentGenericEndpoint("42", "notification/child-created") 287 | assert.Nil(t, err) 288 | 289 | testValues := []testValuesWatcherRequest{ 290 | {"GET", &Watchers{}, nil}, 291 | } 292 | 293 | for _, test := range testValues { 294 | b, err := api.SendWatcherRequest(ep, test.Method) 295 | if test.Error == nil { 296 | assert.Nil(t, err) 297 | } else { 298 | assert.Equal(t, test.Error.Error(), err.Error) 299 | } 300 | assert.Equal(t, test.Body, b) 301 | } 302 | } 303 | 304 | func confluenceRestAPIStub() *httptest.Server { 305 | var resp interface{} 306 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 307 | switch r.RequestURI { 308 | case "/wiki/rest/api/test": 309 | resp = "test" 310 | case "/wiki/rest/api/noauth": 311 | http.Error(w, "unauthorized", http.StatusUnauthorized) 312 | return 313 | case "/wiki/rest/api/nocontent": 314 | http.Error(w, "", http.StatusNoContent) 315 | return 316 | case "/wiki/rest/api/noservice": 317 | http.Error(w, "", http.StatusServiceUnavailable) 318 | return 319 | case "/wiki/rest/api/internalerror": 320 | http.Error(w, "", http.StatusInternalServerError) 321 | return 322 | case "/wiki/rest/api/unknown": 323 | http.Error(w, "", http.StatusRequestTimeout) 324 | return 325 | case "/wiki/rest/api/content/": 326 | resp = Content{} 327 | case "/wiki/rest/api/content/42": 328 | if r.Method == "DELETE" { 329 | resp = "" 330 | w.WriteHeader(http.StatusNoContent) 331 | } else { 332 | resp = Content{} 333 | } 334 | case "/wiki/rest/api/user?username=42": 335 | resp = User{} 336 | case "/wiki/rest/api/user?accountId=42": 337 | resp = User{} 338 | case "/wiki/rest/api/user?key=42": 339 | resp = User{} 340 | case "/wiki/rest/api/user/current": 341 | resp = User{} 342 | case "/wiki/rest/api/user/anonymous": 343 | resp = User{} 344 | case "/wiki/rest/api/search": 345 | resp = Search{} 346 | case "/wiki/rest/api/search?next=true&cursor=abc123": 347 | resp = Search{} 348 | case "/wiki/rest/api/content/42/history": 349 | resp = History{} 350 | case "/wiki/rest/api/content/42/label": 351 | resp = Labels{} 352 | case "/wiki/rest/api/content/42/label/test": 353 | resp = Labels{} 354 | case "/wiki/rest/api/content/42/notification/child-created": 355 | resp = Watchers{} 356 | case "/wiki/rest/api/content/42/child/page": 357 | resp = Search{} 358 | case "/wiki/rest/api/content/42/child/page?limit=25": 359 | resp = Search{} 360 | case "/wiki/rest/api/content/42/child/attachment": 361 | resp = Search{} 362 | case "/wiki/rest/api/content/43/child/attachment/123/data": 363 | resp = Search{} 364 | case "/wiki/rest/api/content/43/child/attachment": 365 | resp = Content{} 366 | case "/wiki/rest/api/content/42/child/comment": 367 | resp = Search{} 368 | case "/wiki/rest/api/content/42/child/history": 369 | resp = Search{} 370 | case "/wiki/rest/api/content/42/child/label": 371 | resp = Search{} 372 | case "/wiki/rest/api/space": 373 | resp = AllSpaces{} 374 | case "/wiki/rest/api/template/blueprint": 375 | resp = TemplateSearch{} 376 | case "/wiki/rest/api/template/page": 377 | resp = TemplateSearch{} 378 | case "/wiki/api/v2/attachments/2495990589": 379 | resp = Attachment{} 380 | default: 381 | http.Error(w, "not found", http.StatusNotFound) 382 | return 383 | } 384 | b, err := json.Marshal(resp) 385 | if err != nil { 386 | http.Error(w, string(b), http.StatusInternalServerError) 387 | return 388 | } 389 | _, _ = w.Write(b) 390 | })) 391 | } 392 | -------------------------------------------------------------------------------- /content.go: -------------------------------------------------------------------------------- 1 | package goconfluence 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Results array 13 | type Results struct { 14 | ID string `json:"id,omitempty"` 15 | Type string `json:"type,omitempty"` 16 | Status string `json:"status,omitempty"` 17 | Content Content `json:"content"` 18 | Excerpt string `json:"excerpt,omitempty"` 19 | Title string `json:"title,omitempty"` 20 | URL string `json:"url,omitempty"` 21 | } 22 | 23 | // Content specifies content properties 24 | type Content struct { 25 | ID string `json:"id,omitempty"` 26 | Type string `json:"type"` 27 | Status string `json:"status,omitempty"` 28 | Title string `json:"title"` 29 | Ancestors []Ancestor `json:"ancestors,omitempty"` 30 | Body Body `json:"body"` 31 | Version *Version `json:"version,omitempty"` 32 | Space *Space `json:"space"` 33 | History *History `json:"history,omitempty"` 34 | Links *Links `json:"_links,omitempty"` 35 | Metadata *Metadata `json:"metadata,omitempty"` 36 | } 37 | 38 | // Metadata specifies metadata properties 39 | type Metadata struct { 40 | Properties *Properties `json:"properties"` 41 | } 42 | 43 | // Properties defines properties of the editor 44 | type Properties struct { 45 | Editor *Editor `json:"editor,omitempty"` 46 | ContentAppearanceDraft *ContentAppearanceDraft `json:"content-appearance-draft"` 47 | ContentAppearancePublished *ContentAppearancePublished `json:"content-appearance-published"` 48 | } 49 | 50 | // Editor contains editor information 51 | type Editor struct { 52 | Key string `json:"key"` 53 | Value string `json:"value"` 54 | } 55 | 56 | // ContentAppearanceDraft sets the appearance of the content in draft form 57 | type ContentAppearanceDraft struct { 58 | Value string `json:"value"` 59 | } 60 | 61 | // ContentAppearancePublished sets the appearance of the content in published form 62 | type ContentAppearancePublished struct { 63 | Value string `json:"value"` 64 | } 65 | 66 | // Links contains link information 67 | type Links struct { 68 | Base string `json:"base"` 69 | TinyUI string `json:"tinyui"` 70 | WebUI string `json:"webui"` 71 | Download string `json:"download"` 72 | } 73 | 74 | // Ancestor defines ancestors to create sub pages 75 | type Ancestor struct { 76 | ID string `json:"id"` 77 | } 78 | 79 | // Body holds the storage information 80 | type Body struct { 81 | Storage Storage `json:"storage"` 82 | View *Storage `json:"view,omitempty"` 83 | } 84 | 85 | // BodyExportView holds the export_view information 86 | type BodyExportView struct { 87 | ExportView *Storage `json:"export_view"` 88 | View *Storage `json:"view,omitempty"` 89 | } 90 | 91 | // Storage defines the storage information 92 | type Storage struct { 93 | Value string `json:"value"` 94 | Representation string `json:"representation"` 95 | } 96 | 97 | // Version defines the content version number 98 | // the version number is used for updating content 99 | type Version struct { 100 | Number int `json:"number"` 101 | MinorEdit bool `json:"minorEdit"` 102 | Message string `json:"message,omitempty"` 103 | By *User `json:"by,omitempty"` 104 | When string `json:"when,omitempty"` 105 | } 106 | 107 | // Space holds the Space information of a Content Page 108 | type Space struct { 109 | ID int `json:"id,omitempty"` 110 | Key string `json:"key,omitempty"` 111 | Name string `json:"name,omitempty"` 112 | Type string `json:"type,omitempty"` 113 | Status string `json:"status,omitempty"` 114 | } 115 | 116 | // getContentIDEndpoint creates the correct api endpoint by given id 117 | func (a *API) getContentIDEndpoint(id string) (*url.URL, error) { 118 | return url.ParseRequestURI(a.endPoint.String() + "/content/" + id) 119 | } 120 | 121 | // getContentEndpoint creates the correct api endpoint 122 | func (a *API) getContentEndpoint() (*url.URL, error) { 123 | return url.ParseRequestURI(a.endPoint.String() + "/content/") 124 | } 125 | 126 | // getContentChildEndpoint creates the correct api endpoint by given id and type 127 | func (a *API) getContentChildEndpoint(id string, t string) (*url.URL, error) { 128 | return url.ParseRequestURI(a.endPoint.String() + "/content/" + id + "/child/" + t) 129 | } 130 | 131 | // getContentGenericEndpoint creates the correct api endpoint by given id and type 132 | func (a *API) getContentGenericEndpoint(id string, t string) (*url.URL, error) { 133 | return url.ParseRequestURI(a.endPoint.String() + "/content/" + id + "/" + t) 134 | } 135 | 136 | // ContentQuery defines the query parameters 137 | // used for content related searching 138 | // Query parameter values https://developer.atlassian.com/cloud/confluence/rest/#api-content-get 139 | type ContentQuery struct { 140 | Expand []string 141 | Limit int // page limit 142 | OrderBy string // fieldpath asc/desc e.g: "history.createdDate desc" 143 | PostingDay string // required for blogpost type Format: yyyy-mm-dd 144 | SpaceKey string 145 | Start int // page start 146 | Status string // current, trashed, draft, any 147 | Title string // required for page 148 | Trigger string // viewed 149 | Type string // page, blogpost 150 | Version int //version number when not lastest 151 | 152 | } 153 | 154 | // GetContentByID querys content by id 155 | func (a *API) GetContentByID(id string, query ContentQuery) (*Content, error) { 156 | ep, err := a.getContentIDEndpoint(id) 157 | if err != nil { 158 | return nil, err 159 | } 160 | ep.RawQuery = addContentQueryParams(query).Encode() 161 | return a.SendContentRequest(ep, "GET", nil) 162 | } 163 | 164 | // ContentSearch results 165 | type ContentSearch struct { 166 | Results []Content `json:"results"` 167 | Start int `json:"start,omitempty"` 168 | Limit int `json:"limit,omitempty"` 169 | Size int `json:"size,omitempty"` 170 | } 171 | 172 | // GetContent querys content using a query parameters 173 | func (a *API) GetContent(query ContentQuery) (*ContentSearch, error) { 174 | ep, err := a.getContentEndpoint() 175 | if err != nil { 176 | return nil, err 177 | } 178 | ep.RawQuery = addContentQueryParams(query).Encode() 179 | 180 | req, err := http.NewRequest("GET", ep.String(), nil) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | res, err := a.Request(req) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | var search ContentSearch 191 | 192 | err = json.Unmarshal(res, &search) 193 | if err != nil { 194 | return nil, err 195 | } 196 | return &search, nil 197 | } 198 | 199 | // GetChildPages returns a content list of child page objects 200 | func (a *API) GetChildPages(id string) (*Search, error) { 201 | var ( 202 | results []Results 203 | searchResult Search 204 | ) 205 | 206 | ep, err := a.getContentChildEndpoint(id, "page") 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | query := ContentQuery{ 212 | Start: 0, 213 | Limit: 25, 214 | } 215 | 216 | searchResult.Start = 0 217 | 218 | for { 219 | ep.RawQuery = addContentQueryParams(query).Encode() 220 | s, err := a.SendSearchRequest(ep, "GET") 221 | if err != nil { 222 | return nil, err 223 | } 224 | results = append(results, s.Results...) 225 | if len(s.Results) < query.Limit { 226 | break 227 | } 228 | query.Start += 25 229 | } 230 | 231 | searchResult.Limit = len(results) 232 | searchResult.Size = len(results) 233 | searchResult.Results = results 234 | 235 | return &searchResult, nil 236 | } 237 | 238 | // GetComments returns a list of comments belonging to id 239 | func (a *API) GetComments(id string) (*Search, error) { 240 | ep, err := a.getContentChildEndpoint(id, "comment") 241 | if err != nil { 242 | return nil, err 243 | } 244 | return a.SendSearchRequest(ep, "GET") 245 | } 246 | 247 | // GetAttachments returns a list of attachments belonging to id 248 | func (a *API) GetAttachments(id string) (*Search, error) { 249 | ep, err := a.getContentChildEndpoint(id, "attachment") 250 | if err != nil { 251 | return nil, err 252 | } 253 | return a.SendSearchRequest(ep, "GET") 254 | } 255 | 256 | // History contains object history information 257 | type History struct { 258 | LastUpdated LastUpdated `json:"lastUpdated"` 259 | Latest bool `json:"latest"` 260 | CreatedBy User `json:"createdBy"` 261 | CreatedDate string `json:"createdDate"` 262 | } 263 | 264 | // LastUpdated contains information about the last update 265 | type LastUpdated struct { 266 | By User `json:"by"` 267 | When string `json:"when"` 268 | FriendlyWhen string `json:"friendlyWhen"` 269 | Message string `json:"message"` 270 | Number int `json:"number"` 271 | MinorEdit bool `json:"minorEdit"` 272 | SyncRev string `json:"syncRev"` 273 | ConfRev string `json:"confRev"` 274 | } 275 | 276 | // GetHistory returns history information 277 | func (a *API) GetHistory(id string) (*History, error) { 278 | ep, err := a.getContentGenericEndpoint(id, "history") 279 | if err != nil { 280 | return nil, err 281 | } 282 | return a.SendHistoryRequest(ep, "GET") 283 | } 284 | 285 | // Labels is the label containter type 286 | type Labels struct { 287 | Labels []Label `json:"results"` 288 | Start int `json:"start,omitempty"` 289 | Limit int `json:"limit,omitempty"` 290 | Size int `json:"size,omitempty"` 291 | } 292 | 293 | // Label contains label information 294 | type Label struct { 295 | Prefix string `json:"prefix"` 296 | Name string `json:"name"` 297 | ID string `json:"id,omitempty"` 298 | Label string `json:"label,omitempty"` 299 | } 300 | 301 | // GetLabels returns a list of labels attachted to a content object 302 | func (a *API) GetLabels(id string) (*Labels, error) { 303 | ep, err := a.getContentGenericEndpoint(id, "label") 304 | if err != nil { 305 | return nil, err 306 | } 307 | return a.SendLabelRequest(ep, "GET", nil) 308 | } 309 | 310 | // AddLabels returns adds labels 311 | func (a *API) AddLabels(id string, labels *[]Label) (*Labels, error) { 312 | ep, err := a.getContentGenericEndpoint(id, "label") 313 | if err != nil { 314 | return nil, err 315 | } 316 | return a.SendLabelRequest(ep, "POST", labels) 317 | } 318 | 319 | // DeleteLabel removes a label by name from content identified by id 320 | func (a *API) DeleteLabel(id string, name string) (*Labels, error) { 321 | ep, err := a.getContentGenericEndpoint(id, "label/"+name) 322 | if err != nil { 323 | return nil, err 324 | } 325 | return a.SendLabelRequest(ep, "DELETE", nil) 326 | } 327 | 328 | // Watchers is a list of Watcher 329 | type Watchers struct { 330 | Watchers []Watcher `json:"results"` 331 | Start int `json:"start,omitempty"` 332 | Limit int `json:"limit,omitempty"` 333 | Size int `json:"size,omitempty"` 334 | } 335 | 336 | // Watcher contains information about watching users of a page 337 | type Watcher struct { 338 | Type string `json:"type"` 339 | Watcher User `json:"watcher"` 340 | ContentID int `json:"contentId"` 341 | } 342 | 343 | // GetWatchers returns a list of watchers 344 | func (a *API) GetWatchers(id string) (*Watchers, error) { 345 | ep, err := a.getContentGenericEndpoint(id, "notification/child-created") 346 | if err != nil { 347 | return nil, err 348 | } 349 | return a.SendWatcherRequest(ep, "GET") 350 | } 351 | 352 | // CreateContent creates content 353 | func (a *API) CreateContent(c *Content) (*Content, error) { 354 | ep, err := a.getContentEndpoint() 355 | if err != nil { 356 | return nil, err 357 | } 358 | return a.SendContentRequest(ep, "POST", c) 359 | } 360 | 361 | // UpdateContent updates content 362 | func (a *API) UpdateContent(c *Content) (*Content, error) { 363 | ep, err := a.getContentIDEndpoint(c.ID) 364 | if err != nil { 365 | return nil, err 366 | } 367 | return a.SendContentRequest(ep, "PUT", c) 368 | } 369 | 370 | // UploadAttachment uploaded the given reader as an attachment to the 371 | // page with the given id. The existing attachment won't be updated with 372 | // a new version number 373 | func (a *API) UploadAttachment(id string, attachmentName string, attachment io.Reader) (*Search, error) { 374 | ep, err := a.getContentChildEndpoint(id, "attachment") 375 | if err != nil { 376 | return nil, err 377 | } 378 | return a.SendContentAttachmentRequest(ep, attachmentName, attachment, map[string]string{}) 379 | } 380 | 381 | // UpdateAttachment update the attachment with an attachmentID on a page with an id to a new version 382 | func (a *API) UpdateAttachment(id string, attachmentName string, attachmentID string, attachment io.Reader) (*Search, error) { 383 | ep, err := a.getContentChildEndpoint(id, "attachment/"+attachmentID+"/data") 384 | if err != nil { 385 | return nil, err 386 | } 387 | return a.SendContentAttachmentRequest(ep, attachmentName, attachment, map[string]string{}) 388 | } 389 | 390 | // DelContent deletes content by id 391 | func (a *API) DelContent(id string) (*Content, error) { 392 | ep, err := a.getContentIDEndpoint(id) 393 | if err != nil { 394 | return nil, err 395 | } 396 | return a.SendContentRequest(ep, "DELETE", nil) 397 | } 398 | 399 | // ContentVersionResult contains the version results 400 | type ContentVersionResult struct { 401 | Result []Version `json:"results"` 402 | } 403 | 404 | // GetContentVersion gets all versions of this content 405 | func (a *API) GetContentVersion(id string) (*ContentVersionResult, error) { 406 | ep, err := a.getContentGenericEndpoint(id, "version") 407 | if err != nil { 408 | return nil, err 409 | } 410 | return a.SendContentVersionRequest(ep, "GET") 411 | } 412 | 413 | // addContentQueryParams adds the defined query parameters 414 | func addContentQueryParams(query ContentQuery) *url.Values { 415 | 416 | data := url.Values{} 417 | if len(query.Expand) != 0 { 418 | data.Set("expand", strings.Join(query.Expand, ",")) 419 | } 420 | //get specific version 421 | if query.Version != 0 { 422 | data.Set("version", strconv.Itoa(query.Version)) 423 | } 424 | if query.Limit != 0 { 425 | data.Set("limit", strconv.Itoa(query.Limit)) 426 | } 427 | if query.OrderBy != "" { 428 | data.Set("orderby", query.OrderBy) 429 | } 430 | if query.PostingDay != "" { 431 | data.Set("postingDay", query.PostingDay) 432 | } 433 | if query.SpaceKey != "" { 434 | data.Set("spaceKey", query.SpaceKey) 435 | } 436 | if query.Start != 0 { 437 | data.Set("start", strconv.Itoa(query.Start)) 438 | } 439 | if query.Status != "" { 440 | data.Set("status", query.Status) 441 | } 442 | if query.Title != "" { 443 | data.Set("title", query.Title) 444 | } 445 | if query.Trigger != "" { 446 | data.Set("trigger", query.Trigger) 447 | } 448 | if query.Type != "" { 449 | data.Set("type", query.Type) 450 | } 451 | return &data 452 | } 453 | --------------------------------------------------------------------------------