BookStackApp / BookStack

A platform to create documentation/wiki content built with PHP & Laravel
https://www.bookstackapp.com/
MIT License
15.18k stars 1.9k forks source link

Comment API Endpoints #4194

Open carlossierra311 opened 1 year ago

carlossierra311 commented 1 year ago

Describe the feature you'd like

I @ssddanbrown. I am working on generating notifications when a new comment is made on a page. I am trying to achieve this using your webhooks implementation and the pages API, through an Azure Function App. But in doing so, I found there's no way to get information on page comments, including users that previously commented on the page, or users that have favorited the page, which I would like to include as recipients of the notification.

I'd like to have access to the following information as part of the data sent by the "commented_on" webhook and/or the page API:

In case anyone else coming here is interested, here is my code so far (which is working, but will send notifications only to the person which last updated the page that was commented on):

Details

```javascript const request = require('request-promise'); const nodemailer = require('nodemailer'); const BOOKSTACK_URL = process.env['BOOKSTACK_URL']; const BOOKSTACK_API_URL = BOOKSTACK_URL + '/api'; // Your BookStack API credentials const API_TOKEN_ID = process.env['BOOKSTACK_API_TOKEN_ID']; const API_TOKEN_SECRET = process.env['BOOKSTACK_API_TOKEN_SECRET']; // Email server configuration const EMAIL_HOST = process.env['EMAIL_HOST']; const EMAIL_PORT = process.env['EMAIL_PORT']; const EMAIL_USERNAME = process.env['EMAIL_USERNAME']; const EMAIL_PASSWORD = process.env['EMAIL_PASSWORD']; const EMAIL_FROM = process.env['EMAIL_FROM']; // Function to get the email of the user who last updated the page async function get_last_updated_user_email(page_id) { // Get the page information const page_url = BOOKSTACK_API_URL + '/pages/' + page_id; const page_data = await request.get({ url: page_url, headers: { 'Authorization': 'Token ' + API_TOKEN_ID + ':' + API_TOKEN_SECRET }, json: true }); // Get the user who last updated the page const user_id = page_data.updated_by.id; const user_url = BOOKSTACK_API_URL + '/users/' + user_id; const user_data = await request.get({ url: user_url, headers: { 'Authorization': 'Token ' + API_TOKEN_ID + ':' + API_TOKEN_SECRET }, json: true }); return user_data.email; } // Function to send a notification email async function send_notification_email(to_email, subject, message, page_url) { // Configure the email transport const transport = nodemailer.createTransport({ host: EMAIL_HOST, port: EMAIL_PORT, auth: { user: EMAIL_USERNAME, pass: EMAIL_PASSWORD } }); // Define the email message const email_message = { from: EMAIL_FROM, to: to_email, subject: subject, html: `

${message}:

${page_url}

`, }; // Send the email await transport.sendMail(email_message); } // Function to handle the webhook event async function handle_webhook_event(event_data) { // Get the page ID from the webhook event data const page_url = event_data.url; const page_id = event_data.related_item.id; // Get the email of the user who last updated the page const last_updated_user_email = await get_last_updated_user_email(page_id); // Send the notification email const subject = 'New comment on BookStack'; const message = 'A new comment was added to ' + event_data.related_item.name; await send_notification_email(last_updated_user_email, subject, message, page_url); } module.exports = async function (context, req) { const event_data = req.body; // Only handle the "commented_on" event if (event_data.event === 'commented_on') { await handle_webhook_event(event_data); } context.res = { status: 204, body: '' }; }; ```

Describe the benefits this would bring to existing BookStack users

It will allow BookStack to have a full comment notification system that allows for proper and timely interaction around a page's content and comments. Otherwise, comments will go unnoticed most of the time, lowering the value and usability of that feature.

Can the goal of this request already be achieved via other means?

Not that I'm aware of.

Have you searched for an existing open/closed issue?

How long have you been using BookStack?

6 months to 1 year

Additional context

No response

ssddanbrown commented 1 year ago

Thanks for the request @carlossierra311. Happy to expand the API out to comments.

Though, I have been thinking about spending time reviewing comments and notifications (in-platform) in a soon release cycle, so would really prefer to wait until after then before adding them to the API, just in case there are more significant changes to be made. Multiple months away from that right now though.

If you're very eager for something now, it should be possible to hack in some endpoints specific for your use-case via the logical theme system if desired. Just shout if you want an example.

carlossierra311 commented 1 year ago

Thant you @ssddanbrown. Yes, I would appreciate the example, to see if I am able to get this working as desired.

ssddanbrown commented 1 year ago

@carlossierra311 Here's a very simple logical theme system functions.php which adds a /api/comments endpoint:

Details

```php apiListingResponse(Comment::query()->with('createdBy'), [ 'id', 'entity_id', 'entity_type', 'text', 'html', 'parent_id', 'local_id', 'created_at', 'updated_at', 'created_by', ]); } } Route::get('/api/comments', [CommentApiController::class, 'list']) ->middleware('api'); ```

It'll act the same as other listing endpoints, so has the same filtering and params available. Should allow you to fetch multiple, or a single comment via filters.

Notes:

carlossierra311 commented 1 year ago

Thank you @ssddanbrown. One question though: is it possible to get the commenter's email?

ssddanbrown commented 1 year ago

@carlossierra311 I've updated my previous code above so that it shows the created_by user details, but this does not include email, that's a little bit more involved (since we specifically hide it). You can either perform another fetch to the users api endpoint for those details. Alternatively, if you want the email as part of the original comment list (Not something we'd do officially), then you can instead use this code:

Code

```php apiListingResponse(Comment::query()->with('createdBy'), [ 'id', 'entity_id', 'entity_type', 'text', 'html', 'parent_id', 'local_id', 'created_at', 'updated_at', 'created_by', ], [ function (Comment $comment) { $comment->createdBy->makeVisible(['email']); } ]); } } Route::get('/api/comments', [CommentApiController::class, 'list']) ->middleware('api'); ```

carlossierra311 commented 1 year ago

Thank you @ssddanbrown. The 'created_by' detail worked as expected. I agree with you that exposing the commenters' emails in that way is not a good idea, so I just used the api.

Here is the code for an Azure Function that can handle the webhook request and produce notifications for the last page updater and for everyone that has made comments on the page. This is good enough for my use case, but I guess it could be modified to notify more specific users only:

Details

```js const request = require('request-promise'); const nodemailer = require('nodemailer'); const BOOKSTACK_URL = process.env['BOOKSTACK_URL']; const BOOKSTACK_API_URL = BOOKSTACK_URL + '/api'; // Your BookStack API credentials const API_TOKEN_ID = process.env['BOOKSTACK_API_TOKEN_ID']; const API_TOKEN_SECRET = process.env['BOOKSTACK_API_TOKEN_SECRET']; // Email server configuration const EMAIL_HOST = process.env['EMAIL_HOST']; const EMAIL_PORT = process.env['EMAIL_PORT']; const EMAIL_USERNAME = process.env['EMAIL_USERNAME']; const EMAIL_PASSWORD = process.env['EMAIL_PASSWORD']; const EMAIL_FROM = process.env['EMAIL_FROM']; // Function to get the email of a BookStack user async function get_user_email(user_id) { const user_url = BOOKSTACK_API_URL + '/users/' + user_id; const user_data = await request.get({ url: user_url, headers: { 'Authorization': 'Token ' + API_TOKEN_ID + ':' + API_TOKEN_SECRET }, json: true }); return user_data.email; } // Function to get all comments on a BookStack page async function get_page_comments(page_id) { const comments_url = BOOKSTACK_API_URL + '/comments/?filter[entity_id]=' + page_id; const comments = await request.get({ url: comments_url, headers: { 'Authorization': 'Token ' + API_TOKEN_ID + ':' + API_TOKEN_SECRET }, json: true }); return comments.data; } // Function to send a notification email async function send_notification_email(to_email, subject, message, page_url) { // Configure the email transport const transport = nodemailer.createTransport({ host: EMAIL_HOST, port: EMAIL_PORT, auth: { user: EMAIL_USERNAME, pass: EMAIL_PASSWORD } }); // Define the email message const email_message = { from: EMAIL_FROM, to: to_email, subject: subject, html: `

${message}:

${page_url}

`, }; // Send the email await transport.sendMail(email_message); } // Function to handle the webhook event async function handle_webhook_event(event_data) { const page_url = event_data.url; const page_id = event_data.related_item.id; const page_comments = await get_page_comments(page_id); const page_last_updater = event_data.related_item.updated_by.id; const page_last_updater_email = await get_user_email(page_last_updater); // Send the notification email to the last updated user const subject = 'New comment on BookStack'; const message = 'A new comment was added to ' + event_data.related_item.name; await send_notification_email(page_last_updater_email, subject, message, page_url); // Send notification emails to each user that has commented on the page const notified_users = [page_last_updater]; for (const comment of page_comments) { const comment_creator = comment.created_by; if (notified_users.includes(comment_creator)) { continue; } const commenter_email = await get_user_email(comment_creator); await send_notification_email(commenter_email, subject, message, page_url); notified_users.push(comment_creator); } } module.exports = async function (context, req) { const event_data = req.body; // Only handle the "commented_on" event if (event_data.event === 'commented_on') { await handle_webhook_event(event_data); } context.res = { status: 204, body: '' }; }; ```

ssddanbrown commented 1 year ago

Thanks for sharing that @carlossierra311 for others to use. I'm going to re-open this, as I'd still like to add a proper in-built comments API eventually, and this thread will be good for others to find. You should be able to unsubscribe from the thread if you don't want to get updates/notifications on this.

carlossierra311 commented 1 year ago

Hi @ssddanbrown. I hope I'm finding you well today.

I'm trying to update to v23.08.2, and I am getting this error code, which I think is related to the code suggested for the comments end point:

> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi

In functions.php line 6:

  Class "BookStack\Http\Controllers\Api\ApiController" not found

Script @php artisan package:discover --ansi handling the post-autoload-dump event returned with error code 1

I changed line 4 of the code to be as follows, and it seems to do the trick: use BookStack\Http\ApiController;

Can you please advise on how to proceed? I don't want to break something unintentionally. Thanks

ssddanbrown commented 1 year ago

@carlossierra311 Yeah, I moved a lot round in v23.06. I've updated the example you linked to with updated references. This kind of breakage may occur using the logical theme system, although not too often.

carlossierra311 commented 1 year ago

Thank you @ssddanbrown.