2019-08-06 16:38:47 +03:00

613 lines
11 KiB
Go

package confluence
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strings"
"github.com/bndr/gopencils"
"github.com/reconquest/karma-go"
)
type User struct {
AccountID string `json:"accountId"`
}
type API struct {
rest *gopencils.Resource
// it's deprecated accordingly to Atlassian documentation,
// but it's only way to set permissions
json *gopencils.Resource
}
type PageInfo struct {
ID string `json:"id"`
Title string `json:"title"`
Version struct {
Number int64 `json:"number"`
} `json:"version"`
Ancestors []struct {
Id string `json:"id"`
Title string `json:"title"`
} `json:"ancestors"`
Links struct {
Full string `json:"webui"`
} `json:"_links"`
}
type AttachmentInfo struct {
Filename string `json:"title"`
ID string `json:"id"`
Metadata struct {
Comment string `json:"comment"`
} `json:"metadata"`
Links struct {
Context string `json:"context"`
Download string `json:"download"`
} `json:"_links"`
}
type form struct {
buffer io.Reader
writer *multipart.Writer
}
func NewAPI(baseURL string, username string, password string) *API {
auth := &gopencils.BasicAuth{username, password}
return &API{
rest: gopencils.Api(baseURL+"/rest/api", auth),
json: gopencils.Api(
baseURL+"/rpc/json-rpc/confluenceservice-v2",
auth,
),
}
}
func (api *API) FindRootPage(space string) (*PageInfo, error) {
page, err := api.FindPage(space, ``)
if err != nil {
return nil, karma.Format(
err,
"can't obtain first page from space %q",
space,
)
}
if page == nil {
return nil, errors.New("no such space")
}
if len(page.Ancestors) == 0 {
return &PageInfo{
ID: page.ID,
Title: page.Title,
}, nil
}
return &PageInfo{
ID: page.Ancestors[0].Id,
Title: page.Ancestors[0].Title,
}, nil
}
func (api *API) FindPage(space string, title string) (*PageInfo, error) {
result := struct {
Results []PageInfo `json:"results"`
}{}
payload := map[string]string{
"spaceKey": space,
"expand": "ancestors,version",
}
if title != "" {
payload["title"] = title
}
request, err := api.rest.Res(
"content/", &result,
).Get(payload)
if err != nil {
return nil, err
}
// allow 404 because it's fine if page is not found,
// the function will return nil, nil
if request.Raw.StatusCode != 404 && request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
return nil, nil
}
return &result.Results[0], nil
}
func (api *API) CreateAttachment(
pageID string,
name string,
comment string,
path string,
) (AttachmentInfo, error) {
var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path)
if err != nil {
return AttachmentInfo{}, err
}
var result struct {
Links struct {
Context string `json:"context"`
} `json:"_links"`
Results []AttachmentInfo `json:"results"`
}
resource := api.rest.Res(
"content/"+pageID+"/child/attachment", &result,
)
resource.Payload = form.buffer
resource.Headers = http.Header{}
resource.SetHeader("Content-Type", form.writer.FormDataContentType())
resource.SetHeader("X-Atlassian-Token", "no-check")
request, err := resource.Post()
if err != nil {
return info, err
}
if request.Raw.StatusCode != 200 {
return info, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
return info, errors.New(
"Confluence REST API for creating attachments returned " +
"0 json objects, expected at least 1",
)
}
for i, info := range result.Results {
if info.Links.Context == "" {
info.Links.Context = result.Links.Context
}
result.Results[i] = info
}
info = result.Results[0]
return info, nil
}
func (api *API) UpdateAttachment(
pageID string,
attachID string,
name string,
comment string,
path string,
) (AttachmentInfo, error) {
var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path)
if err != nil {
return AttachmentInfo{}, err
}
var result struct {
Links struct {
Context string `json:"context"`
} `json:"_links"`
Results []AttachmentInfo `json:"results"`
}
resource := api.rest.Res(
"content/"+pageID+"/child/attachment/"+attachID+"/data", &result,
)
resource.Payload = form.buffer
resource.Headers = http.Header{}
resource.SetHeader("Content-Type", form.writer.FormDataContentType())
resource.SetHeader("X-Atlassian-Token", "no-check")
request, err := resource.Post()
if err != nil {
return info, err
}
if request.Raw.StatusCode != 200 {
return info, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
return info, errors.New(
"Confluence REST API for creating attachments returned " +
"0 json objects, expected at least 1",
)
}
for i, info := range result.Results {
if info.Links.Context == "" {
info.Links.Context = result.Links.Context
}
result.Results[i] = info
}
info = result.Results[0]
return info, nil
}
func getAttachmentPayload(name, comment, path string) (*form, error) {
var (
payload = bytes.NewBuffer(nil)
writer = multipart.NewWriter(payload)
)
file, err := os.Open(path)
if err != nil {
return nil, karma.Format(
err,
"unable to open file: %q",
path,
)
}
defer file.Close()
content, err := writer.CreateFormFile("file", name)
if err != nil {
return nil, karma.Format(
err,
"unable to create form file",
)
}
_, err = io.Copy(content, file)
if err != nil {
return nil, karma.Format(
err,
"unable to copy i/o between form-file and file",
)
}
commentWriter, err := writer.CreateFormField("comment")
if err != nil {
return nil, karma.Format(
err,
"unable to create form field for comment",
)
}
_, err = commentWriter.Write([]byte(comment))
if err != nil {
return nil, karma.Format(
err,
"unable to write comment in form-field",
)
}
err = writer.Close()
if err != nil {
return nil, karma.Format(
err,
"unable to close form-writer",
)
}
return &form{
buffer: payload,
writer: writer,
}, nil
}
func (api *API) GetAttachments(pageID string) ([]AttachmentInfo, error) {
result := struct {
Links struct {
Context string `json:"context"`
} `json:"_links"`
Results []AttachmentInfo `json:"results"`
}{}
payload := map[string]string{
"expand": "version,container",
}
request, err := api.rest.Res(
"content/"+pageID+"/child/attachment", &result,
).Get(payload)
if err != nil {
return nil, err
}
if request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
}
for i, info := range result.Results {
if info.Links.Context == "" {
info.Links.Context = result.Links.Context
}
result.Results[i] = info
}
return result.Results, nil
}
func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
request, err := api.rest.Res(
"content/"+pageID, &PageInfo{},
).Get(map[string]string{"expand": "ancestors,version"})
if err != nil {
return nil, err
}
if request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*PageInfo), nil
}
func (api *API) CreatePage(
space string,
parent *PageInfo,
title string,
body string,
) (*PageInfo, error) {
payload := map[string]interface{}{
"type": "page",
"title": title,
"space": map[string]interface{}{
"key": space,
},
"body": map[string]interface{}{
"storage": map[string]interface{}{
"representation": "storage",
"value": body,
},
},
}
if parent != nil {
payload["ancestors"] = []map[string]interface{}{
{"id": parent.ID},
}
}
request, err := api.rest.Res(
"content/", &PageInfo{},
).Post(payload)
if err != nil {
return nil, err
}
if request.Raw.StatusCode != 200 {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*PageInfo), nil
}
func (api *API) UpdatePage(
page *PageInfo, newContent string,
) error {
nextPageVersion := page.Version.Number + 1
if len(page.Ancestors) == 0 {
return fmt.Errorf(
"page %q info does not contain any information about parents",
page.ID,
)
}
// picking only the last one, which is required by confluence
oldAncestors := []map[string]interface{}{
{"id": page.Ancestors[len(page.Ancestors)-1].Id},
}
payload := map[string]interface{}{
"id": page.ID,
"type": "page",
"title": page.Title,
"version": map[string]interface{}{
"number": nextPageVersion,
"minorEdit": false,
},
"ancestors": oldAncestors,
"body": map[string]interface{}{
"storage": map[string]interface{}{
"value": string(newContent),
"representation": "storage",
},
},
}
request, err := api.rest.Res(
"content/"+page.ID, &map[string]interface{}{},
).Put(payload)
if err != nil {
return err
}
if request.Raw.StatusCode != 200 {
return newErrorStatusNotOK(request)
}
return nil
}
func (api *API) GetUserByName(name string) (*User, error) {
var response struct {
Results []struct {
User User
}
}
_, err := api.rest.
Res("search").
Res("user", &response).
Get(map[string]string{
"cql": fmt.Sprintf("user.fullname~%q", name),
})
if err != nil {
return nil, err
}
if len(response.Results) == 0 {
return nil, karma.
Describe("name", name).
Reason(
"user with given name is not found",
)
}
return &response.Results[0].User, nil
}
func (api *API) GetCurrentUser() (*User, error) {
var user User
_, err := api.rest.
Res("user").
Res("current", &user).
Get()
if err != nil {
return nil, err
}
return &user, nil
}
func (api *API) RestrictPageUpdatesCloud(
page *PageInfo,
allowedUser string,
) error {
user, err := api.GetCurrentUser()
if err != nil {
return err
}
var result interface{}
request, err := api.rest.
Res("content").
Id(page.ID).
Res("restriction", &result).
Post([]map[string]interface{}{
{
"operation": "update",
"restrictions": map[string]interface{}{
"user": []map[string]interface{}{
{
"type": "known",
"accountId": user.AccountID,
},
},
},
},
})
if err != nil {
return err
}
if request.Raw.StatusCode != 200 {
return newErrorStatusNotOK(request)
}
return nil
}
func (api *API) RestrictPageUpdatesServer(
page *PageInfo,
allowedUser string,
) error {
var (
err error
result interface{}
)
request, err := api.json.Res(
"setContentPermissions", &result,
).Post([]interface{}{
page.ID,
"Edit",
map[string]interface{}{
"userName": allowedUser,
},
})
if err != nil {
return err
}
if request.Raw.StatusCode != 200 {
return newErrorStatusNotOK(request)
}
if success, ok := result.(bool); !ok || !success {
return fmt.Errorf(
"'true' response expected, but '%v' encountered",
result,
)
}
return nil
}
func (api *API) RestrictPageUpdates(
page *PageInfo,
allowedUser string,
) error {
var err error
if strings.HasSuffix(api.rest.Api.BaseUrl.Host, "atlassian.net") {
err = api.RestrictPageUpdatesCloud(page, allowedUser)
} else {
err = api.RestrictPageUpdatesServer(page, allowedUser)
}
return err
}
func newErrorStatusNotOK(request *gopencils.Resource) error {
if request.Raw.StatusCode == 401 {
return errors.New(
"Confluence API returned unexpected status: 401 (Unauthorized)",
)
}
if request.Raw.StatusCode == 404 {
return errors.New(
"Confluence API returned unexpected status: 404 (Not Found)",
)
}
output, _ := ioutil.ReadAll(request.Raw.Body)
defer request.Raw.Body.Close()
return fmt.Errorf(
"Confluence API returned unexpected status: %v, "+
"output: %s",
request.Raw.Status, output,
)
}