confluence

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package confluence is a small client for the Confluence Cloud REST API, covering exactly the calls atlio needs. It is hand-rolled rather than built on a third-party library because the page body has to survive as an untouched string end to end — a model layer that re-encodes it would break the lossless round-trip that is the whole point of this tool.

Capabilities are split across API versions as Atlassian publishes them: pages, children and comments come from v2, while CQL search, the credential check and attachment upload only exist in v1.

Index

Constants

This section is empty.

Variables

View Source
var ErrUploadRefused = errors.New("the page may already have an attachment with one of these names")

ErrUploadRefused marks a 400 to an upload that does not replace, which is how Confluence refuses a name the page already has an attachment under. It is worded as a possibility because a 400 is not only that: the server's own message, which the error carries too, is what names the cause.

View Source
var ErrUploadUnconfirmed = atlassian.ErrSentInFull

ErrUploadUnconfirmed marks an upload that failed after its whole body was sent, so Confluence may have stored the files anyway. It is the transport's atlassian.ErrSentInFull under the name of what it means here, so callers of this package need not reach into the transport to recognise it.

Functions

This section is empty.

Types

type Attachment

type Attachment struct {
	Title        string `json:"title"`
	MediaType    string `json:"mediaType"`
	FileSize     int64  `json:"fileSize"`
	DownloadLink string `json:"downloadLink"`
}

Attachment is one file attached to a page. Title is the filename, which is what `confluence attachments download` matches its glob against. DownloadLink is relative to the client's base URL (<site>/wiki), in the form /rest/api/content/{pageID}/child/attachment/{attachmentID}/download.

The API's own attachment id is deliberately not modelled: DownloadLink already carries it, and it is nothing a caller could use — downloads select by filename, and Confluence keeps same-named attachments as versions of one attachment rather than as separate ones, so no two listing entries share a name for an id to tell apart.

type Child

type Child struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Title  string `json:"title"`
	Type   string `json:"type"`
}

Child is one entry of a page's direct children. Type distinguishes pages from whiteboards, folders, databases and embeds. The API returns no space for a child, so callers that need one read it off the parent page.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to one Confluence Cloud site with one set of credentials.

func New

func New(siteURL, email, token string, opts ...atlassian.Option) (*Client, error)

New creates a Client for the Atlassian site at siteURL, its scheme and host (e.g. "https://example.atlassian.net"), authenticating with Atlassian Basic auth (account email + API token). Confluence Cloud serves its API and its pages under /wiki on every site, so the client derives that base itself rather than having it stored with the site.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the site's Confluence base URL, <site>/wiki. Page links the API returns are relative to it, so it is what callers join them onto.

func (*Client) ChildPages

func (c *Client) ChildPages(ctx context.Context, id string, limit int) ([]Child, bool, error)

ChildPages lists a page's direct child pages, oldest sibling first.

It reads /pages/{id}/direct-children — which returns every hierarchical content type — and filters to pages, rather than the /pages/{id}/children endpoint that would do that server-side: the latter is the only deprecated GET in the v2 API. Filtering inside the pagination loop (as opposed to in the caller) is what keeps limit counting child *pages* rather than children of any type.

func (*Client) CommentReplies

func (c *Client) CommentReplies(ctx context.Context, kind CommentKind, commentID string, limit int) ([]Comment, bool, error)

CommentReplies lists the direct replies to one comment.

func (*Client) CreateFooterComment

func (c *Client) CreateFooterComment(ctx context.Context, pageID, body string) (*Comment, error)

CreateFooterComment posts body as a new footer comment on a page and returns the comment the server created.

The body travels as the storage representation, unchanged.

func (*Client) CreatePage

func (c *Client) CreatePage(ctx context.Context, req CreatePageRequest) (*Page, error)

CreatePage creates a page and returns it as the server created it, which is where its ID, version and web link come from.

func (*Client) CurrentUser

func (c *Client) CurrentUser(ctx context.Context) (User, error)

CurrentUser identifies the authenticated account. It is the cheapest authenticated call available, so `auth login` uses it to validate credentials before writing anything to disk.

func (*Client) DownloadAttachment

func (c *Client) DownloadAttachment(ctx context.Context, downloadLink string, w io.Writer) (int64, error)

DownloadAttachment streams the bytes behind downloadLink to w and returns how many it wrote. Errors name downloadLink as the API returned it.

The download redirects once, to Atlassian's media service on another host, whose signed URL rejects a request that also carries the site's credentials. atlassian.Client.Download is what keeps them off it, both on the redirect and for a link that already points there.

func (*Client) GetPage

func (c *Client) GetPage(ctx context.Context, id string, withBody bool) (*Page, error)

GetPage fetches a page. withBody requests the storage representation; callers that only need the version (the optimistic-lock pre-flight in `confluence update`) pass false to avoid pulling the whole body back.

func (*Client) PageAttachments

func (c *Client) PageAttachments(ctx context.Context, pageID string, limit int) ([]Attachment, bool, error)

PageAttachments lists the files attached to a page.

func (*Client) PageComments

func (c *Client) PageComments(ctx context.Context, kind CommentKind, pageID string, limit int) ([]Comment, bool, error)

PageComments lists a page's root comments of the given kind, oldest first, with bodies in the storage representation. The comment endpoints do not accept body-format=view, so rendering for humans happens locally.

func (*Client) PagesByTitle

func (c *Client) PagesByTitle(ctx context.Context, spaceKey string, titles []string) ([]PageMatch, error)

PagesByTitle resolves exact page titles within one space. Titles are unique per space, so one query answers the whole batch; a title that matched nothing is simply missing from the result.

One query per space rather than one OR-ing (space, title) pairs across spaces: every hit then belongs to the space that was asked for, so the caller can attribute it by title alone without the response having to carry the space back.

func (*Client) Search

func (c *Client) Search(ctx context.Context, cql string, limit int) ([]SearchResult, int, error)

Search runs a CQL query, returning at most limit results plus the total number of matches the server reports, which is what lets the caller say how many results were left out.

CQL search only exists in v1, and it pages by offset rather than cursor.

func (*Client) SpaceHomepageID

func (c *Client) SpaceHomepageID(ctx context.Context, key string) (string, error)

SpaceHomepageID returns the ID of the homepage of the space whose key is key. It costs one request: v2 has no endpoint that takes the key in its path, so it filters the space listing by key and takes the first page of results only, which is where a key filter's one match lands.

key may also be the space's alias: a key changed in the space's settings stays the space's key, and the new one becomes the alias its URLs carry. The filter matches either, regardless of case, so the result is picked the same way. A key that names no space the token can see answers with no results rather than a 404, so that case is reported here.

func (*Client) UpdatePage

func (c *Client) UpdatePage(ctx context.Context, req UpdatePageRequest) (*Page, error)

UpdatePage writes a new version of a page and returns the updated page, whose version number is the authoritative new value to record locally.

func (*Client) UploadAttachments

func (c *Client) UploadAttachments(ctx context.Context, pageID string, files []UploadFile, replace bool) ([]UploadedAttachment, error)

UploadAttachments attaches files to a page in one request and returns the attachments the server reports for them.

This is v1: v2 has no upload. Without replace the request is a POST, which Confluence refuses as a whole — creating none of the files — when the page already has an attachment named like one of them; with replace it is a PUT, which adds a version to such an attachment and creates the rest.

Each file part is followed by a minorEdit field of true. The API requires the field either never or once per file, and true keeps the upload from notifying the page's watchers: the edit that embeds the file is the one they should hear about. X-Atlassian-Token is the CSRF opt-out Confluence answers 403 without.

func (*Client) Users

func (c *Client) Users(ctx context.Context, accountIDs []string) ([]User, error)

Users looks up the accounts named by accountIDs, in as few requests as the bulk endpoint's ceiling allows. Accounts that no longer exist, or that the token cannot see, are simply absent from the result.

Like the credential check, this is v1: the v2 API models no users.

type Comment

type Comment struct {
	ID               string  `json:"id"`
	Status           string  `json:"status"`
	Title            string  `json:"title"`
	ParentCommentID  string  `json:"parentCommentId"`
	Version          Version `json:"version"`
	ResolutionStatus string  `json:"resolutionStatus"`
	Body             struct {
		Storage struct {
			Value string `json:"value"`
		} `json:"storage"`
	} `json:"body"`
	Properties struct {
		InlineOriginalSelection string `json:"inlineOriginalSelection"`
	} `json:"properties"`
	Links links `json:"_links"`
}

Comment is one footer or inline comment. ResolutionStatus and Properties are only populated for inline comments.

type CommentKind

type CommentKind string

CommentKind selects between the two comment families a page can carry.

const (
	// FooterComments are the page-level discussion at the bottom of a page.
	FooterComments CommentKind = "footer-comments"
	// InlineComments are anchored to a highlighted span of the body.
	InlineComments CommentKind = "inline-comments"
)

type CreatePageRequest

type CreatePageRequest struct {
	SpaceID  string      `json:"spaceId"`
	Status   string      `json:"status"`
	Title    string      `json:"title"`
	ParentID string      `json:"parentId"`
	Body     storageBody `json:"body"`
}

CreatePageRequest is the v2 page-create payload.

NewCreatePageRequest fixes the status at "current" and the payload has no subtype field, so neither a draft nor a live doc — which `confluence update` could not write afterwards — can be asked for. CreatePage sends no query parameters either, so a private page cannot be asked for: creating one is outside what atlio does.

func NewCreatePageRequest

func NewCreatePageRequest(spaceID, parentID, title, body string) CreatePageRequest

NewCreatePageRequest builds the payload that creates a published page under parentID, in spaceID, with a storage-format body.

type Page

type Page struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Title  string `json:"title"`
	// SpaceID is what a new page is created in: the create request needs the
	// space's ID, and `confluence create` reads it off the parent.
	SpaceID string `json:"spaceId"`
	// Subtype is "live" for a live doc and absent for a classic page. It is
	// what tells the two apart offline, once `confluence read` or
	// `confluence create` has recorded it in the sidecar: a live doc's editor rewrites the storage body behind
	// atlio's back, which some edits have to refuse rather than silently lose.
	Subtype string  `json:"subtype"`
	Version Version `json:"version"`
	Body    struct {
		Storage struct {
			Representation string `json:"representation"`
			Value          string `json:"value"`
		} `json:"storage"`
	} `json:"body"`
	Links links `json:"_links"`
}

Page is the subset of v2's PageSingle atlio needs. Body holds the raw storage representation exactly as the API returned it.

type PageMatch

type PageMatch struct {
	ID    string
	Title string
}

PageMatch is one page a title lookup resolved to. Title is what the search API returned, which is HTML-escaped and may carry highlight markers, so comparing it to a title read out of a page body means normalising it first — format.StripHighlightMarkers does both halves.

type SearchResult

type SearchResult struct {
	Content *struct {
		ID     string `json:"id"`
		Type   string `json:"type"`
		Status string `json:"status"`
		Title  string `json:"title"`
	} `json:"content"`
	Title        string `json:"title"`
	Excerpt      string `json:"excerpt"`
	URL          string `json:"url"`
	EntityType   string `json:"entityType"`
	LastModified string `json:"lastModified"`
}

SearchResult is one CQL hit. Content is absent for results that are not content (spaces, users), so callers fall back to the top-level fields.

type UpdatePageRequest

type UpdatePageRequest struct {
	ID      string      `json:"id"`
	Status  string      `json:"status"`
	Title   string      `json:"title"`
	Body    storageBody `json:"body"`
	Version struct {
		Number  int    `json:"number"`
		Message string `json:"message"`
	} `json:"version"`
}

UpdatePageRequest is the v2 page-update payload. Every field is required by the API; atlio fills them from the file's sidecar.

func NewUpdatePageRequest

func NewUpdatePageRequest(id, status, title, body string, version int, message string) UpdatePageRequest

NewUpdatePageRequest builds an update payload for a storage-format body.

type UploadFile

type UploadFile struct {
	Name    string
	Content io.Reader
}

UploadFile is one file to attach: Name is the attachment's filename, and Content is read to its end as the file's bytes.

type UploadedAttachment

type UploadedAttachment struct {
	Title     string
	MediaType string
	FileSize  int64
	Version   int
}

UploadedAttachment is one attachment as an upload left it. Version is 1 for an attachment the upload created and higher for one it added a version to.

type User

type User struct {
	AccountID   string `json:"accountId"`
	Email       string `json:"email"`
	DisplayName string `json:"displayName"`
}

User is the subset of the current-user response atlio needs.

type Version

type Version struct {
	Number    int    `json:"number"`
	Message   string `json:"message"`
	CreatedAt string `json:"createdAt"`
	AuthorID  string `json:"authorId"`
}

Version mirrors the v2 version object shared by pages and comments.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL