UNPKG

strava-api-handler

Version:
250 lines (209 loc) 8.17 kB
/* eslint-disable sonarjs/no-duplicate-string */ import FormData from 'form-data'; import { Api as ApiBase, ApiResponseType, DefaultApiException, DefaultResponseProcessor } from 'rest-api-handler'; import Activity from './Activity'; import StravaException from './StravaException'; import * as STREAM from './streams'; import { ActivityFilters } from './types/ActivityFilters'; import { ApiActivity } from './types/api/Activity'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore global.FormData = FormData; type Scope = 'read' | 'read_all' | 'profile:read_all' | 'profile:write' | 'activity:read' | 'activity:read_all' | 'activity:write'; type Prompt = 'force' | 'auto'; interface Athlete { badge_type_id: number; city: string; country: string; created_at: string; email: string; firstname: string; follower: string | undefined; friend: string | undefined; id: number; lastname: string; premium: boolean; profile: string; profile_medium: string; resource_state: number; sex: string; state: string; updated_at: string; username: string; } interface Token { access_token: string; athlete: Athlete; expires_at: number; expires_in: number; refresh_token: string; token_type: string; } interface UploadStatus { activity_id: number | undefined; error: string | undefined; external_id: string; id: number; status: string; } function base64Encode(string: string): string { if (typeof btoa !== 'undefined') { // eslint-disable-next-line no-undef return btoa(string); } return Buffer.from(string).toString('base64'); } export default class Api extends ApiBase<ApiResponseType<any>> { protected clientId: string; protected secret: string; protected accessToken?: string; public constructor(clientId: string, secret: string) { super('https://www.strava.com', [new DefaultResponseProcessor(DefaultApiException)], { 'content-Type': 'application/json', }); this.clientId = clientId; this.secret = secret; } public setAccessToken(token: string) { this.accessToken = token; this.setDefaultHeader('Authorization', `Bearer ${token}`); } public getAccessToken() { return this.accessToken; } // eslint-disable-next-line default-param-last public getLoginUrl(redirectUri: string, scope: Scope[] = ['read'], approvalPrompt?: Prompt, state?: string): string { const parameters = { client_id: this.clientId, redirect_uri: redirectUri, response_type: 'code', scope: scope.join(','), ...(state ? { state } : {}), ...(approvalPrompt ? { approval_prompt: approvalPrompt } : {}), }; return `https://www.strava.com/oauth/authorize${ApiBase.convertParametersToUrl(parameters)}`; } protected async requestToken(parameters: Record<string, string>): Promise<Token> { const { data } = await this.request( `oauth/token${ApiBase.convertParametersToUrl({ client_id: this.clientId, client_secret: this.secret, ...parameters, })}`, 'POST', {}, { 'content-type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${base64Encode(`${this.clientId}:${this.secret}`)}`, }, ); this.setAccessToken(data.access_token); return data; } public requestAccessToken(code: string): Promise<Token> { return this.requestToken({ code }); } public refreshToken(token: string): Promise<Token> { return this.requestToken({ grant_type: 'refresh_token', refresh_token: token }); } public async getActivity(id: number) { const { data } = await this.get(`api/v3/activities/${id}`); return Activity.getFromApi(data); } public async getStream(id: number, streams: STREAM.Stream[] = []): Promise<any[]> { const { data } = await this.get(`api/v3/activities/${id}/streams`, { keys: streams.join(','), key_by_type: false, }); const points: any[] = []; Object.keys(data).forEach((item: any) => { data[item].data.forEach((value: any, key: any) => { if (!points[key]) { points[key] = {}; } points[key][item] = value; }); }); return points; } public async getActivities(parameters: ActivityFilters) { const { after, before } = parameters; const { data } = await this.get( `api/v3/athlete/activities${ApiBase.convertParametersToUrl({ ...parameters, ...(after ? { after: typeof after === 'number' ? after : after.valueOf() / 1000 } : {}), ...(before ? { before: typeof before === 'number' ? before : before.valueOf() / 1000 } : {}), })}`, ); return data.map((activity: ApiActivity) => { return Activity.getFromApi(activity); }); } public async processActivities( // eslint-disable-next-line default-param-last filter: ActivityFilters = {}, processor: (workout: Activity<number, ApiActivity>) => Promise<Activity<number, ApiActivity>>, ): Promise<Activity<number, ApiActivity>[]> { const activities = await this.getActivities(filter); const processorPromises = activities.map((activity: Activity<number, ApiActivity>) => { return processor(activity); }); if (activities.length > 0) { const { page } = filter; processorPromises.push( ...(await this.processActivities( { ...filter, page: page ? page + 1 : 2, }, processor, )), ); } return Promise.all(processorPromises); } // eslint-disable-next-line complexity public async uploadActivity( activity: Activity, fileContent: string | Buffer, externalId: string | number, dataType = 'gpx', ): Promise<UploadStatus> { const body = Api.convertData( { data_type: dataType, ...(activity.getTitle() != null ? { name: activity.getTitle() } : {}), ...(activity.getDescription() != null ? { description: activity.getDescription() } : {}), ...(activity.isCommute != null ? { commute: activity.isCommute ? 1 : 0 } : {}), }, Api.FORMATS.FORM_DATA, ) as any as FormData; body.append('file', fileContent, { filename: `${externalId}.${dataType}`, }); const contentType = this.getDefaultHeaders()['content-Type'] as string; this.removeDefaultHeader('content-Type'); const { data } = await this.request('api/v3/uploads', 'POST', { body: body as any }); this.setDefaultHeader('content-Type', contentType); return data; } public async getUploadStatus(uploadId: number): Promise<UploadStatus> { const { data } = await this.get(`api/v3/uploads/${uploadId}`); if (data.error) { throw new StravaException(data.error); } return data; } public async createActivity(activity: Activity<undefined, undefined>): Promise<Activity<number, ApiActivity>> { const { data } = await this.post('api/v3/activities', activity.toApiObject()); return Activity.getFromApi(data); } public async updateActivity(activity: Activity<number>): Promise<Activity> { const { data } = await this.put(`api/v3/activities/${activity.getId()}`, activity.toApiObject()); return Activity.getFromApi(data); } public async updateWeight(weight: number): Promise<Athlete> { const { data } = await this.put('api/v3/athlete', { weight }); return data; } }