first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,566 @@
import type { INodeProperties } from 'n8n-workflow';
export const activityOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['activity'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new activity',
action: 'Create an activity',
},
{
name: 'Get',
value: 'get',
description: 'Get an activity',
action: 'Get an activity',
},
{
name: 'Get Comments',
value: 'getComments',
description: 'Get all activity comments',
action: 'Get all activity comments',
},
{
name: 'Get Kudos',
value: 'getKudos',
description: 'Get all activity kudos',
action: 'Get all activity kudos',
},
{
name: 'Get Laps',
value: 'getLaps',
description: 'Get all activity laps',
action: 'Get all activity laps',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many activities',
action: 'Get many activities',
},
{
name: 'Get Streams',
value: 'getStreams',
description: 'Get activity streams',
action: 'Get all activity streams',
},
{
name: 'Get Zones',
value: 'getZones',
description: 'Get all activity zones',
action: 'Get all activity zones',
},
{
name: 'Update',
value: 'update',
description: 'Update an activity',
action: 'Update an activity',
},
],
default: 'create',
},
];
export const activityFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* activity:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
},
},
default: '',
description: 'The name of the activity',
},
{
displayName: 'Type',
name: 'type',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
'@version': [1],
},
},
default: '',
description: 'Type of activity. For example - Run, Ride etc.',
},
{
displayName: 'Sport Type',
name: 'sport_type',
type: 'options',
options: [
{ name: 'Alpine Ski', value: 'AlpineSki' },
{ name: 'Backcountry Ski', value: 'BackcountrySki' },
{ name: 'Badminton', value: 'Badminton' },
{ name: 'Canoeing', value: 'Canoeing' },
{ name: 'Crossfit', value: 'Crossfit' },
{ name: 'EBike Ride', value: 'EBikeRide' },
{ name: 'Elliptical', value: 'Elliptical' },
{ name: 'EMountain Bike Ride', value: 'EMountainBikeRide' },
{ name: 'Golf', value: 'Golf' },
{ name: 'Gravel Ride', value: 'GravelRide' },
{ name: 'Handcycle', value: 'Handcycle' },
{ name: 'HIIT', value: 'HighIntensityIntervalTraining' },
{ name: 'Hike', value: 'Hike' },
{ name: 'Ice Skate', value: 'IceSkate' },
{ name: 'Inline Skate', value: 'InlineSkate' },
{ name: 'Kayaking', value: 'Kayaking' },
{ name: 'Kitesurf', value: 'Kitesurf' },
{ name: 'Mountain Bike Ride', value: 'MountainBikeRide' },
{ name: 'Nordic Ski', value: 'NordicSki' },
{ name: 'Pickleball', value: 'Pickleball' },
{ name: 'Pilates', value: 'Pilates' },
{ name: 'Racquetball', value: 'Racquetball' },
{ name: 'Ride', value: 'Ride' },
{ name: 'Rock Climbing', value: 'RockClimbing' },
{ name: 'Roller Ski', value: 'RollerSki' },
{ name: 'Rowing', value: 'Rowing' },
{ name: 'Run', value: 'Run' },
{ name: 'Sail', value: 'Sail' },
{ name: 'Skateboard', value: 'Skateboard' },
{ name: 'Snowboard', value: 'Snowboard' },
{ name: 'Snowshoe', value: 'Snowshoe' },
{ name: 'Soccer', value: 'Soccer' },
{ name: 'Squash', value: 'Squash' },
{ name: 'Stair Stepper', value: 'StairStepper' },
{ name: 'Stand Up Paddling', value: 'StandUpPaddling' },
{ name: 'Surfing', value: 'Surfing' },
{ name: 'Swim', value: 'Swim' },
{ name: 'Table Tennis', value: 'TableTennis' },
{ name: 'Tennis', value: 'Tennis' },
{ name: 'Trail Run', value: 'TrailRun' },
{ name: 'Velomobile', value: 'Velomobile' },
{ name: 'Virtual Ride', value: 'VirtualRide' },
{ name: 'Virtual Row', value: 'VirtualRow' },
{ name: 'Virtual Run', value: 'VirtualRun' },
{ name: 'Walk', value: 'Walk' },
{ name: 'Weight Training', value: 'WeightTraining' },
{ name: 'Wheelchair', value: 'Wheelchair' },
{ name: 'Windsurf', value: 'Windsurf' },
{ name: 'Workout', value: 'Workout' },
{ name: 'Yoga', value: 'Yoga' },
],
default: 'Run',
description: 'Type of sport',
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
},
hide: {
'@version': [1],
},
},
},
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
default: '',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
},
},
description: 'ISO 8601 formatted date time',
},
{
displayName: 'Elapsed Time (Seconds)',
name: 'elapsedTime',
type: 'number',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
},
},
typeOptions: {
minValue: 0,
},
default: 0,
description: 'In seconds',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['activity'],
operation: ['create'],
},
},
options: [
{
displayName: 'Commute',
name: 'commute',
type: 'boolean',
default: false,
description: 'Whether to mark as commute',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'Description of the activity',
},
{
displayName: 'Distance',
name: 'distance',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
description: 'In meters',
},
{
displayName: 'Trainer',
name: 'trainer',
type: 'boolean',
default: false,
description: 'Whether to mark as a trainer activity',
},
],
},
/* -------------------------------------------------------------------------- */
/* activity:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Activity ID',
name: 'activityId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['update'],
},
},
default: '',
description: 'ID or email of activity',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['activity'],
operation: ['update'],
},
},
options: [
{
displayName: 'Commute',
name: 'commute',
type: 'boolean',
default: false,
description: 'Whether to mark as commute',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'Description of the activity',
},
{
displayName: 'Gear ID',
name: 'gear_id',
type: 'string',
default: '',
description:
'Identifier for the gear associated with the activity. none clears gear from activity.',
},
{
displayName: 'Mute Activity',
name: 'hide_from_home',
type: 'boolean',
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description: 'Do not publish to Home or Club feeds',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the activity',
},
{
displayName: 'Type',
name: 'type',
type: 'string',
default: '',
description: 'Type of activity. For example - Run, Ride etc.',
displayOptions: {
show: {
'@version': [1],
},
},
},
{
displayName: 'Sport Type',
name: 'sport_type',
type: 'options',
options: [
{ name: 'Alpine Ski', value: 'AlpineSki' },
{ name: 'Backcountry Ski', value: 'BackcountrySki' },
{ name: 'Badminton', value: 'Badminton' },
{ name: 'Canoeing', value: 'Canoeing' },
{ name: 'Crossfit', value: 'Crossfit' },
{ name: 'EBike Ride', value: 'EBikeRide' },
{ name: 'Elliptical', value: 'Elliptical' },
{ name: 'EMountain Bike Ride', value: 'EMountainBikeRide' },
{ name: 'Golf', value: 'Golf' },
{ name: 'Gravel Ride', value: 'GravelRide' },
{ name: 'Handcycle', value: 'Handcycle' },
{ name: 'HIIT', value: 'HighIntensityIntervalTraining' },
{ name: 'Hike', value: 'Hike' },
{ name: 'Ice Skate', value: 'IceSkate' },
{ name: 'Inline Skate', value: 'InlineSkate' },
{ name: 'Kayaking', value: 'Kayaking' },
{ name: 'Kitesurf', value: 'Kitesurf' },
{ name: 'Mountain Bike Ride', value: 'MountainBikeRide' },
{ name: 'Nordic Ski', value: 'NordicSki' },
{ name: 'Pickleball', value: 'Pickleball' },
{ name: 'Pilates', value: 'Pilates' },
{ name: 'Racquetball', value: 'Racquetball' },
{ name: 'Ride', value: 'Ride' },
{ name: 'Rock Climbing', value: 'RockClimbing' },
{ name: 'Roller Ski', value: 'RollerSki' },
{ name: 'Rowing', value: 'Rowing' },
{ name: 'Run', value: 'Run' },
{ name: 'Sail', value: 'Sail' },
{ name: 'Skateboard', value: 'Skateboard' },
{ name: 'Snowboard', value: 'Snowboard' },
{ name: 'Snowshoe', value: 'Snowshoe' },
{ name: 'Soccer', value: 'Soccer' },
{ name: 'Squash', value: 'Squash' },
{ name: 'Stair Stepper', value: 'StairStepper' },
{ name: 'Stand Up Paddling', value: 'StandUpPaddling' },
{ name: 'Surfing', value: 'Surfing' },
{ name: 'Swim', value: 'Swim' },
{ name: 'Table Tennis', value: 'TableTennis' },
{ name: 'Tennis', value: 'Tennis' },
{ name: 'Trail Run', value: 'TrailRun' },
{ name: 'Velomobile', value: 'Velomobile' },
{ name: 'Virtual Ride', value: 'VirtualRide' },
{ name: 'Virtual Row', value: 'VirtualRow' },
{ name: 'Virtual Run', value: 'VirtualRun' },
{ name: 'Walk', value: 'Walk' },
{ name: 'Weight Training', value: 'WeightTraining' },
{ name: 'Wheelchair', value: 'Wheelchair' },
{ name: 'Windsurf', value: 'Windsurf' },
{ name: 'Workout', value: 'Workout' },
{ name: 'Yoga', value: 'Yoga' },
],
default: 'Run',
description: 'Type of sport',
displayOptions: {
hide: {
'@version': [1],
},
},
},
{
displayName: 'Trainer',
name: 'trainer',
type: 'boolean',
default: false,
description: 'Whether to mark as a trainer activity',
},
],
},
/* -------------------------------------------------------------------------- */
/* activity:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Activity ID',
name: 'activityId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['get'],
},
},
default: '',
description: 'ID or email of activity',
},
/* -------------------------------------------------------------------------- */
/* activity */
/* -------------------------------------------------------------------------- */
{
displayName: 'Activity ID',
name: 'activityId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['activity'],
operation: ['getComments', 'getLaps', 'getKudos', 'getZones', 'getStreams'],
},
},
default: '',
description: 'ID or email of activity',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['activity'],
operation: ['getComments', 'getLaps', 'getKudos', 'getZones'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['activity'],
operation: ['getComments', 'getLaps', 'getKudos', 'getZones'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Keys',
name: 'keys',
type: 'multiOptions',
options: [
{
name: 'Altitude',
value: 'altitude',
},
{
name: 'Cadence',
value: 'cadence',
},
{
name: 'Distance',
value: 'distance',
},
{
name: 'Gradient',
value: 'grade_smooth',
},
{
name: 'Heartrate',
value: 'heartrate',
},
{
name: 'Latitude / Longitude',
value: 'latlng',
},
{
name: 'Moving',
value: 'moving',
},
{
name: 'Temperature',
value: 'temp',
},
{
name: 'Time',
value: 'time',
},
{
name: 'Velocity',
value: 'velocity_smooth',
},
{
name: 'Watts',
value: 'watts',
},
],
displayOptions: {
show: {
resource: ['activity'],
operation: ['getStreams'],
},
},
required: true,
default: [],
description: 'Desired stream types to return',
},
/* -------------------------------------------------------------------------- */
/* activity:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['activity'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['activity'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
];
@@ -0,0 +1,80 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
IRequestOptions,
IWebhookFunctions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function stravaApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
) {
const options: IRequestOptions = {
method,
form: body,
qs,
uri: uri || `https://www.strava.com/api/v3${resource}`,
json: true,
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
if (this.getNode().type.includes('Trigger') && resource.includes('/push_subscriptions')) {
const credentials = await this.getCredentials('stravaOAuth2Api');
if (method === 'GET' || method === 'DELETE') {
qs.client_id = credentials.clientId;
qs.client_secret = credentials.clientSecret;
} else {
body.client_id = credentials.clientId;
body.client_secret = credentials.clientSecret;
}
return await this.helpers?.request(options);
} else {
return await this.helpers.requestOAuth2.call(this, 'stravaOAuth2Api', options, {
includeCredentialsOnRefreshOnBody: true,
});
}
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function stravaApiRequestAllItems(
this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
query: IDataObject = {},
) {
const returnData: IDataObject[] = [];
let responseData;
query.per_page = 30;
query.page = 1;
do {
responseData = await stravaApiRequest.call(this, method, resource, body, query);
query.page++;
returnData.push.apply(returnData, responseData as IDataObject[]);
} while (responseData.length !== 0);
return returnData;
}
@@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.strava",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/strava/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.strava/"
}
],
"generic": [
{
"label": "2021: The Year to Automate the New You with n8n",
"icon": "☀️",
"url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/"
}
]
}
}
@@ -0,0 +1,204 @@
import moment from 'moment-timezone';
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { activityFields, activityOperations } from './ActivityDescription';
import { stravaApiRequest, stravaApiRequestAllItems } from './GenericFunctions';
export class Strava implements INodeType {
description: INodeTypeDescription = {
displayName: 'Strava',
name: 'strava',
icon: 'file:strava.svg',
group: ['input'],
version: [1, 1.1],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Strava API',
defaults: {
name: 'Strava',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'stravaOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Activity',
value: 'activity',
},
],
default: 'activity',
},
...activityOperations,
...activityFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
const qs: IDataObject = {};
let responseData;
const nodeVersion = this.getNode().typeVersion;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'activity') {
//https://developers.strava.com/docs/reference/#api-Activities-createActivity
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const startDate = this.getNodeParameter('startDate', i) as string;
const elapsedTime = this.getNodeParameter('elapsedTime', i) as number;
const additionalFields = this.getNodeParameter('additionalFields', i);
if (additionalFields.trainer === true) {
additionalFields.trainer = 1;
}
if (additionalFields.commute === true) {
additionalFields.commute = 1;
}
const body: IDataObject = {
name,
start_date_local: moment(startDate).toISOString(),
elapsed_time: elapsedTime,
};
if (nodeVersion === 1) {
const type = this.getNodeParameter('type', i) as string;
body.type = type;
} else {
const sportType = this.getNodeParameter('sport_type', i) as string;
body.sport_type = sportType;
}
Object.assign(body, additionalFields);
responseData = await stravaApiRequest.call(this, 'POST', '/activities', body);
}
//https://developers.strava.com/docs/reference/#api-Activities-getActivityById
if (operation === 'get') {
const activityId = this.getNodeParameter('activityId', i) as string;
responseData = await stravaApiRequest.call(this, 'GET', `/activities/${activityId}`);
}
if (['getLaps', 'getZones', 'getKudos', 'getComments'].includes(operation)) {
const path: IDataObject = {
getComments: 'comments',
getZones: 'zones',
getKudos: 'kudos',
getLaps: 'laps',
};
const activityId = this.getNodeParameter('activityId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i);
responseData = await stravaApiRequest.call(
this,
'GET',
`/activities/${activityId}/${path[operation]}`,
);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i);
responseData = responseData.splice(0, limit);
}
}
//https://developers.strava.com/docs/reference/#api-Streams-getActivityStreams
if (operation === 'getStreams') {
const activityId = this.getNodeParameter('activityId', i) as string;
const keys = this.getNodeParameter('keys', i) as string[];
qs.keys = keys.toString();
qs.key_by_type = true;
responseData = await stravaApiRequest.call(
this,
'GET',
`/activities/${activityId}/streams`,
{},
qs,
);
}
//https://developers.mailerlite.com/reference#subscribers
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) {
responseData = await stravaApiRequestAllItems.call(
this,
'GET',
'/activities',
{},
qs,
);
} else {
qs.per_page = this.getNodeParameter('limit', i);
responseData = await stravaApiRequest.call(this, 'GET', '/activities', {}, qs);
}
}
//https://developers.strava.com/docs/reference/#api-Activities-updateActivityById
if (operation === 'update') {
const activityId = this.getNodeParameter('activityId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
responseData = await stravaApiRequest.call(
this,
'PUT',
`/activities/${activityId}`,
body,
);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.stravaTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/strava/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.stravatrigger/"
}
]
}
}
@@ -0,0 +1,285 @@
import { randomBytes } from 'crypto';
import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { stravaApiRequest } from './GenericFunctions';
export class StravaTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Strava Trigger',
name: 'stravaTrigger',
icon: 'file:strava.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Strava events occur',
defaults: {
name: 'Strava Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'stravaOAuth2Api',
required: true,
},
],
webhooks: [
{
name: 'setup',
httpMethod: 'GET',
responseMode: 'onReceived',
path: 'webhook',
},
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Object',
name: 'object',
type: 'options',
options: [
{
name: '[All]',
value: '*',
},
{
name: 'Activity',
value: 'activity',
},
{
name: 'Athlete',
value: 'athlete',
},
],
default: '*',
},
{
displayName: 'Event',
name: 'event',
type: 'options',
options: [
{
name: '[All]',
value: '*',
},
{
name: 'Created',
value: 'create',
},
{
name: 'Deleted',
value: 'delete',
},
{
name: 'Updated',
value: 'update',
},
],
default: '*',
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description:
'By default the webhook-data only contain the Object ID. If this option gets activated, it will resolve the data automatically.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Delete If Exist',
name: 'deleteIfExist',
type: 'boolean',
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description:
'Strava allows just one subscription at all times. If you want to delete the current subscription to make room for a new subscription with the current parameters, set this parameter to true. Keep in mind this is a destructive operation.',
},
],
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
const endpoint = '/push_subscriptions';
const webhooks = await stravaApiRequest.call(this, 'GET', endpoint, {});
for (const webhook of webhooks) {
if (webhook.callback_url === webhookUrl) {
webhookData.webhookId = webhook.id;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const endpoint = '/push_subscriptions';
const body = {
callback_url: webhookUrl,
verify_token: randomBytes(20).toString('hex'),
};
let responseData;
try {
responseData = await stravaApiRequest.call(this, 'POST', endpoint, body);
} catch (error) {
if (error?.cause?.error) {
const errors = error?.cause?.error?.errors;
for (error of errors) {
// if there is a subscription already created
if (error.resource === 'PushSubscription' && error.code === 'already exists') {
const options = this.getNodeParameter('options') as IDataObject;
//get the current subscription
const webhooks = await stravaApiRequest.call(
this,
'GET',
'/push_subscriptions',
{},
);
if (options.deleteIfExist) {
// delete the subscription
await stravaApiRequest.call(
this,
'DELETE',
`/push_subscriptions/${webhooks[0].id}`,
);
// now there is room create a subscription with the n8n data
const requestBody = {
callback_url: webhookUrl,
verify_token: randomBytes(20).toString('hex'),
};
responseData = await stravaApiRequest.call(
this,
'POST',
'/push_subscriptions',
requestBody,
);
} else {
error.message = `A subscription already exists [${webhooks[0].callback_url}]. If you want to delete this subscription and create a new one with the current parameters please go to options and set delete if exist to true`;
throw error;
}
}
}
}
if (!responseData) {
throw error;
}
}
if (responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/push_subscriptions/${webhookData.webhookId}`;
try {
await stravaApiRequest.call(this, 'DELETE', endpoint);
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registered anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const body = this.getBodyData();
const query = this.getQueryData() as IDataObject;
const object = this.getNodeParameter('object');
const event = this.getNodeParameter('event');
const resolveData = this.getNodeParameter('resolveData') as boolean;
let objectType, eventType;
if (object === '*') {
objectType = ['activity', 'athlete'];
} else {
objectType = [object];
}
if (event === '*') {
eventType = ['create', 'update', 'delete'];
} else {
eventType = [event];
}
if (this.getWebhookName() === 'setup') {
if (query['hub.challenge']) {
// Is a create webhook confirmation request
const res = this.getResponseObject();
res.status(200).json({ 'hub.challenge': query['hub.challenge'] }).end();
return {
noWebhookResponse: true,
};
}
}
if (object !== '*' && !objectType.includes(body.object_type as string)) {
return {};
}
if (event !== '*' && !eventType.includes(body.aspect_type as string)) {
return {};
}
if (resolveData && body.aspect_type !== 'delete') {
let endpoint = `/athletes/${body.object_id}/stats`;
if (body.object_type === 'activity') {
endpoint = `/activities/${body.object_id}`;
}
body.object_data = await stravaApiRequest.call(this, 'GET', endpoint);
}
return {
workflowData: [this.helpers.returnJsonArray(body)],
};
}
}
@@ -0,0 +1,133 @@
{
"type": "object",
"properties": {
"achievement_count": {
"type": "integer"
},
"athlete": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"resource_state": {
"type": "integer"
}
}
},
"athlete_count": {
"type": "integer"
},
"comment_count": {
"type": "integer"
},
"commute": {
"type": "boolean"
},
"device_watts": {
"type": "boolean"
},
"display_hide_heartrate_option": {
"type": "boolean"
},
"elapsed_time": {
"type": "integer"
},
"flagged": {
"type": "boolean"
},
"from_accepted_tag": {
"type": "boolean"
},
"has_heartrate": {
"type": "boolean"
},
"has_kudoed": {
"type": "boolean"
},
"heartrate_opt_out": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"kudos_count": {
"type": "integer"
},
"location_city": {
"type": "null"
},
"location_state": {
"type": "null"
},
"manual": {
"type": "boolean"
},
"map": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"resource_state": {
"type": "integer"
},
"summary_polyline": {
"type": "string"
}
}
},
"max_heartrate": {
"type": "integer"
},
"moving_time": {
"type": "integer"
},
"name": {
"type": "string"
},
"photo_count": {
"type": "integer"
},
"pr_count": {
"type": "integer"
},
"private": {
"type": "boolean"
},
"resource_state": {
"type": "integer"
},
"sport_type": {
"type": "string"
},
"start_date": {
"type": "string"
},
"start_date_local": {
"type": "string"
},
"timezone": {
"type": "string"
},
"total_photo_count": {
"type": "integer"
},
"trainer": {
"type": "boolean"
},
"type": {
"type": "string"
},
"upload_id_str": {
"type": "string"
},
"utc_offset": {
"type": "integer"
},
"visibility": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 16 16"><path fill="#fc4c02" d="M0 0h16v16H0z"/><g fill="#fff" fill-rule="evenodd"><path d="m6.9 8.8 2.5 4.5 2.4-4.5h-1.5l-.9 1.7-1-1.7z" opacity=".6"/><path d="m7.2 2.5 3.1 6.3H4zm0 3.8 1.2 2.5H5.9z"/></g></svg>

After

Width:  |  Height:  |  Size: 291 B