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
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:
@@ -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 |
Reference in New Issue
Block a user