I am using a queue consumer to do a repair of some data of the page (basically a missing macro configuration property as the data type has changed)
For this, I first get the page with
const res = await api
.asApp()
.requestConfluence(route`/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
headers: {
'Accept': 'application/json'
}
});
which works fine.
I update the body and try to update with (straight from the documentation):
const response = await api.asApp().requestConfluence(route`/wiki/api/v2/pages/${payload.contentId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: pageWithBody
});
console.log(`Repair Response: ${response.status} ${response.statusText}`);
console.log(await response.json());
but the result is:
INFO 2023-04-02T10:22:19.819Z 7c5efaa7-fd55-4a6f-9312-ae55b9e45758 Repair Response: 403 Forbidden
INFO 2023-04-02T10:22:19.819Z 7c5efaa7-fd55-4a6f-9312-ae55b9e45758 { code: 403, message: 'The app is not installed on this instance' }
The app is clearly installed on this instance, so this error is wrong. It seems to be a permissions issue (403), but I do not get an authentication warning (which I can force by using asUser()
instead of asApp()
.
For completeness, here are the scopes from manifest.yml:
permissions:
scopes:
- read:confluence-content.summary
- read:confluence-content.all
- read:page:confluence
- storage:app
- read:space:confluence
- read:confluence-props
- write:confluence-props
- write:confluence-content
- write:confluence-space
I’ve created a version that calls the api asUser()
instead of asApp()
this is called directly from a button on a page. The error now becomes:
INFO 2023-04-02T13:05:47.461Z 9ed4dcfe-8c3d-47da-9124-141a2227cf29 Repair Response: 401 Unauthorized
INFO 2023-04-02T13:05:47.462Z 9ed4dcfe-8c3d-47da-9124-141a2227cf29 { code: 401, message: 'Unauthorized; scope does not match' }
Additionally. On install, I only see these scopes:
Your app will be installed with the following scopes:
- write:confluence-content
- write:confluence-space
Hi @GerbenWierda ,
To call PUT /wiki/api/v2/pages/{id}, your app needs an additional scope: write:page:confluence
.
To elaborate, Confluence has 3 different sets of scopes:
- Connect scopes
- Classic OAuth 2.0 scopes
- Granular OAuth 2.0 scopes
The Confluence V2 REST API uses only “granular scopes”, whilst the Confluence V1 REST API supports both classic and granular scopes.
Here is the code I created to validate this will work with the additional write:page:confluence
scope:
import api, { route } from '@forge/api';
import { Queue } from '@forge/events';
import Resolver from "@forge/resolver";
const testQueue = new Queue({ key: 'test-updates' });
const testResolver = new Resolver();
testResolver.define("test-event-listener", async (queueItem) => {
const eventPayload = queueItem.payload;
const eventContext = queueItem.context;
console.log(` * payload: ${JSON.stringify(eventPayload, null, 2)}`);
console.log(` * context: ${JSON.stringify(eventContext, null, 2)}`);
const pageId = eventPayload.pageViewEvent.content.id;
console.log(` * pageId: ${pageId}`);
const requestPageResponse = await api
.asApp()
.requestConfluence(route`/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
headers: {
'Accept': 'application/json'
}
});
const requestPageData = await requestPageResponse.json();
console.log(` requestPageData = ${JSON.stringify(requestPageData, null, 2)}`);
// Stop processing if on any other page since the remaining processsing updates the page.
if (pageId !== "1489207297") {
return;
}
const pageVersion = requestPageData.version.number;
console.log(` * pageVersion: ${pageVersion}`);
const newPageBody = {
representation: "storage",
value: `Hello, the content of this page was completely replace at ${new Date().toISOString()}.`,
};
const body = {
id: pageId,
status: "current",
title: requestPageData.title,
spaceId: requestPageData.spaceId,
body: newPageBody,
version: {
number: pageVersion + 1
}
}
console.log(` body = ${JSON.stringify(body, null, 2)}`);
const pageUpdateResponse = await api.asApp().requestConfluence(route`/wiki/api/v2/pages/${pageId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const pageUpdateData = await pageUpdateResponse.json();
console.log(`Repair Response: ${pageUpdateResponse.status} ${pageUpdateResponse.statusText}`);
console.log(` pageUpdateData = ${JSON.stringify(pageUpdateData, null, 2)}`);
});
export const testQueueHandler = testResolver.getDefinitions();
export async function dl_handleTriggerPageViewed(event, context) {
console.log(`dl_handleTriggerPageViewed`);
console.log(` * context: ${JSON.stringify(context)}`);
await testQueue.push({
pageViewEvent: event,
pageViewedContext: context
});
};
Regards,
Dugald
Thank you. Weirdly enough I am pretty certain that I tried this earlier (simply by copying the read line to a write line), but something else will have been wrong at the time.
One thing should probably be fixed in the documentation. There it says:
const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/{id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: bodyData
});
which gives me a 400 error with
INFO 2023-04-04T06:19:22.938Z 0a92716e-b1e5-4239-86da-c6c06c9f4c3f Repair Response: 400 Bad Request
INFO 2023-04-04T06:19:22.939Z 0a92716e-b1e5-4239-86da-c6c06c9f4c3f {
errors: [
{
status: 400,
code: 'INVALID_MESSAGE',
title: 'Invalid message',
detail: null
}
]
}
What works is:
const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/{id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(bodyData)
});
(The JSON.stringify()
in your solution (but not in the example in the documentation) is required for me to make it work.)
I had already been wondering about race conditions. It seems you partly have that in mind with:
if (pageId !== "1489207297") {
return;
}
Can you explain what is going on, especially because this is some hard coded number?
Hi @GerbenWierda ,
Since the code executes in response to page viewed events and updates the page with some meaningless content just to provide you an example, I didn’t want it to be re-writing other pages in my test tenant where I had this app installed so I just hard coded the page ID that I was testing this with so it would only continue an update this one page.
Regards,
Dugald