use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceSearchResult {
pub id: String,
pub title: String,
pub space_key: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceSearchResults {
pub results: Vec<ConfluenceSearchResult>,
pub total: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceUserSearchResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub account_id: Option<String>,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceUserSearchResults {
pub users: Vec<ConfluenceUserSearchResult>,
pub total: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceUserRecord {
pub account_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceUserGetResults {
pub users: Vec<ConfluenceUserRecord>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
pub(crate) struct ConfluenceContentSearchResponse {
pub(crate) results: Vec<ConfluenceContentSearchEntry>,
#[serde(default)]
pub(crate) size: u32,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceSearchLinks>,
}
#[derive(Deserialize, Default)]
pub(crate) struct ConfluenceSearchLinks {
pub(crate) next: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceContentSearchEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(rename = "_expandable")]
pub(crate) expandable: Option<ConfluenceExpandable>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceExpandable {
pub(crate) space: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceUserSearchResponse {
pub(crate) results: Vec<ConfluenceUserSearchEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceSearchLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceUserSearchEntry {
#[serde(default)]
pub(crate) user: Option<ConfluenceSearchUser>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSearchUser {
#[serde(rename = "accountId", default)]
pub(crate) account_id: Option<String>,
#[serde(rename = "displayName", default)]
pub(crate) display_name: Option<String>,
#[serde(default)]
pub(crate) email: Option<String>,
#[serde(rename = "publicName", default)]
pub(crate) public_name: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceUserGetEntry {
#[serde(rename = "accountId", default)]
pub(crate) account_id: Option<String>,
#[serde(rename = "accountType", default)]
pub(crate) account_type: Option<String>,
#[serde(rename = "displayName", default)]
pub(crate) display_name: Option<String>,
#[serde(rename = "publicName", default)]
pub(crate) public_name: Option<String>,
#[serde(default)]
pub(crate) email: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluencePageResponse {
pub(crate) id: String,
pub(crate) title: String,
pub(crate) status: String,
#[serde(rename = "spaceId")]
pub(crate) space_id: String,
pub(crate) version: Option<ConfluenceVersion>,
pub(crate) body: Option<ConfluenceBody>,
#[serde(rename = "parentId")]
pub(crate) parent_id: Option<String>,
#[serde(default)]
pub(crate) ancestors: Vec<ConfluenceAncestorEntry>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAncestorEntry {
pub(crate) id: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceVersion {
pub(crate) number: u32,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceBody {
pub(crate) atlas_doc_format: Option<ConfluenceAtlasDoc>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAtlasDoc {
pub(crate) value: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpaceResponse {
pub(crate) key: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpacesResponse {
pub(crate) results: Vec<ConfluenceSpaceEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceSpaceLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpaceLinks {
pub(crate) next: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpaceEntry {
pub(crate) id: String,
#[serde(default)]
pub(crate) key: Option<String>,
#[serde(default)]
pub(crate) name: Option<String>,
#[serde(rename = "type", default)]
pub(crate) type_: Option<String>,
#[serde(default)]
pub(crate) status: Option<String>,
#[serde(rename = "homepageId", default)]
pub(crate) homepage_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceSpace {
pub id: String,
pub key: String,
pub name: String,
#[serde(rename = "type")]
pub type_: String,
pub status: String,
#[serde(rename = "homepageId", skip_serializing_if = "Option::is_none")]
pub homepage_id: Option<String>,
}
impl From<ConfluenceSpaceEntry> for ConfluenceSpace {
fn from(e: ConfluenceSpaceEntry) -> Self {
Self {
id: e.id,
key: e.key.unwrap_or_default(),
name: e.name.unwrap_or_default(),
type_: e.type_.unwrap_or_default(),
status: e.status.unwrap_or_default(),
homepage_id: e.homepage_id,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceSpacePage {
pub results: Vec<ConfluenceSpace>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PageSummary {
pub id: String,
pub title: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub status: String,
#[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(rename = "authorId", skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>,
#[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PageSummaryPage {
pub results: Vec<PageSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceChildrenResponse {
pub(crate) results: Vec<ConfluenceChildEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceChildrenLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceChildEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(default)]
pub(crate) status: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceChildrenLinks {
pub(crate) next: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpacePagesResponse {
pub(crate) results: Vec<ConfluenceSpacePageEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceChildrenLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpacePageEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(default)]
pub(crate) status: Option<String>,
#[serde(rename = "parentId", default)]
pub(crate) parent_id: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpacePagesSummaryResponse {
pub(crate) results: Vec<ConfluenceSpacePageSummaryEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceChildrenLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceSpacePageSummaryEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(default)]
pub(crate) status: Option<String>,
#[serde(rename = "parentId", default)]
pub(crate) parent_id: Option<String>,
#[serde(rename = "authorId", default)]
pub(crate) author_id: Option<String>,
#[serde(rename = "createdAt", default)]
pub(crate) created_at: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ChildPage {
pub id: String,
pub title: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space_key: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CommentKind {
Footer,
Inline,
}
impl CommentKind {
#[must_use]
pub fn endpoint_segment(self) -> &'static str {
match self {
Self::Footer => "footer-comments",
Self::Inline => "inline-comments",
}
}
}
impl std::fmt::Display for CommentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Footer => f.write_str("footer"),
Self::Inline => f.write_str("inline"),
}
}
}
#[derive(Debug, Clone)]
pub struct InlineAnchor {
pub text: String,
pub match_index: usize,
pub match_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceComment {
pub id: String,
pub author: String,
pub kind: CommentKind,
pub body_adf: Option<serde_json::Value>,
pub created: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_marker_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_original_selection: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCommentsResponse {
pub(crate) results: Vec<ConfluenceCommentEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceCommentsLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCommentsLinks {
pub(crate) next: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCommentEntry {
pub(crate) id: String,
#[serde(default)]
pub(crate) version: Option<ConfluenceCommentVersion>,
#[serde(default)]
pub(crate) body: Option<ConfluenceCommentBody>,
#[serde(default)]
pub(crate) properties: Option<ConfluenceInlineCommentProperties>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceInlineCommentProperties {
#[serde(rename = "inlineMarkerRef", default)]
pub(crate) inline_marker_ref: Option<String>,
#[serde(rename = "inlineOriginalSelection", default)]
pub(crate) inline_original_selection: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCommentVersion {
#[serde(rename = "authorId", default)]
pub(crate) author_id: Option<String>,
#[serde(rename = "createdAt", default)]
pub(crate) created_at: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCommentBody {
pub(crate) atlas_doc_format: Option<ConfluenceAtlasDoc>,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceAddCommentRequest {
#[serde(rename = "pageId")]
pub(crate) page_id: String,
pub(crate) body: ConfluenceUpdateBody,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceAddInlineCommentRequest {
#[serde(rename = "pageId")]
pub(crate) page_id: String,
pub(crate) body: ConfluenceUpdateBody,
#[serde(rename = "inlineCommentProperties")]
pub(crate) inline_comment_properties: InlineCommentProperties,
}
#[derive(Serialize)]
pub(crate) struct InlineCommentProperties {
#[serde(rename = "textSelection")]
pub(crate) text_selection: String,
#[serde(rename = "textSelectionMatchCount")]
pub(crate) text_selection_match_count: usize,
#[serde(rename = "textSelectionMatchIndex")]
pub(crate) text_selection_match_index: usize,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceLabelsResponse {
pub(crate) results: Vec<ConfluenceLabelEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceLabelsLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceLabelEntry {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) prefix: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceLabelsLinks {
pub(crate) next: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceLabel {
pub id: String,
pub name: String,
pub prefix: String,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceAddLabelEntry {
pub(crate) prefix: String,
pub(crate) name: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceVersionsResponse {
pub(crate) results: Vec<ConfluenceVersionEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceVersionsLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceVersionEntry {
pub(crate) number: u32,
#[serde(rename = "createdAt", default)]
pub(crate) created_at: Option<String>,
#[serde(default)]
pub(crate) message: Option<String>,
#[serde(rename = "minorEdit", default)]
pub(crate) minor_edit: Option<bool>,
#[serde(rename = "authorId", default)]
pub(crate) author_id: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceVersionsLinks {
pub(crate) next: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageVersion {
pub number: u32,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub author_id: String,
#[serde(default)]
pub message: String,
#[serde(default)]
pub minor_edit: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SinceFilter {
Version(u32),
CreatedAt(String),
}
impl SinceFilter {
pub fn parse(raw: &str) -> Result<Self> {
let trimmed = raw.trim();
if trimmed.is_empty() {
anyhow::bail!("`since` must be a version number or ISO 8601 date");
}
if trimmed.chars().all(|c| c.is_ascii_digit()) {
let n: u32 = trimmed
.parse()
.with_context(|| format!("Invalid version number \"{trimmed}\""))?;
return Ok(Self::Version(n));
}
if trimmed.contains('-') || trimmed.contains('T') {
return Ok(Self::CreatedAt(trimmed.to_string()));
}
anyhow::bail!(
"`since` must be a numeric version (e.g. \"5\") or ISO 8601 date \
(e.g. \"2026-01-01T00:00:00Z\"); got \"{trimmed}\""
);
}
pub(crate) fn matches(&self, version: &PageVersion) -> bool {
match self {
Self::Version(min) => version.number >= *min,
Self::CreatedAt(min) => {
if version.created_at.is_empty() {
false
} else {
version.created_at.as_str() >= min.as_str()
}
}
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PageMetadata {
pub id: String,
pub title: String,
pub current_version: Option<u32>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAttachmentsResponse {
pub(crate) results: Vec<ConfluenceAttachmentEntry>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceAttachmentLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAttachmentLinks {
pub(crate) next: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAttachmentEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(rename = "mediaType", default)]
pub(crate) media_type: Option<String>,
#[serde(rename = "fileSize", default)]
pub(crate) file_size: Option<u64>,
#[serde(rename = "downloadLink", default)]
pub(crate) download_link: Option<String>,
#[serde(default)]
pub(crate) version: Option<ConfluenceAttachmentVersion>,
#[serde(rename = "pageId", default)]
pub(crate) page_id: Option<String>,
#[serde(rename = "fileId", default)]
pub(crate) file_id: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceAttachmentVersion {
pub(crate) number: u32,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceV1AttachmentResponse {
pub(crate) results: Vec<ConfluenceV1AttachmentEntry>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceV1AttachmentEntry {
pub(crate) id: String,
pub(crate) title: String,
#[serde(default)]
pub(crate) extensions: Option<ConfluenceV1AttachmentExtensions>,
#[serde(default)]
pub(crate) version: Option<ConfluenceAttachmentVersion>,
#[serde(default)]
pub(crate) container: Option<ConfluenceV1AttachmentContainer>,
#[serde(rename = "_links", default)]
pub(crate) links: Option<ConfluenceV1AttachmentEntryLinks>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceV1AttachmentExtensions {
#[serde(rename = "mediaType", default)]
pub(crate) media_type: Option<String>,
#[serde(rename = "fileSize", default)]
pub(crate) file_size: Option<u64>,
#[serde(rename = "fileId", default)]
pub(crate) file_id: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceV1AttachmentContainer {
#[serde(default)]
pub(crate) id: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceV1AttachmentEntryLinks {
#[serde(default)]
pub(crate) download: Option<String>,
}
impl From<ConfluenceV1AttachmentEntry> for ConfluenceAttachment {
fn from(e: ConfluenceV1AttachmentEntry) -> Self {
let (media_type, file_size, file_id) = match e.extensions {
Some(x) => (x.media_type, x.file_size, x.file_id),
None => (None, None, None),
};
Self {
id: e.id,
title: e.title,
media_type,
file_size,
download_url: e.links.and_then(|l| l.download),
version: e.version.map(|v| v.number),
page_id: e.container.and_then(|c| c.id),
file_id,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceAttachment {
pub id: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub download_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
}
impl From<ConfluenceAttachmentEntry> for ConfluenceAttachment {
fn from(e: ConfluenceAttachmentEntry) -> Self {
Self {
id: e.id,
title: e.title,
media_type: e.media_type,
file_size: e.file_size,
download_url: e.download_link,
version: e.version.map(|v| v.number),
page_id: e.page_id,
file_id: e.file_id,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfluenceAttachmentPage {
pub results: Vec<ConfluenceAttachment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceCreateRequest {
#[serde(rename = "spaceId")]
pub(crate) space_id: String,
pub(crate) title: String,
pub(crate) body: ConfluenceUpdateBody,
#[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
pub(crate) parent_id: Option<String>,
pub(crate) status: String,
}
#[derive(Deserialize)]
pub(crate) struct ConfluenceCreateResponse {
pub(crate) id: String,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceUpdateRequest {
pub(crate) id: String,
pub(crate) status: String,
pub(crate) title: String,
pub(crate) body: ConfluenceUpdateBody,
pub(crate) version: ConfluenceUpdateVersion,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceUpdateBody {
pub(crate) representation: String,
pub(crate) value: String,
}
#[derive(Serialize)]
pub(crate) struct ConfluenceUpdateVersion {
pub(crate) number: u32,
pub(crate) message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MovePosition {
Append,
Before,
After,
}
impl MovePosition {
pub fn as_str(self) -> &'static str {
match self {
Self::Append => "append",
Self::Before => "before",
Self::After => "after",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MovedPage {
pub id: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
pub ancestors: Vec<String>,
}