import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
export const confluenceTriggerOptions = [
{ label: 'Page Created', id: 'confluence_page_created' },
{ label: 'Page Updated', id: 'confluence_page_updated' },
{ label: 'Page Removed', id: 'confluence_page_removed' },
{ label: 'Page Moved', id: 'confluence_page_moved' },
{ label: 'Comment Created', id: 'confluence_comment_created' },
{ label: 'Comment Removed', id: 'confluence_comment_removed' },
{ label: 'Blog Post Created', id: 'confluence_blog_created' },
{ label: 'Blog Post Updated', id: 'confluence_blog_updated' },
{ label: 'Blog Post Removed', id: 'confluence_blog_removed' },
{ label: 'Attachment Created', id: 'confluence_attachment_created' },
{ label: 'Attachment Removed', id: 'confluence_attachment_removed' },
{ label: 'Space Created', id: 'confluence_space_created' },
{ label: 'Space Updated', id: 'confluence_space_updated' },
{ label: 'Label Added', id: 'confluence_label_added' },
{ label: 'Label Removed', id: 'confluence_label_removed' },
{ label: 'Comment Updated', id: 'confluence_comment_updated' },
{ label: 'Attachment Updated', id: 'confluence_attachment_updated' },
{ label: 'Page Restored', id: 'confluence_page_restored' },
{ label: 'Blog Post Restored', id: 'confluence_blog_restored' },
{ label: 'Space Removed', id: 'confluence_space_removed' },
{ label: 'Page Permissions Updated', id: 'confluence_page_permissions_updated' },
{ label: 'User Created', id: 'confluence_user_created' },
{ label: 'Generic Webhook (All Events)', id: 'confluence_webhook' },
]
export function confluenceSetupInstructions(eventType: string): string {
const instructions = [
'Note: You must have admin permissions in your Confluence workspace to create webhooks. See the Confluence webhook documentation for details.',
'In Confluence, navigate to Settings > Webhooks.',
'Click "Create a Webhook" to add a new webhook.',
'Paste the Webhook URL from above into the URL field.',
'Optionally, enter the Webhook Secret from above into the secret field for added security.',
`Select the events you want to trigger this workflow. For this trigger, select ${eventType}.`,
'Click "Create" to activate the webhook.',
]
return instructions
.map(
(instruction, index) =>
`
${index === 0 ? instruction : `${index}. ${instruction}`}
`
)
.join('')
}
export function buildConfluenceExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'webhookSecret',
title: 'Webhook Secret',
type: 'short-input',
placeholder: 'Enter a strong secret',
description:
'Optional secret to validate webhook deliveries from Confluence using HMAC signature',
password: true,
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'confluenceDomain',
title: 'Confluence Domain',
type: 'short-input',
placeholder: 'your-company.atlassian.net',
description: 'Your Confluence Cloud domain',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
export function buildConfluenceAttachmentExtraFields(triggerId: string): SubBlockConfig[] {
return [
...buildConfluenceExtraFields(triggerId),
{
id: 'confluenceEmail',
title: 'Confluence Email',
type: 'short-input',
placeholder: 'user@example.com',
description:
'Your Atlassian account email. Required together with API token to download attachment files.',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'confluenceApiToken',
title: 'API Token',
type: 'short-input',
placeholder: 'Enter your Atlassian API token',
description:
'API token from https://id.atlassian.com/manage-profile/security/api-tokens. Required to download attachment file content.',
password: true,
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'includeFileContent',
title: 'Include File Content',
type: 'switch',
defaultValue: false,
description:
'Download and include actual file content from attachments. Requires email, API token, and domain.',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Base webhook outputs common to all Confluence triggers.
*/
function buildBaseWebhookOutputs(): Record {
return {
timestamp: {
type: 'number',
description: 'Timestamp of the webhook event (Unix epoch milliseconds)',
},
userAccountId: {
type: 'string',
description: 'Account ID of the user who triggered the event',
},
accountType: {
type: 'string',
description: 'Account type (e.g., customer)',
},
}
}
/**
* Shared content-entity output fields present on page, blog, comment, and attachment objects.
*/
function buildContentEntityFields(): Record {
return {
id: { type: 'number', description: 'Content ID' },
title: { type: 'string', description: 'Content title' },
contentType: {
type: 'string',
description: 'Content type (page, blogpost, comment, attachment)',
},
version: { type: 'number', description: 'Version number' },
spaceKey: { type: 'string', description: 'Space key the content belongs to' },
creatorAccountId: { type: 'string', description: 'Account ID of the creator' },
lastModifierAccountId: { type: 'string', description: 'Account ID of the last modifier' },
self: { type: 'string', description: 'URL link to the content' },
creationDate: { type: 'number', description: 'Creation timestamp (Unix epoch milliseconds)' },
modificationDate: {
type: 'number',
description: 'Last modification timestamp (Unix epoch milliseconds)',
},
}
}
/** Page-related outputs for page events. */
export function buildPageOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
page: buildContentEntityFields(),
}
}
/** Comment-related outputs for comment events. */
export function buildCommentOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
comment: {
...buildContentEntityFields(),
parent: {
id: { type: 'number', description: 'Parent page/blog ID' },
title: { type: 'string', description: 'Parent page/blog title' },
contentType: { type: 'string', description: 'Parent content type (page or blogpost)' },
spaceKey: { type: 'string', description: 'Space key of the parent' },
self: { type: 'string', description: 'URL link to the parent content' },
},
},
}
}
/** Blog post outputs for blog events. */
export function buildBlogOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
blog: buildContentEntityFields(),
}
}
/** Attachment-related outputs for attachment events. */
export function buildAttachmentOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
attachment: {
...buildContentEntityFields(),
mediaType: { type: 'string', description: 'MIME type of the attachment' },
fileSize: { type: 'number', description: 'File size in bytes' },
parent: {
id: { type: 'number', description: 'Container page/blog ID' },
title: { type: 'string', description: 'Container page/blog title' },
contentType: { type: 'string', description: 'Container content type' },
},
},
files: {
type: 'file[]',
description:
'Attachment file content downloaded from Confluence (if includeFileContent is enabled with credentials)',
},
}
}
/** Space-related outputs for space events. */
export function buildSpaceOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
space: {
key: { type: 'string', description: 'Space key' },
name: { type: 'string', description: 'Space name' },
self: { type: 'string', description: 'URL link to the space' },
},
}
}
/** Label-related outputs for label events. */
export function buildLabelOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
label: {
name: { type: 'string', description: 'Label name' },
id: { type: 'string', description: 'Label ID' },
prefix: { type: 'string', description: 'Label prefix (global, my, team)' },
},
content: {
id: { type: 'number', description: 'Content ID the label was added to or removed from' },
title: { type: 'string', description: 'Content title' },
contentType: { type: 'string', description: 'Content type (page, blogpost)' },
},
}
}
/** Page permissions outputs for permission change events. */
export function buildPagePermissionsOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
page: {
...buildContentEntityFields(),
permissions: {
type: 'json',
description: 'Updated permissions object for the page',
},
},
}
}
/** User-related outputs for user events. */
export function buildUserOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
user: {
accountId: { type: 'string', description: 'Account ID of the new user' },
accountType: { type: 'string', description: 'Account type (e.g., atlassian, app)' },
displayName: { type: 'string', description: 'Display name of the user' },
emailAddress: {
type: 'string',
description:
'Email address of the user (may not be available due to GDPR/privacy settings)',
},
publicName: { type: 'string', description: 'Public name of the user' },
self: { type: 'string', description: 'URL link to the user profile' },
},
}
}
/** Combined outputs for the generic webhook trigger (all events). */
export function buildGenericWebhookOutputs(): Record {
return {
...buildBaseWebhookOutputs(),
page: { type: 'json', description: 'Page object (present in page events)' },
comment: { type: 'json', description: 'Comment object (present in comment events)' },
blog: { type: 'json', description: 'Blog post object (present in blog events)' },
attachment: { type: 'json', description: 'Attachment object (present in attachment events)' },
space: { type: 'json', description: 'Space object (present in space events)' },
label: { type: 'json', description: 'Label object (present in label events)' },
content: { type: 'json', description: 'Content object (present in label events)' },
user: { type: 'json', description: 'User object (present in user events)' },
files: {
type: 'file[]',
description:
'Attachment file content (present in attachment events when includeFileContent is enabled)',
},
}
}
export function extractPageData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
page: body.page || {},
}
}
export function extractCommentData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
comment: body.comment || {},
}
}
export function extractBlogData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
blog: body.blog || body.blogpost || {},
}
}
export function extractAttachmentData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
attachment: body.attachment || {},
}
}
export function extractSpaceData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
space: body.space || {},
}
}
export function extractLabelData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
label: body.label || {},
content: body.content || body.page || body.blog || {},
}
}
export function extractPagePermissionsData(body: Record) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
page: body.page || {},
}
}
export function extractUserData(body: Record) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
user: body.user || {},
}
}
/**
* Maps trigger IDs to the exact Confluence event strings they accept.
* Admin REST API webhooks include an `event` field (e.g. `"event": "page_created"`).
* Connect app webhooks do NOT — for those we fall back to entity-category matching.
*/
const TRIGGER_EVENT_MAP: Record = {
confluence_page_created: ['page_created'],
confluence_page_updated: ['page_updated'],
confluence_page_removed: ['page_removed', 'page_trashed'],
confluence_page_moved: ['page_moved'],
confluence_comment_created: ['comment_created'],
confluence_comment_removed: ['comment_removed'],
confluence_blog_created: ['blog_created'],
confluence_blog_updated: ['blog_updated'],
confluence_blog_removed: ['blog_removed', 'blog_trashed'],
confluence_attachment_created: ['attachment_created'],
confluence_attachment_removed: ['attachment_removed', 'attachment_trashed'],
confluence_space_created: ['space_created'],
confluence_space_updated: ['space_updated'],
confluence_label_added: ['label_added', 'label_created'],
confluence_label_removed: ['label_removed', 'label_deleted'],
confluence_comment_updated: ['comment_updated'],
confluence_attachment_updated: ['attachment_updated'],
confluence_page_restored: ['page_restored'],
confluence_blog_restored: ['blog_restored'],
confluence_space_removed: ['space_removed'],
confluence_page_permissions_updated: ['content_permissions_updated'],
confluence_user_created: ['user_created', 'user_reactivated'],
}
const TRIGGER_CATEGORY_MAP: Record = {
confluence_page_created: 'page',
confluence_page_updated: 'page',
confluence_page_removed: 'page',
confluence_page_moved: 'page',
confluence_comment_created: 'comment',
confluence_comment_removed: 'comment',
confluence_blog_created: 'blog',
confluence_blog_updated: 'blog',
confluence_blog_removed: 'blog',
confluence_attachment_created: 'attachment',
confluence_attachment_removed: 'attachment',
confluence_space_created: 'space',
confluence_space_updated: 'space',
confluence_label_added: 'label',
confluence_label_removed: 'label',
confluence_comment_updated: 'comment',
confluence_attachment_updated: 'attachment',
confluence_page_restored: 'page',
confluence_blog_restored: 'blog',
confluence_space_removed: 'space',
confluence_page_permissions_updated: 'page',
confluence_user_created: 'user',
}
/**
* Infers the entity category from a Confluence webhook payload by checking
* which entity key is present in the body.
*/
function inferEntityCategory(body: Record): string | null {
if (body.comment) return 'comment'
if (body.attachment) return 'attachment'
if (body.blog || body.blogpost) return 'blog'
if (body.label) return 'label'
if (body.page) return 'page'
if (body.space) return 'space'
if (body.user) return 'user'
return null
}
/**
* Checks if a Confluence webhook payload matches a trigger.
*
* Admin REST API webhooks (Settings > Webhooks) include an `event` field
* for exact action-level matching. Connect app webhooks omit it, so we
* fall back to entity-category matching (page vs comment vs blog, etc.).
*/
export function isConfluencePayloadMatch(
triggerId: string,
body: Record
): boolean {
if (triggerId === 'confluence_webhook') {
return true
}
const event = body.event as string | undefined
if (event) {
const acceptedEvents = TRIGGER_EVENT_MAP[triggerId]
return acceptedEvents ? acceptedEvents.includes(event) : false
}
const expectedCategory = TRIGGER_CATEGORY_MAP[triggerId]
if (!expectedCategory) {
return false
}
return inferEntityCategory(body) === expectedCategory
}