From f26ae1ecb577d014b0375cf4ab9a8b3e32088df7 Mon Sep 17 00:00:00 2001 From: Mykyta Mazurenko Date: Wed, 4 Jun 2025 19:32:29 +0300 Subject: [PATCH 1/2] add confluence app with actions and triggers --- .github/workflows/pull_request.yml | 3 + ts/package.json | 1 + ts/src/ActionsCatalogue/index.ts | 2 + .../confluence/allowed-paths/blogposts.ts | 113 + ts/src/apps/confluence/allowed-paths/index.ts | 12 + ts/src/apps/confluence/allowed-paths/pages.ts | 120 + .../apps/confluence/allowed-paths/spaces.ts | 39 + ts/src/apps/confluence/allowed-paths/tasks.ts | 84 + ts/src/apps/confluence/constants.ts | 24 + ts/src/apps/confluence/helpers/constants.ts | 131 + .../helpers/get-blogpost-id-allowed-values.ts | 41 + .../helpers/get-label-id-allowed-values.ts | 40 + .../helpers/get-media-type-allowed-values.ts | 84 + .../helpers/get-page-id-allowed-values.ts | 40 + .../helpers/get-space-id-allowed-values.ts | 44 + .../helpers/get-task-id-allowed-values.ts | 46 + ts/src/apps/confluence/index.ts | 99 + ts/src/apps/confluence/triggers/index.ts | 3 + .../triggers/new-attachment.trigger.ts | 230 + .../triggers/new-blogpost.trigger.ts | 297 + .../confluence/triggers/new-page.trigger.ts | 292 + ts/src/apps/zoom/index.ts | 1 - ts/src/i18n/en/apps/Confluence/index.ts | 83 + ts/src/i18n/en/index.ts | 2 + ts/src/i18n/i18n-types.ts | 380 + ts/src/qtests/confluence.qtest.ts | 219 + ts/src/schemas/confluence.swagger.json | 24701 ++++++++++++++++ ts/yarn.lock | 76 + 28 files changed, 27206 insertions(+), 1 deletion(-) create mode 100644 ts/src/apps/confluence/allowed-paths/blogposts.ts create mode 100644 ts/src/apps/confluence/allowed-paths/index.ts create mode 100644 ts/src/apps/confluence/allowed-paths/pages.ts create mode 100644 ts/src/apps/confluence/allowed-paths/spaces.ts create mode 100644 ts/src/apps/confluence/allowed-paths/tasks.ts create mode 100644 ts/src/apps/confluence/constants.ts create mode 100644 ts/src/apps/confluence/helpers/constants.ts create mode 100644 ts/src/apps/confluence/helpers/get-blogpost-id-allowed-values.ts create mode 100644 ts/src/apps/confluence/helpers/get-label-id-allowed-values.ts create mode 100644 ts/src/apps/confluence/helpers/get-media-type-allowed-values.ts create mode 100644 ts/src/apps/confluence/helpers/get-page-id-allowed-values.ts create mode 100644 ts/src/apps/confluence/helpers/get-space-id-allowed-values.ts create mode 100644 ts/src/apps/confluence/helpers/get-task-id-allowed-values.ts create mode 100644 ts/src/apps/confluence/index.ts create mode 100644 ts/src/apps/confluence/triggers/index.ts create mode 100644 ts/src/apps/confluence/triggers/new-attachment.trigger.ts create mode 100644 ts/src/apps/confluence/triggers/new-blogpost.trigger.ts create mode 100644 ts/src/apps/confluence/triggers/new-page.trigger.ts create mode 100644 ts/src/i18n/en/apps/Confluence/index.ts create mode 100644 ts/src/qtests/confluence.qtest.ts create mode 100644 ts/src/schemas/confluence.swagger.json diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 709d7ceb..438ce83a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -75,6 +75,9 @@ env: ZOOM_REFRESH_TOKEN: ${{ secrets.ZOOM_REFRESH_TOKEN }} ZOOM_CLIENT_ID: ${{ secrets.ZOOM_CLIENT_ID }} ZOOM_CLIENT_SECRET: ${{ secrets.ZOOM_CLIENT_SECRET }} + CONFLUENCE_USERNAME: ${{ secrets.CONFLUENCE_USERNAME }} + CONFLUENCE_PASSWORD: ${{ secrets.CONFLUENCE_PASSWORD }} + CONFLUENCE_CLOUD_ID: ${{ secrets.CONFLUENCE_CLOUD_ID }} jobs: PullRequestTests: diff --git a/ts/package.json b/ts/package.json index 231d6bd3..a6b96862 100644 --- a/ts/package.json +++ b/ts/package.json @@ -54,6 +54,7 @@ "libsodium-wrappers": "^0.7.15", "lodash": "^4.17.21", "mime-types": "^3.0.1", + "node-html-parser": "^7.0.1", "notion-to-md": "^3.1.1", "semver": "^7.6.3", "to-title-case": "^1.0.0", diff --git a/ts/src/ActionsCatalogue/index.ts b/ts/src/ActionsCatalogue/index.ts index 822b7501..25769a9e 100644 --- a/ts/src/ActionsCatalogue/index.ts +++ b/ts/src/ActionsCatalogue/index.ts @@ -42,6 +42,7 @@ import { Log } from '../decorators/Logger'; import { Locales } from '../i18n/i18n-types'; import { PiecesAppCatalogue } from '../pieces/piecesCatalogue'; import { Debugger, DebugLevels } from '../utils/Debugger'; +import confluence from '../apps/confluence'; if (process.env.TS_DEBUG) { Debugger.level = DebugLevels.Verbose; @@ -56,6 +57,7 @@ export interface IQoreApi { } const NEW_APPS = { + confluence, zoom, googleContacts, googleDocs, diff --git a/ts/src/apps/confluence/allowed-paths/blogposts.ts b/ts/src/apps/confluence/allowed-paths/blogposts.ts new file mode 100644 index 00000000..72a825da --- /dev/null +++ b/ts/src/apps/confluence/allowed-paths/blogposts.ts @@ -0,0 +1,113 @@ +import { TAllowedPaths } from '@qoretechnologies/ts-toolkit'; +import { OpenAPIV2 } from 'openapi-types'; +import { buildActionsFromSwaggerSchema } from '../../../global/helpers'; +import confluence from '../../../schemas/confluence.swagger.json'; +import { CONFLUENCE_APP_NAME } from '../constants'; +import { confluencePaginationResponseConverter } from '../helpers/constants'; +import { getConfluenceBlogpostIdAllowedValues } from '../helpers/get-blogpost-id-allowed-values'; +import { getConfluenceLabelIdAllowedValues } from '../helpers/get-label-id-allowed-values'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; + +export const CONFLUENCE_BLOG_POSTS_ALLOWED_PATHS = { + '/blogposts': { + GET: { + override_options: { + 'space-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + POST: { + override_options: { + spaceId: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + title: { + required: true, + }, + }, + }, + }, + '/blogposts/{id}': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceBlogpostIdAllowedValues, + }, + }, + }, + PUT: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceBlogpostIdAllowedValues, + }, + spaceId: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + request_data_converter: (req) => { + const id = req?.query?.id; + + return { + ...req, + body: { + ...req.body, + id, + }, + }; + }, + }, + DELETE: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceBlogpostIdAllowedValues, + }, + }, + }, + }, + '/spaces/{id}/blogposts': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + }, + '/labels/{id}/blogposts': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceLabelIdAllowedValues, + }, + 'space-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + }, +} satisfies TAllowedPaths; + +export const CONFLUENCE_BLOG_POSTS_ACTIONS = buildActionsFromSwaggerSchema({ + schema: confluence as unknown as OpenAPIV2.Document, + allowedPaths: CONFLUENCE_BLOG_POSTS_ALLOWED_PATHS, + app: CONFLUENCE_APP_NAME, +}); diff --git a/ts/src/apps/confluence/allowed-paths/index.ts b/ts/src/apps/confluence/allowed-paths/index.ts new file mode 100644 index 00000000..be45a6de --- /dev/null +++ b/ts/src/apps/confluence/allowed-paths/index.ts @@ -0,0 +1,12 @@ +import { IQorePartialAppActionWithSwaggerPath } from '@qoretechnologies/ts-toolkit'; +import { CONFLUENCE_BLOG_POSTS_ACTIONS } from './blogposts'; +import { CONFLUENCE_PAGES_ACTIONS } from './pages'; +import { CONFLUENCE_SPACES_ACTIONS } from './spaces'; +import { CONFLUENCE_TASKS_ACTIONS } from './tasks'; + +export const CONFLUENCE_ACTIONS = [ + ...CONFLUENCE_BLOG_POSTS_ACTIONS, + ...CONFLUENCE_PAGES_ACTIONS, + ...CONFLUENCE_SPACES_ACTIONS, + ...CONFLUENCE_TASKS_ACTIONS, +] satisfies IQorePartialAppActionWithSwaggerPath[]; diff --git a/ts/src/apps/confluence/allowed-paths/pages.ts b/ts/src/apps/confluence/allowed-paths/pages.ts new file mode 100644 index 00000000..cacc1189 --- /dev/null +++ b/ts/src/apps/confluence/allowed-paths/pages.ts @@ -0,0 +1,120 @@ +import { TAllowedPaths } from '@qoretechnologies/ts-toolkit'; +import { OpenAPIV2 } from 'openapi-types'; +import { buildActionsFromSwaggerSchema } from '../../../global/helpers'; +import confluence from '../../../schemas/confluence.swagger.json'; +import { CONFLUENCE_APP_NAME } from '../constants'; +import { getConfluencePageIdAllowedValues } from '../helpers/get-page-id-allowed-values'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; +import { getConfluenceLabelIdAllowedValues } from '../helpers/get-label-id-allowed-values'; +import { confluencePaginationResponseConverter } from '../helpers/constants'; + +export const CONFLUENCE_PAGES_ALLOWED_PATHS = { + '/pages': { + GET: { + override_options: { + 'space-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + POST: { + override_options: { + spaceId: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + parentId: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + title: { + required: true, + }, + }, + }, + }, + '/pages/{id}': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + }, + }, + PUT: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + spaceId: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + parentId: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + }, + }, + DELETE: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + }, + }, + }, + '/pages/{id}/title': { + PUT: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + }, + }, + }, + '/spaces/{id}/pages': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + }, + '/labels/{id}/pages': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceLabelIdAllowedValues, + }, + 'space-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + }, +} satisfies TAllowedPaths; + +export const CONFLUENCE_PAGES_ACTIONS = buildActionsFromSwaggerSchema({ + schema: confluence as unknown as OpenAPIV2.Document, + allowedPaths: CONFLUENCE_PAGES_ALLOWED_PATHS, + app: CONFLUENCE_APP_NAME, +}); diff --git a/ts/src/apps/confluence/allowed-paths/spaces.ts b/ts/src/apps/confluence/allowed-paths/spaces.ts new file mode 100644 index 00000000..21a4a784 --- /dev/null +++ b/ts/src/apps/confluence/allowed-paths/spaces.ts @@ -0,0 +1,39 @@ +import { TAllowedPaths } from '@qoretechnologies/ts-toolkit'; +import { OpenAPIV2 } from 'openapi-types'; +import { buildActionsFromSwaggerSchema } from '../../../global/helpers'; +import confluence from '../../../schemas/confluence.swagger.json'; +import { CONFLUENCE_APP_NAME } from '../constants'; +import { getConfluenceLabelIdAllowedValues } from '../helpers/get-label-id-allowed-values'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; +import { confluencePaginationResponseConverter } from '../helpers/constants'; + +export const CONFLUENCE_SPACES_ALLOWED_PATHS = { + '/spaces': { + GET: { + response_data_converter: confluencePaginationResponseConverter, + }, + }, + '/spaces/{id}': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + labels: { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceLabelIdAllowedValues, + }, + }, + }, + }, +} satisfies TAllowedPaths; + +export const CONFLUENCE_SPACES_ACTIONS = buildActionsFromSwaggerSchema({ + schema: confluence as unknown as OpenAPIV2.Document, + allowedPaths: CONFLUENCE_SPACES_ALLOWED_PATHS, + app: CONFLUENCE_APP_NAME, +}); diff --git a/ts/src/apps/confluence/allowed-paths/tasks.ts b/ts/src/apps/confluence/allowed-paths/tasks.ts new file mode 100644 index 00000000..593a2fee --- /dev/null +++ b/ts/src/apps/confluence/allowed-paths/tasks.ts @@ -0,0 +1,84 @@ +import { TAllowedPaths } from '@qoretechnologies/ts-toolkit'; +import { OpenAPIV2 } from 'openapi-types'; +import { buildActionsFromSwaggerSchema } from '../../../global/helpers'; +import confluence from '../../../schemas/confluence.swagger.json'; +import { CONFLUENCE_APP_NAME } from '../constants'; +import { getConfluenceBlogpostIdAllowedValues } from '../helpers/get-blogpost-id-allowed-values'; +import { getConfluencePageIdAllowedValues } from '../helpers/get-page-id-allowed-values'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; +import { getConfluenceTaskIdAllowedValues } from '../helpers/get-task-id-allowed-values'; +import { confluencePaginationResponseConverter } from '../helpers/constants'; + +export const CONFLUENCE_TASKS_ALLOWED_PATHS = { + '/tasks': { + GET: { + override_options: { + 'task-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceTaskIdAllowedValues, + }, + 'space-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + 'page-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluencePageIdAllowedValues, + }, + 'blogpost-id': { + type: { + type: 'list', + element_type: 'softstring', + }, + get_element_allowed_values: getConfluenceBlogpostIdAllowedValues, + }, + }, + response_data_converter: confluencePaginationResponseConverter, + }, + }, + '/tasks/{id}': { + GET: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceTaskIdAllowedValues, + }, + }, + }, + PUT: { + override_options: { + id: { + type: 'softstring', + get_allowed_values: getConfluenceTaskIdAllowedValues, + }, + spaceId: { + type: 'softstring', + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + pageId: { + type: 'softstring', + get_allowed_values: getConfluencePageIdAllowedValues, + }, + blogpostId: { + type: 'softstring', + get_allowed_values: getConfluenceBlogpostIdAllowedValues, + }, + }, + }, + }, +} satisfies TAllowedPaths; + +export const CONFLUENCE_TASKS_ACTIONS = buildActionsFromSwaggerSchema({ + schema: confluence as unknown as OpenAPIV2.Document, + allowedPaths: CONFLUENCE_TASKS_ALLOWED_PATHS, + app: CONFLUENCE_APP_NAME, +}); diff --git a/ts/src/apps/confluence/constants.ts b/ts/src/apps/confluence/constants.ts new file mode 100644 index 00000000..6c13a069 --- /dev/null +++ b/ts/src/apps/confluence/constants.ts @@ -0,0 +1,24 @@ +import { TCustomConnOptions } from '@qoretechnologies/ts-toolkit'; + +export const CONFLUENCE_APP_NAME = 'Confluence'; +export const CONFLUENCE_APP_LOGO = + 'PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KDTwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIFRyYW5zZm9ybWVkIGJ5OiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgZmlsbD0iIzAwMDAwMCI+Cg08ZyBpZD0iU1ZHUmVwb19iZ0NhcnJpZXIiIHN0cm9rZS13aWR0aD0iMCIvPgoNPGcgaWQ9IlNWR1JlcG9fdHJhY2VyQ2FycmllciIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cg08ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+Cg08ZGVmcz4KDTxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjI4LjYwNyIgeTE9Ii02MC44MjUiIHgyPSIxMS4wODUiIHkyPSItNTAuNzU2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAtMjkuNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+Cg08c3RvcCBvZmZzZXQ9IjAuMTgiIHN0b3AtY29sb3I9IiMwMDUyY2MiLz4KDTxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzI2ODRmZiIvPgoNPC9saW5lYXJHcmFkaWVudD4KDTxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjYyMS40NDIiIHkxPSIxODE3LjU2NyIgeDI9IjYwMy45MTUiIHkyPSIxODI3LjY0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC0xLCAwLCAwLCAxLCA2MjQuODMsIC0xODE2LjcxKSIgeGxpbms6aHJlZj0iI2EiLz4KDTwvZGVmcz4KDTx0aXRsZT5maWxlX3R5cGVfY29uZmx1ZW5jZTwvdGl0bGU+Cg08cGF0aCBkPSJNMy4wMTUsMjMuMDg3Yy0uMjg5LjQ3Mi0uNjE0LDEuMDItLjg5MSwxLjQ1NmEuODkyLjg5MiwwLDAsMCwuMywxLjIxMmw1Ljc5MiwzLjU2NGEuODkuODksMCwwLDAsMS4yMjYtLjI5bC4wMDgtLjAxM2MuMjMxLS4zODcuNTMtLjg5MS44NTUtMS40MywyLjI5NC0zLjc4Nyw0LjYtMy4zMjMsOC43NjMtMS4zMzZsNS43NDMsMi43MzFBLjg5Mi44OTIsMCwwLDAsMjYsMjguNTU5bC4wMTEtLjAyNEwyOC43NjYsMjIuM2EuODkxLjg5MSwwLDAsMC0uNDQ1LTEuMTY3Yy0xLjIxMi0uNTctMy42MjItMS43MDctNS43OTItMi43NTRDMTQuNzI0LDE0LjU4Niw4LjA5LDE0LjgzMSwzLjAxNSwyMy4wODdaIiBzdHlsZT0iZmlsbDp1cmwoI2EpIi8+Cg08cGF0aCBkPSJNMjguOTg1LDguOTMyYy4yODktLjQ3Mi42MTQtMS4wMi44OTEtMS40NTZhLjg5Mi44OTIsMCwwLDAtLjMtMS4yMTJMMjMuNzg1LDIuN2EuODkuODksMCwwLDAtMS4yMzYuMjQxLjU4NC41ODQsMCwwLDAtLjAzMy4wNTNjLS4yMzIuMzg3LS41My44OTEtLjg1NiwxLjQzLTIuMjk0LDMuNzg3LTQuNiwzLjMyMy04Ljc2MywxLjMzNkw3LjE3MiwzLjA0M2EuODkuODksMCwwLDAtMS4xODcuNDIxbC0uMDExLjAyNEwzLjIxNiw5LjcyNmEuODkxLjg5MSwwLDAsMCwuNDQ1LDEuMTY3YzEuMjEyLjU3LDMuNjIyLDEuNzA2LDUuNzkyLDIuNzUzQzE3LjI3NiwxNy40MzMsMjMuOTEsMTcuMTc5LDI4Ljk4NSw4LjkzMloiIHN0eWxlPSJmaWxsOnVybCgjYikiLz4KDTwvZz4KDTwvc3ZnPg=='; + +export class ConfluenceError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConfluenceError'; + } +} + +export const CONFLUENCE_CONN_OPTIONS = { + cloud_id: { + display_name: 'Cloud ID', + short_desc: 'The cloud ID', + desc: 'The cloud ID', + type: 'string', + }, + swagger_base_path: { + type: 'string', + }, +} satisfies TCustomConnOptions; diff --git a/ts/src/apps/confluence/helpers/constants.ts b/ts/src/apps/confluence/helpers/constants.ts new file mode 100644 index 00000000..05a7693e --- /dev/null +++ b/ts/src/apps/confluence/helpers/constants.ts @@ -0,0 +1,131 @@ +import { + IQoreAllowedValue, + QorusRequest, + TQoreResponseDataConverterFunction, +} from '@qoretechnologies/ts-toolkit'; +import { delay } from '../../../global/helpers'; +import { Debugger } from '../../../utils/Debugger'; +import { CONFLUENCE_APP_NAME } from '../constants'; + +export const CONFLUENCE_ALLOWED_VALUES_TIMEOUT = 60_000; +export const CONFLUENCE_ALLOWED_VALUES_FETCH_DELAY = 300; + +export type TFetchConfluenceAllowedValuesOptions = { + token: string; + cloudId: string; + object?: string; + path: string; + limit?: number; + maxResults?: number; + mapItemToAllowedValue: (item: ItemType) => IQoreAllowedValue; +}; + +type TObjectsResponse = { + [key in ResponseKey]: ItemType[]; +} & { + page_size: number; + total_records: number; + next_page_token?: string; +}; + +export const fetchConfluenceAllowedValues = async < + ItemType = unknown, + ResponseKey extends string = 'results', +>( + options: TFetchConfluenceAllowedValuesOptions +): Promise[]> => { + const items = await fetchConfluenceRecords(options); + + return items.map(options.mapItemToAllowedValue); +}; + +export const fetchConfluenceRecords = async < + ItemType = unknown, + ResponseKey extends string = 'results', +>( + options: Omit, 'mapItemToAllowedValue'> +): Promise => { + const { path, cloudId, object = 'results', token } = options; + + const items: ItemType[] = []; + let cursor: string | undefined = undefined; + const startTime = Date.now(); + const maxResults = options.maxResults || 200; + const limit = options.limit || 100; + + try { + do { + if (Date.now() - startTime > CONFLUENCE_ALLOWED_VALUES_TIMEOUT) { + Debugger.log(`Timeout fetching Confluence allowed values for ${path}`); + break; + } + + if (items.length >= maxResults) { + break; + } + + const response: { data?: TObjectsResponse } = + (await QorusRequest.get<{ + data: TObjectsResponse; + }>( + { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + ...(cursor ? { cursor: cursor as string } : {}), + limit: limit.toString(), + }, + path, + }, + { + url: `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2`, + endpointId: CONFLUENCE_APP_NAME, + } + )) || {}; + + const responseData = response.data; + + if (!responseData) { + Debugger.log(`No data found for Confluence records for ${path}`); + break; + } + + const objectData = responseData[object as ResponseKey]; + + if (!objectData?.length) { + break; + } + + cursor = responseData?.next_page_token || undefined; + + items.push(...objectData); + + if (cursor) { + await delay(CONFLUENCE_ALLOWED_VALUES_FETCH_DELAY); + } + } while (cursor); + } catch (error) { + console.error(error); + Debugger.log(`Error fetching Confluence records for ${object}`, error); + + return items; + } + + return items; +}; + +export const confluencePaginationResponseConverter: TQoreResponseDataConverterFunction = (req) => { + const _links = req?.body?._links as { next?: string }; + let cursor: string | null = null; + + if (_links?.next) { + const fullUrl = new URL(`http://example.com${_links.next}`); + cursor = fullUrl.searchParams.get('cursor'); + } + + return { + ...req.body, + cursor, + }; +}; diff --git a/ts/src/apps/confluence/helpers/get-blogpost-id-allowed-values.ts b/ts/src/apps/confluence/helpers/get-blogpost-id-allowed-values.ts new file mode 100644 index 00000000..4d20d128 --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-blogpost-id-allowed-values.ts @@ -0,0 +1,41 @@ +import { + IQoreAllowedValue, + TCustomConnOptions, + TQoreGetAllowedValuesFunction, +} from '@qoretechnologies/ts-toolkit'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { ConfluenceError } from '../constants'; +import { fetchConfluenceAllowedValues } from './constants'; + +type TConfluenceBlogpost = { + id: string; + spaceId: string; + title: string; + status: string; +}; + +const mapConfluenceBlogpostToAllowedValue = ( + blogpost: TConfluenceBlogpost +): IQoreAllowedValue => ({ + value: blogpost.id, + display_name: blogpost.title, + desc: `Space ID: ${blogpost.spaceId}\nStatus: ${blogpost.status}`, +}); + +export const getConfluenceBlogpostIdAllowedValues: TQoreGetAllowedValuesFunction< + TCustomConnOptions, + string +> = async (context) => { + const { cloud_id, token } = getQoreContextRequiredValues({ + context, + connectionFields: ['cloud_id', 'token'], + ErrorClass: ConfluenceError, + }); + + return await fetchConfluenceAllowedValues({ + token, + cloudId: cloud_id, + path: '/blogposts', + mapItemToAllowedValue: mapConfluenceBlogpostToAllowedValue, + }); +}; diff --git a/ts/src/apps/confluence/helpers/get-label-id-allowed-values.ts b/ts/src/apps/confluence/helpers/get-label-id-allowed-values.ts new file mode 100644 index 00000000..750f4a12 --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-label-id-allowed-values.ts @@ -0,0 +1,40 @@ +import { + IQoreAllowedValue, + TCustomConnOptions, + TQoreGetAllowedValuesFunction, +} from '@qoretechnologies/ts-toolkit'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { ConfluenceError } from '../constants'; +import { fetchConfluenceAllowedValues } from './constants'; + +type TConfluenceLabel = { + id: string; + name: string; + prefix: string; +}; + +const mapConfluenceLabelToAllowedValue = (label: TConfluenceLabel): IQoreAllowedValue => { + return { + value: label.id, + display_name: label.name, + desc: `Prefix: ${label.prefix}\nName: ${label.name}`, + }; +}; + +export const getConfluenceLabelIdAllowedValues: TQoreGetAllowedValuesFunction< + TCustomConnOptions, + string +> = async (context) => { + const { cloud_id, token } = getQoreContextRequiredValues({ + context, + connectionFields: ['cloud_id', 'token'], + ErrorClass: ConfluenceError, + }); + + return await fetchConfluenceAllowedValues({ + token, + cloudId: cloud_id, + path: '/labels', + mapItemToAllowedValue: mapConfluenceLabelToAllowedValue, + }); +}; diff --git a/ts/src/apps/confluence/helpers/get-media-type-allowed-values.ts b/ts/src/apps/confluence/helpers/get-media-type-allowed-values.ts new file mode 100644 index 00000000..ca9acd79 --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-media-type-allowed-values.ts @@ -0,0 +1,84 @@ +import { IQoreAllowedValue } from '@qoretechnologies/ts-toolkit'; + +export const ConfluenceMediaTypeAllowedValues = [ + { + value: 'image/png', + display_name: 'PNG Image', + desc: 'Portable Network Graphics image format', + }, + { + value: 'image/jpeg', + display_name: 'JPEG Image', + desc: 'Joint Photographic Experts Group image format', + }, + { + value: 'image/gif', + display_name: 'GIF Image', + desc: 'Graphics Interchange Format image', + }, + { + value: 'image/svg+xml', + display_name: 'SVG Image', + desc: 'Scalable Vector Graphics image format', + }, + { + value: 'application/pdf', + display_name: 'PDF Document', + desc: 'Portable Document Format', + }, + { + value: 'application/msword', + display_name: 'Word Document', + desc: 'Microsoft Word document', + }, + { + value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + display_name: 'Word Document (DOCX)', + desc: 'Microsoft Word Open XML document', + }, + { + value: 'application/vnd.ms-excel', + display_name: 'Excel Spreadsheet', + desc: 'Microsoft Excel spreadsheet', + }, + { + value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + display_name: 'Excel Spreadsheet (XLSX)', + desc: 'Microsoft Excel Open XML spreadsheet', + }, + { + value: 'application/vnd.ms-powerpoint', + display_name: 'PowerPoint Presentation', + desc: 'Microsoft PowerPoint presentation', + }, + { + value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + display_name: 'PowerPoint Presentation (PPTX)', + desc: 'Microsoft PowerPoint Open XML presentation', + }, + { + value: 'text/plain', + display_name: 'Text File', + desc: 'Plain text file', + }, + { + value: 'text/csv', + display_name: 'CSV File', + desc: 'Comma-separated values file', + }, + { + value: 'application/zip', + display_name: 'ZIP Archive', + desc: 'ZIP compressed archive', + }, + { + value: 'video/mp4', + display_name: 'MP4 Video', + desc: 'MPEG-4 video file', + }, + { + value: 'audio/mpeg', + display_name: 'MP3 Audio', + desc: 'MPEG audio file', + }, +] satisfies IQoreAllowedValue[]; diff --git a/ts/src/apps/confluence/helpers/get-page-id-allowed-values.ts b/ts/src/apps/confluence/helpers/get-page-id-allowed-values.ts new file mode 100644 index 00000000..036da7aa --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-page-id-allowed-values.ts @@ -0,0 +1,40 @@ +import { + IQoreAllowedValue, + TCustomConnOptions, + TQoreGetAllowedValuesFunction, +} from '@qoretechnologies/ts-toolkit'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { ConfluenceError } from '../constants'; +import { fetchConfluenceAllowedValues } from './constants'; + +type TConfluencePage = { + id: string; + status: string; + title: string; +}; + +const mapConfluencePageToAllowedValue = (page: TConfluencePage): IQoreAllowedValue => { + return { + value: page.id, + display_name: page.title, + desc: `Status: ${page.status}\nTitle: ${page.title}`, + }; +}; + +export const getConfluencePageIdAllowedValues: TQoreGetAllowedValuesFunction< + TCustomConnOptions, + string +> = async (context) => { + const { cloud_id, token } = getQoreContextRequiredValues({ + context, + connectionFields: ['cloud_id', 'token'], + ErrorClass: ConfluenceError, + }); + + return await fetchConfluenceAllowedValues({ + token, + cloudId: cloud_id, + path: '/pages', + mapItemToAllowedValue: mapConfluencePageToAllowedValue, + }); +}; diff --git a/ts/src/apps/confluence/helpers/get-space-id-allowed-values.ts b/ts/src/apps/confluence/helpers/get-space-id-allowed-values.ts new file mode 100644 index 00000000..30b6635f --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-space-id-allowed-values.ts @@ -0,0 +1,44 @@ +import { + IQoreAllowedValue, + TCustomConnOptions, + TQoreGetAllowedValuesFunction, +} from '@qoretechnologies/ts-toolkit'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { ConfluenceError } from '../constants'; +import { fetchConfluenceAllowedValues } from './constants'; + +type TConfluenceSpace = { + id: string; + type: string; + name: string; + status: string; + currentActiveAlias: string; +}; + +const mapConfluenceSpaceToAllowedValue = (space: TConfluenceSpace): IQoreAllowedValue => { + const alias = space.currentActiveAlias?.length < 10 ? `[${space.currentActiveAlias}] ` : ''; + + return { + value: space.id, + display_name: `${alias}${space.name}`, + desc: `Status: ${space.status}\nCurrent Active Alias: ${space.currentActiveAlias}\nType: ${space.type}`, + }; +}; + +export const getConfluenceSpaceIdAllowedValues: TQoreGetAllowedValuesFunction< + TCustomConnOptions, + string +> = async (context) => { + const { cloud_id, token } = getQoreContextRequiredValues({ + context, + connectionFields: ['cloud_id', 'token'], + ErrorClass: ConfluenceError, + }); + + return await fetchConfluenceAllowedValues({ + token, + cloudId: cloud_id, + path: '/spaces', + mapItemToAllowedValue: mapConfluenceSpaceToAllowedValue, + }); +}; diff --git a/ts/src/apps/confluence/helpers/get-task-id-allowed-values.ts b/ts/src/apps/confluence/helpers/get-task-id-allowed-values.ts new file mode 100644 index 00000000..93d1c2da --- /dev/null +++ b/ts/src/apps/confluence/helpers/get-task-id-allowed-values.ts @@ -0,0 +1,46 @@ +import { + IQoreAllowedValue, + TCustomConnOptions, + TQoreGetAllowedValuesFunction, +} from '@qoretechnologies/ts-toolkit'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { ConfluenceError } from '../constants'; +import { fetchConfluenceAllowedValues } from './constants'; +import { parse } from 'node-html-parser'; + +type TConfluenceTask = { + id: string; + status: string; + body?: { + value: string; + }; +}; + +const mapConfluenceTaskToAllowedValue = (task: TConfluenceTask): IQoreAllowedValue => { + const bodyContent = task.body?.value ? parse(task.body.value).text : ''; + const title = bodyContent.length > 10 ? bodyContent.slice(0, 10) + '...' : bodyContent; + + return { + value: task.id, + display_name: title || `Task ${task.id}`, + desc: `Status: ${task.status}\nBody: ${bodyContent}`, + }; +}; + +export const getConfluenceTaskIdAllowedValues: TQoreGetAllowedValuesFunction< + TCustomConnOptions, + string +> = async (context) => { + const { cloud_id, token } = getQoreContextRequiredValues({ + context, + connectionFields: ['cloud_id', 'token'], + ErrorClass: ConfluenceError, + }); + + return await fetchConfluenceAllowedValues({ + token, + cloudId: cloud_id, + path: '/tasks', + mapItemToAllowedValue: mapConfluenceTaskToAllowedValue, + }); +}; diff --git a/ts/src/apps/confluence/index.ts b/ts/src/apps/confluence/index.ts new file mode 100644 index 00000000..7da43bd4 --- /dev/null +++ b/ts/src/apps/confluence/index.ts @@ -0,0 +1,99 @@ +import { + QorusRequest, + TQoreAppWithActions, + TQoreMappedOptions, +} from '@qoretechnologies/ts-toolkit'; +import { mapActionsToApp, mapTriggersToApp } from '../../global/helpers'; +import L from '../../i18n/i18n-node'; +import { Locales } from '../../i18n/i18n-types'; +import { CONFLUENCE_ACTIONS } from './allowed-paths'; +import { CONFLUENCE_APP_LOGO, CONFLUENCE_APP_NAME, CONFLUENCE_CONN_OPTIONS } from './constants'; +import * as CONFLUENCE_TRIGGERS from './triggers'; + +export default (locale: Locales) => + ({ + name: CONFLUENCE_APP_NAME, + display_name: L[locale].apps[CONFLUENCE_APP_NAME].displayName(), + short_desc: L[locale].apps[CONFLUENCE_APP_NAME].shortDesc(), + desc: L[locale].apps[CONFLUENCE_APP_NAME].longDesc(), + logo: CONFLUENCE_APP_LOGO, + logo_file_name: 'confluence-logo.svg', + logo_mime_type: 'image/svg+xml', + actions: [ + ...mapActionsToApp(CONFLUENCE_APP_NAME, CONFLUENCE_ACTIONS, locale), + ...mapTriggersToApp(CONFLUENCE_APP_NAME, CONFLUENCE_TRIGGERS, locale), + ], + rest: { + url: 'https://api.atlassian.com', + data: 'json', + oauth2_grant_type: 'authorization_code', + oauth2_scopes: [ + 'offline_access', + 'read:page:confluence', + 'write:page:confluence', + 'delete:page:confluence', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'delete:blogpost:confluence', + 'read:space:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'read:label:confluence', + 'write:label:confluence', + 'read:confluence-space.summary', + 'write:confluence-content', + ], + oauth2_auth_url: 'https://auth.atlassian.com/authorize', + oauth2_token_url: 'https://auth.atlassian.com/oauth/token', + ping_method: 'GET', + ping_path: '/oauth/token/accessible-resources', + }, + swagger_options: { + parse_flags: -1, + }, + swagger: 'schemas/confluence.swagger.json', + rest_modifiers: { + options: CONFLUENCE_CONN_OPTIONS, + set_options_post_auth: async ( + context + ): Promise> => { + const token = context?.conn_opts?.token; + + if (!token) { + throw new Error('The token is required to set confluence options post auth'); + } + + try { + const userAccounts = await QorusRequest.get>( + { + path: '/oauth/token/accessible-resources', + headers: { + Authorization: `Bearer ${token}`, + }, + }, + { + url: 'https://api.atlassian.com', + endpointId: 'Atlassian', + } + ); + const userInfo = userAccounts?.data[0]; + + if (!userInfo?.id) { + throw new Error('The user account id was not found'); + } + + const cloud_id = userInfo.id; + + return { + cloud_id, + swagger_base_path: `/ex/confluence/${cloud_id}/wiki/api/v2`, + }; + } catch (error) { + console.error(error); + throw error; + } + }, + }, + }) satisfies TQoreAppWithActions; diff --git a/ts/src/apps/confluence/triggers/index.ts b/ts/src/apps/confluence/triggers/index.ts new file mode 100644 index 00000000..e468c8ae --- /dev/null +++ b/ts/src/apps/confluence/triggers/index.ts @@ -0,0 +1,3 @@ +export { default as ConfluenceNewAttachmentTrigger } from './new-attachment.trigger'; +export { default as ConfluenceNewBlogpostTrigger } from './new-blogpost.trigger'; +export { default as ConfluenceNewPageTrigger } from './new-page.trigger'; diff --git a/ts/src/apps/confluence/triggers/new-attachment.trigger.ts b/ts/src/apps/confluence/triggers/new-attachment.trigger.ts new file mode 100644 index 00000000..85f88bf4 --- /dev/null +++ b/ts/src/apps/confluence/triggers/new-attachment.trigger.ts @@ -0,0 +1,230 @@ +import { EQoreAppActionCode, QoreAppCreator, QorusRequest } from '@qoretechnologies/ts-toolkit'; +import { DEFAULT_TRIGGER_POLL_ITEM_LIMIT } from '../../../global/constants'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { pollCreatedItemsForTrigger } from '../../../global/helpers/event-triggers'; +import { CONFLUENCE_APP_NAME, ConfluenceError } from '../constants'; +import { ConfluenceMediaTypeAllowedValues } from '../helpers/get-media-type-allowed-values'; + +type ConfluenceAttachment = { + id: string; + status: string; + title: string; + mediaType: string; + mediaTypeDescription: string; + comment: string; + fileId: string; + fileSize: number; + webuiLink: string; + downloadLink: string; + version: { + number: number; + authorId: string; + message: string; + createdAt: string; + }; + _links: { + download: string; + webui: string; + }; +}; + +const ConfluenceNewAttachmentTrigger = QoreAppCreator.createLocalizedTrigger({ + app: CONFLUENCE_APP_NAME, + action: 'new_attachment', + action_code: EQoreAppActionCode.EVENT, + options: { + status: { + type: { + type: 'list', + element_type: 'string', + }, + required: false, + element_allowed_values: [ + { + value: 'current', + display_name: 'Current', + desc: 'Attachments that are currently active', + }, + { + value: 'archived', + display_name: 'Archived', + desc: 'Attachments that have been archived', + }, + { + value: 'trashed', + display_name: 'Trashed', + desc: 'Attachments that have been moved to trash', + }, + ], + }, + mediaType: { + type: 'string', + required: false, + allowed_values_creatable: true, + allowed_values: ConfluenceMediaTypeAllowedValues, + }, + }, + event_function: async (context, update, should_stop) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const status = context.opts?.status as string[] | undefined; + const mediaType = context.opts?.mediaType; + + const getItems = () => { + return fetchLatestAttachments({ + token, + cloud_id, + status, + mediaType, + }); + }; + + await pollCreatedItemsForTrigger({ + trigger_name: 'confluence_new_attachment', + uniqueField: 'id', + getItems, + update, + should_stop, + }); + }, + get_example_event_data: async (context) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const attachments = await fetchLatestAttachments({ + token, + cloud_id, + }); + + return attachments?.length > 0 ? attachments[0] : null; + }, + event_info: { + desc: 'Confluence New Attachment Trigger Event Info', + type: { + type: 'hash', + fields: { + id: { + type: 'string', + }, + status: { + type: 'string', + }, + title: { + type: 'string', + }, + mediaType: { + type: 'string', + }, + mediaTypeDescription: { + type: 'string', + }, + comment: { + type: 'string', + }, + fileId: { + type: 'string', + }, + fileSize: { + type: 'number', + }, + webuiLink: { + type: 'string', + }, + downloadLink: { + type: 'string', + }, + version: { + type: { + type: 'hash', + fields: { + number: { + type: 'number', + }, + authorId: { + type: 'string', + }, + message: { + type: 'string', + }, + createdAt: { + type: 'string', + }, + }, + }, + }, + _links: { + type: { + type: 'hash', + fields: { + download: { + type: 'string', + }, + webui: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, +}); + +export default ConfluenceNewAttachmentTrigger; + +const fetchLatestAttachments = async (options: { + token: string; + cloud_id: string; + status?: string[]; + mediaType?: string; +}) => { + const { token, cloud_id, status, mediaType } = options; + const limit = DEFAULT_TRIGGER_POLL_ITEM_LIMIT; + + try { + const params: Record = { + limit: limit.toString(), + sort: '-created-date', + }; + + if (status && status.length > 0) { + params.status = status.join(','); + } + + if (mediaType) { + params['media-type'] = mediaType; + } + + const response = await QorusRequest.get<{ data: { results: ConfluenceAttachment[] } }>( + { + path: `/attachments`, + params, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }, + { + url: `https://api.atlassian.com/ex/confluence/${cloud_id}/wiki/api/v2`, + endpointId: CONFLUENCE_APP_NAME, + } + ); + + const attachments = response?.data?.results || []; + + if (attachments.length === 0) { + return []; + } + + return attachments; + } catch (error) { + throw new ConfluenceError(`Failed to fetch latest attachments: ${error.message || error}`); + } +}; diff --git a/ts/src/apps/confluence/triggers/new-blogpost.trigger.ts b/ts/src/apps/confluence/triggers/new-blogpost.trigger.ts new file mode 100644 index 00000000..762162de --- /dev/null +++ b/ts/src/apps/confluence/triggers/new-blogpost.trigger.ts @@ -0,0 +1,297 @@ +import { EQoreAppActionCode, QoreAppCreator, QorusRequest } from '@qoretechnologies/ts-toolkit'; +import { DEFAULT_TRIGGER_POLL_ITEM_LIMIT } from '../../../global/constants'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { pollCreatedItemsForTrigger } from '../../../global/helpers/event-triggers'; +import { CONFLUENCE_APP_NAME, ConfluenceError } from '../constants'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; + +type ConfluenceBlogpost = { + id: string; + status: string; + title: string; + spaceId: string; + authorId: string; + createdAt: string; + version: { + number: number; + authorId: string; + message: string; + createdAt: string; + }; + body?: { + storage?: { + value: string; + representation: string; + }; + atlas_doc_format?: { + value: string; + representation: string; + }; + view?: { + value: string; + representation: string; + }; + }; + _links: { + editui: string; + webui: string; + }; +}; + +const ConfluenceNewBlogpostTrigger = QoreAppCreator.createLocalizedTrigger({ + app: CONFLUENCE_APP_NAME, + action: 'new_blogpost', + action_code: EQoreAppActionCode.EVENT, + options: { + space_id: { + type: 'string', + required: false, + allowed_values_creatable: true, + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + status: { + type: { + type: 'list', + element_type: 'string', + }, + required: false, + element_allowed_values: [ + { + value: 'current', + display_name: 'Current', + desc: 'Blogposts that are currently active and published', + }, + { + value: 'deleted', + display_name: 'Deleted', + desc: 'Blogposts that have been deleted', + }, + { + value: 'trashed', + display_name: 'Trashed', + desc: 'Blogposts that have been moved to trash', + }, + ], + }, + body_format: { + type: 'string', + required: false, + allowed_values: [ + { + value: 'storage', + display_name: 'Storage Format', + desc: 'Confluence storage format (XML-based)', + }, + { + value: 'atlas_doc_format', + display_name: 'Atlas Document Format', + desc: 'Atlassian Document Format (ADF)', + }, + ], + }, + }, + event_function: async (context, update, should_stop) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const space_id = context.opts?.space_id; + const status = context.opts?.status as string[] | undefined; + const body_format = context.opts?.body_format; + + const getItems = () => { + return fetchLatestBlogposts({ + token, + cloud_id, + space_id, + status, + body_format, + }); + }; + + await pollCreatedItemsForTrigger({ + trigger_name: 'confluence_new_blogpost', + uniqueField: 'id', + getItems, + update, + should_stop, + }); + }, + get_example_event_data: async (context) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const blogposts = await fetchLatestBlogposts({ + token, + cloud_id, + }); + + return blogposts?.length > 0 ? blogposts[0] : null; + }, + event_info: { + desc: 'Confluence New Blogpost Trigger Event Info', + type: { + type: 'hash', + fields: { + id: { + type: 'string', + }, + status: { + type: 'string', + }, + title: { + type: 'string', + }, + spaceId: { + type: 'string', + }, + authorId: { + type: 'string', + }, + createdAt: { + type: 'string', + }, + version: { + type: { + type: 'hash', + fields: { + number: { + type: 'number', + }, + authorId: { + type: 'string', + }, + message: { + type: 'string', + }, + createdAt: { + type: 'string', + }, + }, + }, + }, + body: { + type: { + type: 'hash', + fields: { + storage: { + type: { + type: 'hash', + fields: { + value: { + type: 'string', + }, + representation: { + type: 'string', + }, + }, + }, + }, + atlas_doc_format: { + type: { + type: 'hash', + fields: { + value: { + type: 'string', + }, + representation: { + type: 'string', + }, + }, + }, + }, + view: { + type: { + type: 'hash', + fields: { + value: { + type: 'string', + }, + representation: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + _links: { + type: { + type: 'hash', + fields: { + editui: { + type: 'string', + }, + webui: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, +}); + +export default ConfluenceNewBlogpostTrigger; + +const fetchLatestBlogposts = async (options: { + token: string; + cloud_id: string; + space_id?: string; + status?: string[]; + body_format?: string; +}) => { + const { token, cloud_id, space_id, status, body_format } = options; + const limit = DEFAULT_TRIGGER_POLL_ITEM_LIMIT; + + try { + const params: Record = { + limit: limit.toString(), + sort: '-created-date', + }; + + if (space_id) { + params['space-id'] = space_id; + } + + if (status && status.length > 0) { + params.status = status.join(','); + } + + if (body_format) { + params['body-format'] = body_format; + } + + const response = await QorusRequest.get<{ data: { results: ConfluenceBlogpost[] } }>( + { + path: `/blogposts`, + params, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }, + { + url: `https://api.atlassian.com/ex/confluence/${cloud_id}/wiki/api/v2`, + endpointId: CONFLUENCE_APP_NAME, + } + ); + + const blogposts = response?.data?.results || []; + + if (blogposts.length === 0) { + return []; + } + + return blogposts; + } catch (error) { + throw new ConfluenceError(`Failed to fetch latest blogposts: ${error.message || error}`); + } +}; diff --git a/ts/src/apps/confluence/triggers/new-page.trigger.ts b/ts/src/apps/confluence/triggers/new-page.trigger.ts new file mode 100644 index 00000000..f36cdd3e --- /dev/null +++ b/ts/src/apps/confluence/triggers/new-page.trigger.ts @@ -0,0 +1,292 @@ +import { EQoreAppActionCode, QoreAppCreator, QorusRequest } from '@qoretechnologies/ts-toolkit'; +import { DEFAULT_TRIGGER_POLL_ITEM_LIMIT } from '../../../global/constants'; +import { getQoreContextRequiredValues } from '../../../global/helpers'; +import { pollCreatedItemsForTrigger } from '../../../global/helpers/event-triggers'; +import { CONFLUENCE_APP_NAME, ConfluenceError } from '../constants'; +import { getConfluenceSpaceIdAllowedValues } from '../helpers/get-space-id-allowed-values'; + +type ConfluencePage = { + id: string; + status: string; + title: string; + spaceId: string; + authorId: string; + createdAt: string; + version: { + number: number; + authorId: string; + message: string; + createdAt: string; + }; + body?: { + storage?: { + value: string; + representation: string; + }; + atlas_doc_format?: { + value: string; + representation: string; + }; + }; + _links: { + editui: string; + webui: string; + }; +}; + +const ConfluenceNewPageTrigger = QoreAppCreator.createLocalizedTrigger({ + app: CONFLUENCE_APP_NAME, + action: 'new_page', + action_code: EQoreAppActionCode.EVENT, + options: { + space_id: { + type: 'string', + required: false, + get_allowed_values: getConfluenceSpaceIdAllowedValues, + }, + status: { + type: { + type: 'list', + element_type: 'string', + }, + required: false, + element_allowed_values: [ + { + value: 'current', + display_name: 'Current', + desc: 'Pages that are currently active and published', + }, + { + value: 'archived', + display_name: 'Archived', + desc: 'Pages that have been archived', + }, + { + value: 'deleted', + display_name: 'Deleted', + desc: 'Pages that have been deleted', + }, + { + value: 'trashed', + display_name: 'Trashed', + desc: 'Pages that have been moved to trash', + }, + ], + }, + body_format: { + type: 'string', + required: false, + allowed_values: [ + { + value: 'storage', + display_name: 'Storage Format', + desc: 'Confluence storage format (XML-based)', + }, + { + value: 'atlas_doc_format', + display_name: 'Atlas Document Format', + desc: 'Atlassian Document Format (ADF)', + }, + ], + }, + subtype: { + type: 'string', + required: false, + allowed_values: [ + { + value: 'live', + display_name: 'Live Pages', + desc: 'Live pages in Confluence', + }, + { + value: 'page', + display_name: 'Regular Pages', + desc: 'Regular Confluence pages', + }, + ], + }, + }, + event_function: async (context, update, should_stop) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const space_id = context.opts?.space_id; + const status = context.opts?.status as string[] | undefined; + const body_format = context.opts?.body_format; + const subtype = context.opts?.subtype; + + const getItems = () => { + return fetchLatestPages({ + token, + cloud_id, + space_id, + status, + body_format, + subtype, + }); + }; + + await pollCreatedItemsForTrigger({ + trigger_name: 'confluence_new_page', + uniqueField: 'id', + getItems, + update, + should_stop, + }); + }, + get_example_event_data: async (context) => { + const { token, cloud_id } = getQoreContextRequiredValues({ + context, + connectionFields: ['token', 'cloud_id'], + ErrorClass: ConfluenceError, + }); + + const pages = await fetchLatestPages({ + token, + cloud_id, + }); + + return pages?.length > 0 ? pages[0] : null; + }, + event_info: { + desc: 'Confluence New Page Trigger Event Info', + type: { + type: 'hash', + fields: { + id: { type: 'string' }, + title: { type: 'string' }, + lastOwnerId: { type: 'string' }, + status: { type: 'string' }, + parentId: { type: 'string' }, + createdAt: { type: 'string' }, + spaceId: { type: 'string' }, + ownerId: { type: 'string' }, + authorId: { type: 'string' }, + position: { type: 'number' }, + parentType: { type: 'string' }, + version: { + type: { + type: 'hash', + fields: { + number: { type: 'number' }, + message: { type: 'string' }, + minorEdit: { type: 'boolean' }, + authorId: { type: 'string' }, + createdAt: { type: 'string' }, + nscStepVersion: { type: 'number' }, + }, + }, + }, + body: { + type: { + type: 'hash', + fields: { + storage: { + type: { + type: 'hash', + fields: { + value: { + type: 'string', + }, + representation: { + type: 'string', + }, + }, + }, + }, + atlas_doc_format: { + type: { + type: 'hash', + fields: { + value: { + type: 'string', + }, + representation: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + _links: { + type: { + type: 'hash', + fields: { + editui: { type: 'string' }, + webui: { type: 'string' }, + edituiv2: { type: 'string' }, + tinyui: { type: 'string' }, + }, + }, + }, + }, + }, + }, +}); + +export default ConfluenceNewPageTrigger; + +const fetchLatestPages = async (options: { + token: string; + cloud_id: string; + space_id?: string; + status?: string[]; + body_format?: string; + subtype?: string; +}) => { + const { token, cloud_id, space_id, status, body_format, subtype } = options; + const limit = DEFAULT_TRIGGER_POLL_ITEM_LIMIT; + + try { + const params: Record = { + limit: limit.toString(), + sort: '-created-date', + }; + + if (space_id) { + params['space-id'] = space_id; + } + + if (status && status.length > 0) { + params.status = status.join(','); + } + + if (body_format) { + params['body-format'] = body_format; + } + + if (subtype) { + params.subtype = subtype; + } + + const response = await QorusRequest.get<{ data: { results: ConfluencePage[] } }>( + { + path: `/pages`, + params, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }, + { + url: `https://api.atlassian.com/ex/confluence/${cloud_id}/wiki/api/v2`, + endpointId: CONFLUENCE_APP_NAME, + } + ); + + const pages = response?.data?.results || []; + + if (pages.length === 0) { + return []; + } + + return pages; + } catch (error) { + throw new ConfluenceError(`Failed to fetch latest pages: ${error.message || error}`); + } +}; diff --git a/ts/src/apps/zoom/index.ts b/ts/src/apps/zoom/index.ts index d86a9828..186b1f14 100644 --- a/ts/src/apps/zoom/index.ts +++ b/ts/src/apps/zoom/index.ts @@ -24,7 +24,6 @@ export default (locale: Locales) => url: 'https://api.zoom.us/v2', data: 'json', oauth2_grant_type: 'authorization_code', - oauth2_client_id: 'your-zoom-client-id', oauth2_client_secret: actionsCatalogue.getOauth2ClientSecret(ZOOM_APP_NAME), oauth2_auth_url: 'https://zoom.us/oauth/authorize', oauth2_token_url: 'https://zoom.us/oauth/token', diff --git a/ts/src/i18n/en/apps/Confluence/index.ts b/ts/src/i18n/en/apps/Confluence/index.ts new file mode 100644 index 00000000..8f68ff5a --- /dev/null +++ b/ts/src/i18n/en/apps/Confluence/index.ts @@ -0,0 +1,83 @@ +/* eslint-disable max-len */ +const ConfluenceAppEn = { + displayName: 'Confluence', + shortDesc: + 'Confluence is a collaboration tool used to help teams collaborate and share knowledge efficiently.', + longDesc: + 'Confluence is a powerful collaboration tool that allows teams to create, share, and manage content in a centralized platform. It is designed to enhance team productivity by providing a space for documentation, project management, and knowledge sharing. With features like real-time editing, commenting, and integration with other tools, Confluence helps teams work together more effectively.', + triggers: { + new_attachment: { + displayName: 'New Attachment', + shortDesc: 'Triggers when a new attachment is uploaded to Confluence.', + longDesc: + 'Monitors Confluence for newly uploaded attachments with optional filtering by status and media type. Triggers when new attachments are detected.', + options: { + status: { + displayName: 'Status', + shortDesc: 'Filter by attachment status', + longDesc: 'Filter attachments by their status (current, archived, or trashed).', + }, + mediaType: { + displayName: 'Media Type', + shortDesc: 'Filter by media type', + longDesc: 'Filter attachments by their media type (e.g., image/png, application/pdf).', + }, + }, + }, + new_blogpost: { + displayName: 'New Blogpost', + shortDesc: 'Triggers when a new blogpost is created in Confluence.', + longDesc: + 'Monitors Confluence for newly created blogposts with optional filtering by space, status, and body format. Triggers when new blogposts are detected.', + options: { + space_id: { + displayName: 'Space ID', + shortDesc: 'Filter by Confluence space', + longDesc: 'Filter blogposts by the Confluence space they belong to.', + }, + status: { + displayName: 'Status', + shortDesc: 'Filter by blogpost status', + longDesc: 'Filter blogposts by their status (current, deleted, or trashed).', + }, + body_format: { + displayName: 'Body Format', + shortDesc: 'Content format to retrieve', + longDesc: + 'The format to retrieve the blogpost content in (storage format or Atlas Document Format).', + }, + }, + }, + new_page: { + displayName: 'New Page', + shortDesc: 'Triggers when a new page is created in Confluence.', + longDesc: + 'Monitors Confluence for newly created pages with optional filtering by space, status, body format, and subtype. Triggers when new pages are detected.', + options: { + space_id: { + displayName: 'Space ID', + shortDesc: 'Filter by Confluence space', + longDesc: 'Filter pages by the Confluence space they belong to.', + }, + status: { + displayName: 'Status', + shortDesc: 'Filter by page status', + longDesc: 'Filter pages by their status (current, archived, deleted, or trashed).', + }, + body_format: { + displayName: 'Body Format', + shortDesc: 'Content format to retrieve', + longDesc: + 'The format to retrieve the page content in (storage format or Atlas Document Format).', + }, + subtype: { + displayName: 'Subtype', + shortDesc: 'Filter by page subtype', + longDesc: 'Filter pages by their subtype (live pages or regular pages).', + }, + }, + }, + }, +}; + +export default ConfluenceAppEn; diff --git a/ts/src/i18n/en/index.ts b/ts/src/i18n/en/index.ts index 082f37be..3a4e813d 100644 --- a/ts/src/i18n/en/index.ts +++ b/ts/src/i18n/en/index.ts @@ -30,6 +30,7 @@ import Teams from './apps/Teams'; import Xero from './apps/Xero'; import Zendesk from './apps/Zendesk'; import Zoom from './apps/Zoom'; +import Confluence from './apps/Confluence'; const en = { common: {}, @@ -141,6 +142,7 @@ const en = { Magento, Shopify, Zoom, + Confluence, }, } satisfies BaseTranslation; diff --git a/ts/src/i18n/i18n-types.ts b/ts/src/i18n/i18n-types.ts index 9b0f8601..4669caa0 100644 --- a/ts/src/i18n/i18n-types.ts +++ b/ts/src/i18n/i18n-types.ts @@ -29494,6 +29494,196 @@ type RootTranslation = { } } } + Confluence: { + /** + * C​o​n​f​l​u​e​n​c​e + */ + displayName: string + /** + * C​o​n​f​l​u​e​n​c​e​ ​i​s​ ​a​ ​c​o​l​l​a​b​o​r​a​t​i​o​n​ ​t​o​o​l​ ​u​s​e​d​ ​t​o​ ​h​e​l​p​ ​t​e​a​m​s​ ​c​o​l​l​a​b​o​r​a​t​e​ ​a​n​d​ ​s​h​a​r​e​ ​k​n​o​w​l​e​d​g​e​ ​e​f​f​i​c​i​e​n​t​l​y​. + */ + shortDesc: string + /** + * C​o​n​f​l​u​e​n​c​e​ ​i​s​ ​a​ ​p​o​w​e​r​f​u​l​ ​c​o​l​l​a​b​o​r​a​t​i​o​n​ ​t​o​o​l​ ​t​h​a​t​ ​a​l​l​o​w​s​ ​t​e​a​m​s​ ​t​o​ ​c​r​e​a​t​e​,​ ​s​h​a​r​e​,​ ​a​n​d​ ​m​a​n​a​g​e​ ​c​o​n​t​e​n​t​ ​i​n​ ​a​ ​c​e​n​t​r​a​l​i​z​e​d​ ​p​l​a​t​f​o​r​m​.​ ​I​t​ ​i​s​ ​d​e​s​i​g​n​e​d​ ​t​o​ ​e​n​h​a​n​c​e​ ​t​e​a​m​ ​p​r​o​d​u​c​t​i​v​i​t​y​ ​b​y​ ​p​r​o​v​i​d​i​n​g​ ​a​ ​s​p​a​c​e​ ​f​o​r​ ​d​o​c​u​m​e​n​t​a​t​i​o​n​,​ ​p​r​o​j​e​c​t​ ​m​a​n​a​g​e​m​e​n​t​,​ ​a​n​d​ ​k​n​o​w​l​e​d​g​e​ ​s​h​a​r​i​n​g​.​ ​W​i​t​h​ ​f​e​a​t​u​r​e​s​ ​l​i​k​e​ ​r​e​a​l​-​t​i​m​e​ ​e​d​i​t​i​n​g​,​ ​c​o​m​m​e​n​t​i​n​g​,​ ​a​n​d​ ​i​n​t​e​g​r​a​t​i​o​n​ ​w​i​t​h​ ​o​t​h​e​r​ ​t​o​o​l​s​,​ ​C​o​n​f​l​u​e​n​c​e​ ​h​e​l​p​s​ ​t​e​a​m​s​ ​w​o​r​k​ ​t​o​g​e​t​h​e​r​ ​m​o​r​e​ ​e​f​f​e​c​t​i​v​e​l​y​. + */ + longDesc: string + triggers: { + new_attachment: { + /** + * N​e​w​ ​A​t​t​a​c​h​m​e​n​t + */ + displayName: string + /** + * T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​a​ ​n​e​w​ ​a​t​t​a​c​h​m​e​n​t​ ​i​s​ ​u​p​l​o​a​d​e​d​ ​t​o​ ​C​o​n​f​l​u​e​n​c​e​. + */ + shortDesc: string + /** + * M​o​n​i​t​o​r​s​ ​C​o​n​f​l​u​e​n​c​e​ ​f​o​r​ ​n​e​w​l​y​ ​u​p​l​o​a​d​e​d​ ​a​t​t​a​c​h​m​e​n​t​s​ ​w​i​t​h​ ​o​p​t​i​o​n​a​l​ ​f​i​l​t​e​r​i​n​g​ ​b​y​ ​s​t​a​t​u​s​ ​a​n​d​ ​m​e​d​i​a​ ​t​y​p​e​.​ ​T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​n​e​w​ ​a​t​t​a​c​h​m​e​n​t​s​ ​a​r​e​ ​d​e​t​e​c​t​e​d​. + */ + longDesc: string + options: { + status: { + /** + * S​t​a​t​u​s + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​a​t​t​a​c​h​m​e​n​t​ ​s​t​a​t​u​s + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​a​t​t​a​c​h​m​e​n​t​s​ ​b​y​ ​t​h​e​i​r​ ​s​t​a​t​u​s​ ​(​c​u​r​r​e​n​t​,​ ​a​r​c​h​i​v​e​d​,​ ​o​r​ ​t​r​a​s​h​e​d​)​. + */ + longDesc: string + } + mediaType: { + /** + * M​e​d​i​a​ ​T​y​p​e + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​m​e​d​i​a​ ​t​y​p​e + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​a​t​t​a​c​h​m​e​n​t​s​ ​b​y​ ​t​h​e​i​r​ ​m​e​d​i​a​ ​t​y​p​e​ ​(​e​.​g​.​,​ ​i​m​a​g​e​/​p​n​g​,​ ​a​p​p​l​i​c​a​t​i​o​n​/​p​d​f​)​. + */ + longDesc: string + } + } + } + new_blogpost: { + /** + * N​e​w​ ​B​l​o​g​p​o​s​t + */ + displayName: string + /** + * T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​a​ ​n​e​w​ ​b​l​o​g​p​o​s​t​ ​i​s​ ​c​r​e​a​t​e​d​ ​i​n​ ​C​o​n​f​l​u​e​n​c​e​. + */ + shortDesc: string + /** + * M​o​n​i​t​o​r​s​ ​C​o​n​f​l​u​e​n​c​e​ ​f​o​r​ ​n​e​w​l​y​ ​c​r​e​a​t​e​d​ ​b​l​o​g​p​o​s​t​s​ ​w​i​t​h​ ​o​p​t​i​o​n​a​l​ ​f​i​l​t​e​r​i​n​g​ ​b​y​ ​s​p​a​c​e​,​ ​s​t​a​t​u​s​,​ ​a​n​d​ ​b​o​d​y​ ​f​o​r​m​a​t​.​ ​T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​n​e​w​ ​b​l​o​g​p​o​s​t​s​ ​a​r​e​ ​d​e​t​e​c​t​e​d​. + */ + longDesc: string + options: { + space_id: { + /** + * S​p​a​c​e​ ​I​D + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​C​o​n​f​l​u​e​n​c​e​ ​s​p​a​c​e + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​b​l​o​g​p​o​s​t​s​ ​b​y​ ​t​h​e​ ​C​o​n​f​l​u​e​n​c​e​ ​s​p​a​c​e​ ​t​h​e​y​ ​b​e​l​o​n​g​ ​t​o​. + */ + longDesc: string + } + status: { + /** + * S​t​a​t​u​s + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​b​l​o​g​p​o​s​t​ ​s​t​a​t​u​s + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​b​l​o​g​p​o​s​t​s​ ​b​y​ ​t​h​e​i​r​ ​s​t​a​t​u​s​ ​(​c​u​r​r​e​n​t​,​ ​d​e​l​e​t​e​d​,​ ​o​r​ ​t​r​a​s​h​e​d​)​. + */ + longDesc: string + } + body_format: { + /** + * B​o​d​y​ ​F​o​r​m​a​t + */ + displayName: string + /** + * C​o​n​t​e​n​t​ ​f​o​r​m​a​t​ ​t​o​ ​r​e​t​r​i​e​v​e + */ + shortDesc: string + /** + * T​h​e​ ​f​o​r​m​a​t​ ​t​o​ ​r​e​t​r​i​e​v​e​ ​t​h​e​ ​b​l​o​g​p​o​s​t​ ​c​o​n​t​e​n​t​ ​i​n​ ​(​s​t​o​r​a​g​e​ ​f​o​r​m​a​t​ ​o​r​ ​A​t​l​a​s​ ​D​o​c​u​m​e​n​t​ ​F​o​r​m​a​t​)​. + */ + longDesc: string + } + } + } + new_page: { + /** + * N​e​w​ ​P​a​g​e + */ + displayName: string + /** + * T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​a​ ​n​e​w​ ​p​a​g​e​ ​i​s​ ​c​r​e​a​t​e​d​ ​i​n​ ​C​o​n​f​l​u​e​n​c​e​. + */ + shortDesc: string + /** + * M​o​n​i​t​o​r​s​ ​C​o​n​f​l​u​e​n​c​e​ ​f​o​r​ ​n​e​w​l​y​ ​c​r​e​a​t​e​d​ ​p​a​g​e​s​ ​w​i​t​h​ ​o​p​t​i​o​n​a​l​ ​f​i​l​t​e​r​i​n​g​ ​b​y​ ​s​p​a​c​e​,​ ​s​t​a​t​u​s​,​ ​b​o​d​y​ ​f​o​r​m​a​t​,​ ​a​n​d​ ​s​u​b​t​y​p​e​.​ ​T​r​i​g​g​e​r​s​ ​w​h​e​n​ ​n​e​w​ ​p​a​g​e​s​ ​a​r​e​ ​d​e​t​e​c​t​e​d​. + */ + longDesc: string + options: { + space_id: { + /** + * S​p​a​c​e​ ​I​D + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​C​o​n​f​l​u​e​n​c​e​ ​s​p​a​c​e + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​p​a​g​e​s​ ​b​y​ ​t​h​e​ ​C​o​n​f​l​u​e​n​c​e​ ​s​p​a​c​e​ ​t​h​e​y​ ​b​e​l​o​n​g​ ​t​o​. + */ + longDesc: string + } + status: { + /** + * S​t​a​t​u​s + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​p​a​g​e​ ​s​t​a​t​u​s + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​p​a​g​e​s​ ​b​y​ ​t​h​e​i​r​ ​s​t​a​t​u​s​ ​(​c​u​r​r​e​n​t​,​ ​a​r​c​h​i​v​e​d​,​ ​d​e​l​e​t​e​d​,​ ​o​r​ ​t​r​a​s​h​e​d​)​. + */ + longDesc: string + } + body_format: { + /** + * B​o​d​y​ ​F​o​r​m​a​t + */ + displayName: string + /** + * C​o​n​t​e​n​t​ ​f​o​r​m​a​t​ ​t​o​ ​r​e​t​r​i​e​v​e + */ + shortDesc: string + /** + * T​h​e​ ​f​o​r​m​a​t​ ​t​o​ ​r​e​t​r​i​e​v​e​ ​t​h​e​ ​p​a​g​e​ ​c​o​n​t​e​n​t​ ​i​n​ ​(​s​t​o​r​a​g​e​ ​f​o​r​m​a​t​ ​o​r​ ​A​t​l​a​s​ ​D​o​c​u​m​e​n​t​ ​F​o​r​m​a​t​)​. + */ + longDesc: string + } + subtype: { + /** + * S​u​b​t​y​p​e + */ + displayName: string + /** + * F​i​l​t​e​r​ ​b​y​ ​p​a​g​e​ ​s​u​b​t​y​p​e + */ + shortDesc: string + /** + * F​i​l​t​e​r​ ​p​a​g​e​s​ ​b​y​ ​t​h​e​i​r​ ​s​u​b​t​y​p​e​ ​(​l​i​v​e​ ​p​a​g​e​s​ ​o​r​ ​r​e​g​u​l​a​r​ ​p​a​g​e​s​)​. + */ + longDesc: string + } + } + } + } + } } } @@ -58979,6 +59169,196 @@ export type TranslationFunctions = { } } } + Confluence: { + /** + * Confluence + */ + displayName: () => LocalizedString + /** + * Confluence is a collaboration tool used to help teams collaborate and share knowledge efficiently. + */ + shortDesc: () => LocalizedString + /** + * Confluence is a powerful collaboration tool that allows teams to create, share, and manage content in a centralized platform. It is designed to enhance team productivity by providing a space for documentation, project management, and knowledge sharing. With features like real-time editing, commenting, and integration with other tools, Confluence helps teams work together more effectively. + */ + longDesc: () => LocalizedString + triggers: { + new_attachment: { + /** + * New Attachment + */ + displayName: () => LocalizedString + /** + * Triggers when a new attachment is uploaded to Confluence. + */ + shortDesc: () => LocalizedString + /** + * Monitors Confluence for newly uploaded attachments with optional filtering by status and media type. Triggers when new attachments are detected. + */ + longDesc: () => LocalizedString + options: { + status: { + /** + * Status + */ + displayName: () => LocalizedString + /** + * Filter by attachment status + */ + shortDesc: () => LocalizedString + /** + * Filter attachments by their status (current, archived, or trashed). + */ + longDesc: () => LocalizedString + } + mediaType: { + /** + * Media Type + */ + displayName: () => LocalizedString + /** + * Filter by media type + */ + shortDesc: () => LocalizedString + /** + * Filter attachments by their media type (e.g., image/png, application/pdf). + */ + longDesc: () => LocalizedString + } + } + } + new_blogpost: { + /** + * New Blogpost + */ + displayName: () => LocalizedString + /** + * Triggers when a new blogpost is created in Confluence. + */ + shortDesc: () => LocalizedString + /** + * Monitors Confluence for newly created blogposts with optional filtering by space, status, and body format. Triggers when new blogposts are detected. + */ + longDesc: () => LocalizedString + options: { + space_id: { + /** + * Space ID + */ + displayName: () => LocalizedString + /** + * Filter by Confluence space + */ + shortDesc: () => LocalizedString + /** + * Filter blogposts by the Confluence space they belong to. + */ + longDesc: () => LocalizedString + } + status: { + /** + * Status + */ + displayName: () => LocalizedString + /** + * Filter by blogpost status + */ + shortDesc: () => LocalizedString + /** + * Filter blogposts by their status (current, deleted, or trashed). + */ + longDesc: () => LocalizedString + } + body_format: { + /** + * Body Format + */ + displayName: () => LocalizedString + /** + * Content format to retrieve + */ + shortDesc: () => LocalizedString + /** + * The format to retrieve the blogpost content in (storage format or Atlas Document Format). + */ + longDesc: () => LocalizedString + } + } + } + new_page: { + /** + * New Page + */ + displayName: () => LocalizedString + /** + * Triggers when a new page is created in Confluence. + */ + shortDesc: () => LocalizedString + /** + * Monitors Confluence for newly created pages with optional filtering by space, status, body format, and subtype. Triggers when new pages are detected. + */ + longDesc: () => LocalizedString + options: { + space_id: { + /** + * Space ID + */ + displayName: () => LocalizedString + /** + * Filter by Confluence space + */ + shortDesc: () => LocalizedString + /** + * Filter pages by the Confluence space they belong to. + */ + longDesc: () => LocalizedString + } + status: { + /** + * Status + */ + displayName: () => LocalizedString + /** + * Filter by page status + */ + shortDesc: () => LocalizedString + /** + * Filter pages by their status (current, archived, deleted, or trashed). + */ + longDesc: () => LocalizedString + } + body_format: { + /** + * Body Format + */ + displayName: () => LocalizedString + /** + * Content format to retrieve + */ + shortDesc: () => LocalizedString + /** + * The format to retrieve the page content in (storage format or Atlas Document Format). + */ + longDesc: () => LocalizedString + } + subtype: { + /** + * Subtype + */ + displayName: () => LocalizedString + /** + * Filter by page subtype + */ + shortDesc: () => LocalizedString + /** + * Filter pages by their subtype (live pages or regular pages). + */ + longDesc: () => LocalizedString + } + } + } + } + } } } diff --git a/ts/src/qtests/confluence.qtest.ts b/ts/src/qtests/confluence.qtest.ts new file mode 100644 index 00000000..75c9de11 --- /dev/null +++ b/ts/src/qtests/confluence.qtest.ts @@ -0,0 +1,219 @@ +describe('Tests Confluence Actions', () => { + let connection: string; + beforeAll(() => { + const username = process.env.CONFLUENCE_USERNAME; + const password = process.env.CONFLUENCE_PASSWORD; + const cloudId = process.env.CONFLUENCE_CLOUD_ID; + if (!username || !password || !cloudId) { + throw new Error('Missing required environment variables for Confluence tests'); + } + connection = testApi.createConnection('confluence', { + opts: { + username: username!, + password: password!, + cloud_id: cloudId!, + swagger_base_path: `/ex/confluence/${cloudId}/wiki/api/v2`, + oauth2_grant_type: 'none', + ping_path: '', + } as any, + }); + expect(connection).toBeDefined(); + }); + + // const base_context = { + // conn_opts: { + // token: process.env.CONFLUENCE_TOKEN, + // cloud_id: process.env.CONFLUENCE_CLOUD_ID, + // } as any, + // }; + + // describe('Should test Confluence allowed values', () => { + // it('Should get blogpost allowed values', async () => { + // const allowed_values = await getConfluenceBlogpostIdAllowedValues(base_context); + // expect(allowed_values).toBeDefined(); + // expect(allowed_values.length).toBeGreaterThan(0); + // }); + // it('Should get task allowed values', async () => { + // const allowed_values = await getConfluenceTaskIdAllowedValues(base_context); + // expect(allowed_values).toBeDefined(); + // expect(allowed_values.length).toBeGreaterThan(0); + // }); + // it('Should get space allowed values', async () => { + // const allowed_values = await getConfluenceSpaceIdAllowedValues(base_context); + // expect(allowed_values).toBeDefined(); + // expect(allowed_values.length).toBeGreaterThan(0); + // }); + // it('Should get page allowed values', async () => { + // const allowed_values = await getConfluencePageIdAllowedValues(base_context); + // expect(allowed_values).toBeDefined(); + // expect(allowed_values.length).toBeGreaterThan(0); + // }); + // it('Should get label allowed values', async () => { + // const allowed_values = await getConfluencePageIdAllowedValues(base_context); + // expect(allowed_values).toBeDefined(); + // expect(allowed_values.length).toBeGreaterThan(0); + // }); + // }); + + describe('Should test Confluence actions', () => { + let createdBlogpostId: string | undefined; + let spaceId: string | undefined; + let createdPageId: string | undefined; + let taskId: string | undefined; + + it('Should get spaces', async () => { + const response = await testApi.execAppAction('confluence', 'getSpaces', connection, {}); + + expect(response).toBeDefined(); + expect(response.results).toBeDefined(); + expect(Array.isArray(response.results)).toBe(true); + expect(response.results.length).toBeGreaterThan(0); + + spaceId = response.results[0].id; + }); + + it('Should get space by id', async () => { + expect(spaceId).toBeDefined(); + + const { body } = await testApi.execAppAction('confluence', 'getSpaceById', connection, { + id: spaceId, + }); + + expect(body).toBeDefined(); + expect(body.id).toBe(spaceId); + }); + + it('Should create a blogpost', async () => { + const { body } = await testApi.execAppAction('confluence', 'createBlogPost', connection, { + spaceId, + title: 'Test Blog Post', + }); + + expect(body).toBeDefined(); + expect(body.id).toBeDefined(); + + createdBlogpostId = body.id; + }); + + it('Should get Blog post by id', async () => { + expect(createdBlogpostId).toBeDefined(); + + const { body } = await testApi.execAppAction('confluence', 'getBlogPostById', connection, { + id: createdBlogpostId, + }); + + expect(body).toBeDefined(); + expect(body.id).toBe(createdBlogpostId); + }); + + it('Should list blog posts', async () => { + const response = await testApi.execAppAction('confluence', 'getBlogPosts', connection, {}); + + expect(response).toBeDefined(); + expect(response.results).toBeDefined(); + expect(Array.isArray(response.results)).toBe(true); + expect(response.results.length).toBeGreaterThan(0); + }); + + it('Should delete blog post', async () => { + expect(createdBlogpostId).toBeDefined(); + + const response = await testApi.execAppAction('confluence', 'deleteBlogPost', connection, { + id: createdBlogpostId, + }); + + expect(response).toBeDefined(); + }); + + it('Should create a page', async () => { + const { body } = await testApi.execAppAction('confluence', 'createPage', connection, { + title: 'Test Page', + spaceId, + }); + + expect(body).toBeDefined(); + expect(body.id).toBeDefined(); + + createdPageId = body.id; + }); + + it('Should get page by id', async () => { + expect(createdPageId).toBeDefined(); + + const { body } = await testApi.execAppAction('confluence', 'getPageById', connection, { + id: createdPageId, + }); + + expect(body).toBeDefined(); + expect(body.id).toBe(createdPageId); + }); + + it('Should update page title', async () => { + expect(createdPageId).toBeDefined(); + + const response = await testApi.execAppAction('confluence', 'updatePageTitle', connection, { + id: createdPageId, + status: 'current', + title: 'Updated Test Page', + }); + + expect(response).toBeDefined(); + }); + + it('Should list pages in space', async () => { + expect(spaceId).toBeDefined(); + + const response = await testApi.execAppAction('confluence', 'getPagesInSpace', connection, { + id: spaceId, + }); + + expect(response).toBeDefined(); + expect(response.results).toBeDefined(); + expect(Array.isArray(response.results)).toBe(true); + expect(response.results.length).toBeGreaterThan(0); + }); + + it('Should delete page', async () => { + expect(createdPageId).toBeDefined(); + + const response = await testApi.execAppAction('confluence', 'deletePage', connection, { + id: createdPageId, + }); + + expect(response).toBeDefined(); + }); + + it('Should get tasks', async () => { + const response = await testApi.execAppAction('confluence', 'getTasks', connection, {}); + + expect(response).toBeDefined(); + expect(response.results).toBeDefined(); + expect(Array.isArray(response.results)).toBe(true); + expect(response.results.length).toBeGreaterThan(0); + + taskId = response.results[0].id; + }); + + it('Should get task by id', async () => { + expect(taskId).toBeDefined(); + + const { body } = await testApi.execAppAction('confluence', 'getTaskById', connection, { + id: taskId, + }); + + expect(body).toBeDefined(); + expect(body.id).toBe(taskId); + }); + + it('Should update task', async () => { + expect(taskId).toBeDefined(); + + const response = await testApi.execAppAction('confluence', 'updateTask', connection, { + id: taskId, + status: 'complete', + }); + + expect(response).toBeDefined(); + }); + }); +}); diff --git a/ts/src/schemas/confluence.swagger.json b/ts/src/schemas/confluence.swagger.json new file mode 100644 index 00000000..a33c2ebb --- /dev/null +++ b/ts/src/schemas/confluence.swagger.json @@ -0,0 +1,24701 @@ +{ + "swagger": "2.0", + "info": { + "description": "This document describes Confluence's v2 APIs. This is intended to be an iteration on the existing Confluence Cloud REST API with improvements in both endpoint definitions and performance.", + "termsOfService": "https://developer.atlassian.com/platform/marketplace/atlassian-developer-terms/", + "title": "The Confluence Cloud REST API v2", + "version": "2.0.0" + }, + "host": "no-default", + "basePath": "/wiki/api/v2", + "schemes": [ + "https" + ], + "paths": { + "/admin-key": { + "delete": { + "parameters": [], + "responses": { + "204": { + "description": "Returned if admin key access was successfully disabled for the calling user or if the user did not have an admin key in the first place." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing from the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to use admin keys or if the site is not a Confluence Cloud Premium or Enterprise instance." + } + }, + "tags": [ + "Admin Key" + ], + "description": "Disables admin key access for the calling user within the site.\n\n**[Permissions](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Centralized-user-management-content) required**:\nUser must be an organization or site admin.", + "operationId": "disableAdminKey", + "summary": "Disable Admin Key", + "x-atlassian-connect-scope": "INACCESSIBLE" + }, + "get": { + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "Returned if an admin key is currently enabled for the calling user.", + "schema": { + "$ref": "#/definitions/AdminKeyResponse" + } + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing from the request." + }, + "404": { + "description": "Returned if the calling user does not currently have an admin key, if the calling user does not have permission to use admin keys, or if the site is not a Confluence Cloud Premium or Enterprise instance." + } + }, + "tags": [ + "Admin Key" + ], + "description": "Returns information about the admin key if one is currently enabled for the calling user within the site.\n\n**[Permissions](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Centralized-user-management-content) required**:\nUser must be an organization or site admin.", + "operationId": "getAdminKey", + "summary": "Get Admin Key", + "x-atlassian-connect-scope": "INACCESSIBLE" + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "properties": { + "durationInMinutes": { + "description": "The requested duration of admin key access in minutes, up to a maximum of 60 minutes, after which the issued admin key will automatically expire.", + "example": 60, + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "Returned if a new admin key is successfully issued for the calling user.", + "schema": { + "$ref": "#/definitions/AdminKeyResponse" + } + }, + "400": { + "description": "Returned if the request body contains an invalid `durationInMinutes`." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing from the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to use admin keys or if the site is not a Confluence Cloud Premium or Enterprise instance." + } + }, + "tags": [ + "Admin Key" + ], + "description": "Enables admin key access for the calling user within the site. If an admin key already exists for the user, a new one will be issued with an updated expiration time.\n\n**Note:** The `durationInMinutes` field within the request body is optional. If the request body is empty or if the `durationInMinutes` is set to 0 minutes, a new admin key will be issued to the calling user with a default duration of 10 minutes.\n\n**[Permissions](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Centralized-user-management-content) required**:\nUser must be an organization or site admin.", + "operationId": "enableAdminKey", + "summary": "Enable Admin Key", + "x-atlassian-connect-scope": "INACCESSIBLE" + } + }, + "/attachments": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "Used to sort the result by a particular field.", + "enum": [ + "created-date", + "-created-date", + "modified-date", + "-modified-date" + ], + "in": "query", + "name": "sort", + "required": false, + "type": "string" + }, + { + "description": "Used for pagination, this opaque cursor will be returned in the `next` URL in the `Link` response header. Use the relative URL in the `Link` header to retrieve the `next` set of results.", + "in": "query", + "name": "cursor", + "required": false, + "type": "string" + }, + { + "collectionFormat": "multi", + "description": "Filter the results to attachments based on their status. By default, `current` and `archived` are used.", + "in": "query", + "items": { + "enum": [ + "current", + "archived", + "trashed" + ], + "type": "string" + }, + "name": "status", + "required": false, + "type": "array" + }, + { + "description": "Filters on the mediaType of attachments. Only one may be specified.", + "in": "query", + "name": "mediaType", + "required": false, + "type": "string" + }, + { + "description": "Filters on the file-name of attachments. Only one may be specified.", + "in": "query", + "name": "filename", + "required": false, + "type": "string" + }, + { + "default": 50, + "description": "Maximum number of attachments per result to return. If more results exist, use the `Link` header to retrieve a relative URL that will return the next set of results.", + "format": "int32", + "in": "query", + "maximum": 250, + "minimum": 1, + "name": "limit", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the requested attachments are returned.", + "headers": { + "Link": { + "description": "This header contains URL(s) within angle brackets and a relation description for each URL, describing how the provided URL relates to the incoming request's URL. For example, rel=\"next\" would be the URL necessary to get the next page of information. Example response header format: `Link: >; rel=\"next\", ; rel=\"base\"`\n", + "type": "string" + } + }, + "schema": { + "properties": { + "_links": { + "$ref": "#/definitions/MultiEntityLinks" + }, + "results": { + "items": { + "$ref": "#/definitions/AttachmentBulk" + }, + "type": "array" + } + }, + "title": "MultiEntityResult", + "type": "object" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence" + ] + } + ], + "tags": [ + "Attachment" + ], + "description": "Returns all attachments. The number of results is limited by the `limit` parameter and additional results (if available)\nwill be available through the `next` URL present in the `Link` response header.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the container of the attachment.", + "operationId": "getAttachments", + "summary": "Get attachments", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{attachment-id}/properties": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment for which content properties should be returned.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "Filters the response to return a specific content property with matching key (case sensitive).", + "in": "query", + "name": "key", + "required": false, + "type": "string" + }, + { + "description": "Used to sort the result by a particular field.", + "enum": [ + "key", + "-key" + ], + "in": "query", + "name": "sort", + "required": false, + "type": "string" + }, + { + "description": "Used for pagination, this opaque cursor will be returned in the `next` URL in the `Link` response header. Use the relative URL in the `Link` header to retrieve the `next` set of results.", + "in": "query", + "name": "cursor", + "required": false, + "type": "string" + }, + { + "default": 25, + "description": "Maximum number of attachments per result to return. If more results exist, use the `Link` header to retrieve a relative URL that will return the next set of results.", + "format": "int32", + "in": "query", + "maximum": 250, + "minimum": 1, + "name": "limit", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the requested content properties are successfully retrieved.", + "headers": { + "Link": { + "description": "This header contains URL(s) within angle brackets and a relation description for each URL, describing how the provided URL relates to the incoming request's URL. For example, rel=\"next\" would be the URL necessary to get the next page of information. Example response header format: `Link: /properties?cursor=>; rel=\"next\", ; rel=\"base\"`\n", + "type": "string" + } + }, + "schema": { + "properties": { + "_links": { + "$ref": "#/definitions/MultiEntityLinks" + }, + "results": { + "items": { + "$ref": "#/definitions/ContentProperty" + }, + "type": "array" + } + }, + "title": "MultiEntityResult", + "type": "object" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment or the attachment was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence" + ] + } + ], + "tags": [ + "Content Properties" + ], + "description": "Retrieves all Content Properties tied to a specified attachment.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the attachment.", + "operationId": "getAttachmentContentProperties", + "summary": "Get content properties for attachment", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence" + ], + "state": "Current" + } + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment to create a property for.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "The content property to be created", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ContentPropertyCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "Returned if the content property was created successfully.", + "schema": { + "$ref": "#/definitions/ContentProperty" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment or the attachment was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence", + "write:attachment:confluence" + ] + } + ], + "tags": [ + "Content Properties" + ], + "description": "Creates a new content property for an attachment.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to update the attachment.", + "operationId": "createAttachmentProperty", + "summary": "Create content property for attachment", + "x-atlassian-connect-scope": "WRITE", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence", + "write:attachment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{attachment-id}/properties/{property-id}": { + "delete": { + "parameters": [ + { + "description": "The ID of the attachment the property belongs to.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "The ID of the property to be deleted.", + "format": "int64", + "in": "path", + "name": "property-id", + "required": true, + "type": "integer" + } + ], + "responses": { + "204": { + "description": "Returned if the content property was deleted successfully." + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment or the attachment was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence", + "write:attachment:confluence" + ] + } + ], + "tags": [ + "Content Properties" + ], + "description": "Deletes a content property for an attachment by its id. \n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to attachment the page.", + "operationId": "deleteAttachmentPropertyById", + "summary": "Delete content property for attachment by id", + "x-atlassian-connect-scope": "WRITE", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence", + "write:attachment:confluence" + ], + "state": "Current" + } + ] + }, + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment for which content properties should be returned.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?0-9+", + "required": true, + "type": "string" + }, + { + "description": "The ID of the content property to be returned", + "format": "int64", + "in": "path", + "name": "property-id", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the requested content property is successfully retrieved.", + "schema": { + "$ref": "#/definitions/ContentProperty" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment,the attachment was not found, or the property was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence" + ] + } + ], + "tags": [ + "Content Properties" + ], + "description": "Retrieves a specific Content Property by ID that is attached to a specified attachment.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the attachment.", + "operationId": "getAttachmentContentPropertiesById", + "summary": "Get content property for attachment by id", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence" + ], + "state": "Current" + } + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment the property belongs to.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "The ID of the property to be updated.", + "format": "int64", + "in": "path", + "name": "property-id", + "required": true, + "type": "integer" + }, + { + "description": "The content property to be updated.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ContentPropertyUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Returned if the content property was updated successfully.", + "schema": { + "$ref": "#/definitions/ContentProperty" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment or the attachment was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence", + "write:attachment:confluence" + ] + } + ], + "tags": [ + "Content Properties" + ], + "description": "Update a content property for attachment by its id. \n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to edit the attachment.", + "operationId": "updateAttachmentPropertyById", + "summary": "Update content property for attachment by id", + "x-atlassian-connect-scope": "WRITE", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence", + "write:attachment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{attachment-id}/versions/{version-number}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment for which version details should be returned.", + "in": "path", + "name": "attachment-id", + "pattern": "(att)?0-9+", + "required": true, + "type": "string" + }, + { + "description": "The version number of the attachment to be returned.", + "format": "int64", + "in": "path", + "name": "version-number", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the requested version details are successfully retrieved.", + "schema": { + "$ref": "#/definitions/DetailedVersion" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nspecified attachment, the attachment was not found, or the version number does not exist." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence" + ] + } + ], + "tags": [ + "Version" + ], + "description": "Retrieves version details for the specified attachment and version number.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the attachment.", + "operationId": "getAttachmentVersionDetails", + "summary": "Get version details for attachment version", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{id}": { + "delete": { + "parameters": [ + { + "description": "The ID of the attachment to be deleted.", + "format": "int64", + "in": "path", + "name": "id", + "required": true, + "type": "integer" + }, + { + "default": false, + "description": "If attempting to purge the attachment.", + "in": "query", + "name": "purge", + "required": false, + "type": "boolean" + } + ], + "responses": { + "204": { + "description": "Returned if the attachment was successfully deleted." + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if:\n- The provided attachment does not exist\n- The user does not have permissions to view the container of the attachment\n- The user does not have the needed permissions to delete an attachment in the space" + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "delete:attachment:confluence" + ] + } + ], + "tags": [ + "Attachment" + ], + "description": "Delete an attachment by id.\n\nDeleting an attachment moves the attachment to the trash, where it can be restored later. To permanently delete an attachment (or \"purge\" it),\nthe endpoint must be called on a **trashed** attachment with the following param `purge=true`.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the container of the attachment.\nPermission to delete attachments in the space.\nPermission to administer the space (if attempting to purge).", + "operationId": "deleteAttachment", + "summary": "Delete attachment", + "x-atlassian-connect-scope": "DELETE", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "delete:attachment:confluence" + ], + "state": "Current" + } + ] + }, + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment to be returned. If you don't know the attachment's ID, use Get attachments for page/blogpost/custom content.", + "in": "path", + "name": "id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "Allows you to retrieve a previously published version. Specify the previous version's number to retrieve its details.", + "in": "query", + "name": "version", + "type": "integer" + }, + { + "default": false, + "description": "Includes labels associated with this attachment in the response.\nThe number of results will be limited to 50 and sorted in the default sort order. \nA `meta` and `_links` property will be present to indicate if more results are available and a link to retrieve the rest of the results.", + "in": "query", + "name": "include-labels", + "type": "boolean" + }, + { + "default": false, + "description": "Includes content properties associated with this attachment in the response.\nThe number of results will be limited to 50 and sorted in the default sort order. \nA `meta` and `_links` property will be present to indicate if more results are available and a link to retrieve the rest of the results.", + "in": "query", + "name": "include-properties", + "type": "boolean" + }, + { + "default": false, + "description": "Includes operations associated with this attachment in the response, as defined in the `Operation` object.\nThe number of results will be limited to 50 and sorted in the default sort order. \nA `meta` and `_links` property will be present to indicate if more results are available and a link to retrieve the rest of the results.", + "in": "query", + "name": "include-operations", + "type": "boolean" + }, + { + "default": false, + "description": "Includes versions associated with this attachment in the response.\nThe number of results will be limited to 50 and sorted in the default sort order. \nA `meta` and `_links` property will be present to indicate if more results are available and a link to retrieve the rest of the results.", + "in": "query", + "name": "include-versions", + "type": "boolean" + }, + { + "default": true, + "description": "Includes the current version associated with this attachment in the response.\nBy default this is included and can be omitted by setting the value to `false`.", + "in": "query", + "name": "include-version", + "type": "boolean" + }, + { + "default": false, + "description": "Includes collaborators on the attachment.", + "in": "query", + "name": "include-collaborators", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Returned if the requested attachment is returned.", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/AttachmentSingle" + }, + { + "properties": { + "_links": { + "properties": { + "base": { + "description": "Base url of the Confluence site.", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + ] + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nrequested attachment or the attachment was not found." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:attachment:confluence" + ] + } + ], + "tags": [ + "Attachment" + ], + "description": "Returns a specific attachment.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the attachment's container.", + "operationId": "getAttachmentById", + "summary": "Get attachment by id", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:attachment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{id}/footer-comments": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment for which comments should be returned.", + "in": "path", + "name": "id", + "pattern": "(att)?[0-9]+", + "required": true, + "type": "string" + }, + { + "description": "The content format type to be returned in the `body` field of the response. If available, the representation will be available under a response field of the same name under the `body` field.", + "enum": [ + "storage", + "atlas_doc_format" + ], + "in": "query", + "name": "body-format", + "required": false, + "type": "string" + }, + { + "description": "Used for pagination, this opaque cursor will be returned in the `next` URL in the `Link` response header. Use the relative URL in the `Link` header to retrieve the `next` set of results.", + "in": "query", + "name": "cursor", + "required": false, + "type": "string" + }, + { + "default": 25, + "description": "Maximum number of comments per result to return. If more results exist, use the `Link` header to retrieve a relative URL that will return the next set of results.", + "format": "int32", + "in": "query", + "maximum": 250, + "minimum": 1, + "name": "limit", + "type": "integer" + }, + { + "description": "Used to sort the result by a particular field.", + "enum": [ + "created-date", + "-created-date", + "modified-date", + "-modified-date" + ], + "in": "query", + "name": "sort", + "required": false, + "type": "string" + }, + { + "description": "Version number of the attachment to retrieve comments for. If no version provided, retrieves comments for the latest version.", + "format": "int64", + "in": "query", + "name": "version", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the attachment comments were successfully retrieved", + "headers": { + "Link": { + "description": "This header contains URL(s) within angle brackets and a relation description for each URL, describing how the provided URL relates to the incoming request's URL. For example, rel=\"next\" would be the URL necessary to get the next page of information. Example response header format: `Link: /comments?cursor=>; rel=\"next\", ; rel=\"base\"`\n", + "type": "string" + } + }, + "schema": { + "properties": { + "_links": { + "$ref": "#/definitions/MultiEntityLinks" + }, + "results": { + "items": { + "$ref": "#/definitions/AttachmentCommentModel" + }, + "type": "array" + } + }, + "title": "MultiEntityResult", + "type": "object" + } + }, + "400": { + "description": "Returned if an invalid request is provided." + }, + "401": { + "description": "Returned if the authentication credentials are incorrect or missing\nfrom the request." + }, + "404": { + "description": "Returned if the calling user does not have permission to view the\nattachment or associated containers." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oAuthDefinitions": [ + "read:comment:confluence" + ] + } + ], + "tags": [ + "Comment" + ], + "description": "Returns the comments of the specific attachment.\nThe number of results is limited by the `limit` parameter and additional results (if available) will be available through\nthe `next` URL present in the `Link` response header.\n\n**[Permissions](https://confluence.atlassian.com/x/_AozKw) required**:\nPermission to view the attachment and its corresponding containers.", + "operationId": "getAttachmentComments", + "summary": "Get attachment comments", + "x-atlassian-connect-scope": "READ", + "x-atlassian-data-security-policy": [ + { + "app-access-rule-exempt": false + } + ], + "x-atlassian-oauth2-scopes": [ + { + "scheme": "oAuthDefinitions", + "scopes": [ + "read:comment:confluence" + ], + "state": "Current" + } + ] + } + }, + "/attachments/{id}/labels": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The ID of the attachment for which labels should be returned.", + "format": "int64", + "in": "path", + "name": "id", + "required": true, + "type": "integer" + }, + { + "description": "Filter the results to labels based on their prefix.", + "enum": [ + "my", + "team", + "global", + "system" + ], + "in": "query", + "name": "prefix", + "required": false, + "type": "string" + }, + { + "description": "Used to sort the result by a particular field.", + "in": "query", + "items": { + "$ref": "#/definitions/LabelSortOrder" + }, + "name": "sort", + "required": false, + "type": "string" + }, + { + "description": "Used for pagination, this opaque cursor will be returned in the `next` URL in the `Link` response header. Use the relative URL in the `Link` header to retrieve the `next` set of results.", + "in": "query", + "name": "cursor", + "required": false, + "type": "string" + }, + { + "default": 25, + "description": "Maximum number of labels per result to return. If more results exist, use the `Link` header to retrieve a relative URL that will return the next set of results.", + "format": "int32", + "in": "query", + "maximum": 250, + "minimum": 1, + "name": "limit", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Returned if the requested labels are returned.", + "headers": { + "Link": { + "description": "This header contains URL(s) within angle brackets and a relation description for each URL, describing how the provided URL relates to the incoming request's URL. For example, rel=\"next\" would be the URL necessary to get the next page of information. Example response header format: `Link: /labels?cursor=>; rel=\"next\", ; rel=\"base\"`\n", + "type": "string" + } + }, + "schema": { + "properties": { + "_links": { + "$ref": "#/definitions/MultiEntityLinks" + }, + "results": { + "items": { + "$ref": "#/definitions/Label" + }, + "type": "array" + } + }, + "title": "MultiEntityResult