Skip to content

Calendly (upgrade) - api v2 #467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/appmixer/calendly/bundle.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "appmixer.calendly",
"version": "2.0.0",
"version": "3.0.0",
"changelog": {
"1.0.1": [
"Initial version"
Expand All @@ -10,6 +10,12 @@
],
"2.0.0": [
"Breaking Change: Updated authorization to OAuth2"
],
"3.0.0": [
"(breaking change): Update components to use Calendly API v2. This change affect both InviteeCreated and InviteeCanceled.",
"InviteeCreated and InviteeCanceled have completely new output parameters. It is needed to map all the values from these components again.",
"(new) Invitee No Show Created: triggers when an invitee is marked as no-show.",
"(new) Invitee No Show Deleted: triggers when an invitee no-show mark is removed."
]
}
}
41 changes: 24 additions & 17 deletions src/appmixer/calendly/calendly-commons.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
'use strict';
const request = require('request-promise');

module.exports = {

/**
* Get request for calendly.
* @param {string} token
* @param {string} event - should be one of ['invitee.created', 'invitee.canceled']
* @param {string} url - webhook callback url that will be registered
* @param {Context} context
* @param {string} event - should be one of ['invitee.created', 'invitee.canceled', 'invitee_no_show.created', 'invitee_no_show.deleted']

* @returns {*}
*/
registerWebhookSubscription(token, event, url) {
async registerWebhookSubscription(context, event) {
const { accessToken, profileInfo: { resource } } = context.auth;
const url = context.getWebhookUrl();
context.log({ step: 'registerWebhookSubscription webhookUrl', url });

return request({
const { data } = await context.httpRequest({
method: 'POST',
url: 'https://calendly.com/api/v1/hooks',
url: 'https://api.calendly.com/webhook_subscriptions',
headers: {
'Authorization': `Bearer ${token}`
'Authorization': `Bearer ${accessToken}`
},
json: true,
body: {
data: {
url: url,
events: [event]
events: [event],
scope: 'user',
user: resource.uri,
organization: resource.current_organization
}
});
context.log({ step: 'registerWebhookSubscription response', data });
return data.resource;
},

/**
* DELETE request for calendly to remove webhook subscription.
* @param {string} webhookId
* @param {string} token
* @param {string} webhookUri
* @param {Context} context
* @returns {*}
*/
removeWebhookSubscription(webhookId, token) {
async removeWebhookSubscription(webhookUri, context) {
const { accessToken } = context.auth;

return request({
await context.httpRequest({
method: 'DELETE',
url: `https://calendly.com/api/v1/hooks/${webhookId}`,
url: webhookUri,
headers: {
'Authorization': `Bearer ${token}`
'Authorization': `Bearer ${accessToken}`
}
});
}
Expand Down
44 changes: 19 additions & 25 deletions src/appmixer/calendly/events/InviteeCanceled/InviteeCanceled.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use strict';
const commons = require('../../calendly-commons');
const Promise = require('bluebird');

/**
* Component which triggers whenever new event is canceled.
* Component which triggers whenever an event is canceled.
* @extends {Component}
*/
module.exports = {
Expand All @@ -24,46 +23,41 @@ module.exports = {
*/
async receive(context) {

let data = context.messages.webhook.content.data;
if (!data) {
return;
if (context.messages.webhook) {
const { data: webhookData } = context.messages.webhook.content;
context.log({ step: 'webhookData received', webhookData });

if (webhookData) {
await context.sendJson(webhookData, 'out');
}

return context.response();
}
await Promise.map([data], data => {
data.payload.event['assigned_to'] = data.payload.event['assigned_to'].join(', ');
return context.sendJson(data, 'event');
});
return context.response();
},

/**
* Register webhook in Calendly API.
* @param {Context} context
* @return {Promise}
*/
registerWebhook(context) {

let url = context.getWebhookUrl();

return this.unregisterWebhook(context)
.then(() => {
return commons.registerWebhookSubscription(context.auth.accessToken, 'invitee.canceled', url);
}).then(response => {
return context.saveState({ webhookId: response.id });
});
async registerWebhook(context) {
await this.unregisterWebhook(context);
const response = await commons.registerWebhookSubscription(context, 'invitee.canceled');
return context.saveState({ webhookUri: response.uri });
},

/**
* Delete registered webhook. If there is no webhookId in state, do nothing.
* Delete registered webhook. If there is no webhookUri in state, do nothing.
* @param {Context} context
* @return {Promise}
*/
unregisterWebhook(context) {

let webhookId = context.state.webhookId;
if (!webhookId) {
return Promise.resolve();
const { webhookUri } = context.state;
if (!webhookUri) {
return;
}

return commons.removeWebhookSubscription(webhookId, context.auth.accessToken);
return commons.removeWebhookSubscription(webhookUri, context);
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inconsistent parameter order in removeWebhookSubscription

The parameter order here differs from other event handlers. This implementation passes webhookUri first, then context, which could create maintenance issues as other files use the opposite order.

- return commons.removeWebhookSubscription(webhookUri, context);
+ return commons.removeWebhookSubscription(context, webhookUri);

Alternatively, ensure the removeWebhookSubscription method supports both parameter orders.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return commons.removeWebhookSubscription(webhookUri, context);
return commons.removeWebhookSubscription(context, webhookUri);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all 4 components, the order is fine which is return commons.removeWebhookSubscription(webhookUri, context);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZbynekPelunek You're right about the parameter order. The removeWebhookSubscription method in calendly-commons.js does indeed expect arguments as (webhookUri, context).

However, I noticed a small issue in the InviteeCanceled.js file. The state is saved with the key webhookUri, but retrieved using webhookId:

// In registerWebhook:
return context.saveState({ webhookUri: response.uri });

// In unregisterWebhook:
let webhookUri = context.state.webhookId;

This inconsistency could prevent proper webhook unregistration. Consider updating it to:

const { webhookUri } = context.state;

Which matches how it's implemented in the other three components.

✏️ Learnings added
Learnt from: ZbynekPelunek
PR: clientIO/appmixer-connectors#467
File: src/appmixer/calendly/events/InviteeCanceled/InviteeCanceled.js:61-61
Timestamp: 2025-04-09T10:27:31.974Z
Learning: In the Calendly connector, the parameter order for `removeWebhookSubscription` method in `calendly-commons.js` is `(webhookUri, context)`, not `(context, webhookUri)`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
};
Loading