Skip to content

Commit

Permalink
Merge pull request #371 from benoitdemaegdt/develop
Browse files Browse the repository at this point in the history
Release 1.9.0
  • Loading branch information
benoitdemaegdt authored Mar 4, 2020
2 parents e1f3cb8 + 5722e86 commit fe28894
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 36 deletions.
2 changes: 1 addition & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "maxplorateur-client",
"version": "1.8.0",
"version": "1.9.0",
"private": true,
"scripts": {
"serve": "node_modules/.bin/vue-cli-service serve",
Expand Down
2 changes: 1 addition & 1 deletion server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "maxplorateur-server",
"version": "1.8.0",
"version": "1.9.0",
"description": "find a tgvmax seat",
"scripts": {
"clean": "rm -fR ./node_modules ./.nyc_output ./coverage ./dist",
Expand Down
12 changes: 9 additions & 3 deletions server/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,14 @@ export class Config {
public disableCronCheck: boolean;

/**
* disable trainline calls
* zeit username
*/
public disableTrainline: number;
public zeitUsername: string | undefined;

/**
* zeit username
*/
public zeitPassword: string | undefined;

constructor() {
/* tslint:disable */
Expand All @@ -111,7 +116,8 @@ export class Config {
this.disableCronCheck = isNil(process.env.DISABLE_CRON_CHECK)
? config.get('disableCronCheck')
: process.env.DISABLE_CRON_CHECK === 'true';
this.disableTrainline = 2;
this.zeitUsername = process.env.ZEIT_USERNAME;
this.zeitPassword = process.env.ZEIT_PASSWORD;
}

private getWhitelist = (): string => {
Expand Down
90 changes: 61 additions & 29 deletions server/src/core/CronChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as cron from 'node-cron';
import Config from '../Config';
import Notification from '../core/Notification';
import Database from '../database/database';
import { IAvailability, ITravelAlert, IUser } from '../types';
import { SncfMobile } from './SncfMobile';
import { Trainline } from './Trainline';
import { IAvailability, IConnector, IConnectorParams, ITravelAlert, IUser } from '../types';
import Sncf from './connectors/Sncf';
import Zeit from './connectors/Zeit';

/**
* Periodically check Tgvmax availability
Expand All @@ -18,6 +18,39 @@ import { Trainline } from './Trainline';
* if NO -> update lastCheck to current time and continue
*/
class CronChecks {

/**
* connectors
*/
private readonly connectors: IConnector[];

constructor() {
this.connectors = [
{
name: 'Zeit',
weight: 30,
async isTgvmaxAvailable({
origin, destination, fromTime, toTime, tgvmaxNumber,
}: IConnectorParams): Promise<IAvailability> {
console.log(`${moment(new Date()).tz('Europe/Paris').format('DD-MM-YYYY HH:mm:ss')} - using zeit connector`); // tslint:disable-line

return Zeit.isTgvmaxAvailable({ origin, destination, fromTime, toTime, tgvmaxNumber });
},
},
{
name: 'Sncf',
weight: 70,
async isTgvmaxAvailable({
origin, destination, fromTime, toTime, tgvmaxNumber,
}: IConnectorParams): Promise<IAvailability> {
console.log(`${moment(new Date()).tz('Europe/Paris').format('DD-MM-YYYY HH:mm:ss')} - using sncf connector`); // tslint:disable-line

return Sncf.isTgvmaxAvailable({ origin, destination, fromTime, toTime, tgvmaxNumber });
},
},
];
}

/**
* init CronJob
*/
Expand All @@ -35,35 +68,20 @@ class CronChecks {
* Send notification if tgvmax seat is available
*/
for (const travelAlert of travelAlerts) {
let availability: IAvailability;

/**
* split load on trainline and sncf mobile APIs
*/
if (random(0, 1) === Config.disableTrainline) {
console.log(`${moment(new Date()).tz('Europe/Paris').format('DD-MM-YYYY HH:mm:ss')} - processing travelAlert ${travelAlert._id} - trainline API`); // tslint:disable-line
const trainline: Trainline = new Trainline(
travelAlert.origin.trainlineId,
travelAlert.destination.trainlineId,
travelAlert.fromTime,
travelAlert.toTime,
travelAlert.tgvmaxNumber,
);
availability = await trainline.isTgvmaxAvailable();
} else {
console.log(`${moment(new Date()).tz('Europe/Paris').format('DD-MM-YYYY HH:mm:ss')} - processing travelAlert ${travelAlert._id} - sncf API`); // tslint:disable-line
const sncfMobile: SncfMobile = new SncfMobile(
travelAlert.origin.sncfId,
travelAlert.destination.sncfId,
travelAlert.fromTime,
travelAlert.toTime,
travelAlert.tgvmaxNumber,
);
availability = await sncfMobile.isTgvmaxAvailable();
}
const selectedConnector: IConnector = this.getConnector();
const availability: IAvailability = await selectedConnector.isTgvmaxAvailable({ // tslint:disable-line
origin: travelAlert.origin,
destination: travelAlert.destination,
fromTime: travelAlert.fromTime,
toTime: travelAlert.toTime,
tgvmaxNumber: travelAlert.tgvmaxNumber,
});

if (!availability.isTgvmaxAvailable) {
await Database.updateOne('alerts', {_id: new ObjectId(travelAlert._id)}, {$set: {lastCheck: new Date()}});
await Database.updateOne(
'alerts', { _id: new ObjectId(travelAlert._id)}, { $set: { lastCheck: new Date() },
});
await this.delay(Config.delay);
continue;
}
Expand Down Expand Up @@ -95,6 +113,20 @@ class CronChecks {
});
}

/**
* select the connector that will process tgvmax availability
*/
private readonly getConnector = (): IConnector => {
const MAX_WEIGHT: number = 100;
let rand: number = random(0, MAX_WEIGHT);
for (const connector of this.connectors) {
rand = rand - connector.weight;
if (rand <= 0) { return connector; }
}

return this.connectors[this.connectors.length - 1];
}

/**
* fetch all pending travelAlert in database
*/
Expand Down
157 changes: 157 additions & 0 deletions server/src/core/connectors/Sncf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import Axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import * as httpsProxyAgent from 'https-proxy-agent';
import { filter, get, isEmpty, isNil, map, random, uniq } from 'lodash';
import * as moment from 'moment-timezone';
import Config from '../../Config';
import { IAvailability, IConnectorParams, ISncfMobileTrain } from '../../types';

/**
* Sncf connector
*/
class Sncf {
/**
* connector generic function
*/
public async isTgvmaxAvailable({
origin, destination, fromTime, toTime, tgvmaxNumber,
}: IConnectorParams): Promise<IAvailability> {
const tgvmaxHours: string[] = await this.getTgvmaxHours({
origin, destination, fromTime, toTime, tgvmaxNumber,
});

/**
* If previous call returns an empty array, there is no TGVmax available
*/
return isEmpty(tgvmaxHours)
? { isTgvmaxAvailable: false, hours: [] }
: { isTgvmaxAvailable: true, hours: uniq(tgvmaxHours) };
}

/**
* get data from sncf api
*/
private readonly getTgvmaxHours = async({
origin, destination, fromTime, toTime, tgvmaxNumber,
}: IConnectorParams): Promise<string[]> => {
const results: ISncfMobileTrain[] = [];
let keepSearching: boolean = true;
let departureMinTime: string = moment(fromTime).tz('Europe/Paris').format('YYYY-MM-DD[T]HH:mm:ss');
const departureMaxTime: string = moment(toTime).tz('Europe/Paris').format('YYYY-MM-DD[T]HH:mm:ss');

try {
while (keepSearching) {
const config: AxiosRequestConfig = {
url: `${Config.baseSncfMobileUrl}/m700/vmd/maq/v3/proposals/train`,
method: 'POST',
headers: {
Accept: 'application/json',
'User-Agent': 'OUI.sncf/65.1.1 CFNetwork/1107.1 Darwin/19.0.0',
'Accept-Language': 'fr-FR ',
'Content-Type': 'application/json;charset=UTF8',
Host: 'wshoraires.oui.sncf',
'x-vsc-locale': 'fr_FR',
'X-Device-Type': 'IOS',
},
data: {
departureTown: {
codes: {
resarail: origin.sncfId,
},
},
destinationTown: {
codes: {
resarail: destination.sncfId,
},
},
features: [
'TRAIN_AND_BUS',
'DIRECT_TRAVEL',
],
outwardDate: moment(departureMinTime).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ'),
passengers: [
{
age: 25, // random
ageRank: 'YOUNG',
birthday: '1995-03-06', // random
commercialCard: {
number: tgvmaxNumber,
type: 'HAPPY_CARD',
},
type: 'HUMAN',
},
],
travelClass: 'SECOND',
},
};

/**
* split load between multiple servers
*/
if (process.env.NODE_ENV === 'production' && !isNil(Config.proxyUrl) && random(0, 1) === 0) {
config.httpsAgent = new httpsProxyAgent(Config.proxyUrl);
}

/**
* interceptor for handling sncf 200 ok that should be 500 or 301
*/
Axios.interceptors.response.use(async(res: AxiosResponse) => {
const data: {exceptionType?: string} = res.data as {exceptionType?: string};
if (!isNil(data.exceptionType)) {
return Promise.reject({
response: {
status: 500,
statusText: data.exceptionType,
},
});
}

return res;
});

/**
* get data from oui.sncf
*/
const response: AxiosResponse = await Axios.request(config);

const pageResults: {journeys: ISncfMobileTrain[]} = response.data as {journeys: ISncfMobileTrain[]};
const pageJourneys: ISncfMobileTrain[] = pageResults.journeys;

results.push(...pageJourneys);

const pageLastTripDeparture: string = moment(pageJourneys[pageJourneys.length - 1].departureDate)
.tz('Europe/Paris').format('YYYY-MM-DD[T]HH:mm:ss');

if (moment(departureMaxTime).isSameOrBefore(pageLastTripDeparture)
|| moment(departureMinTime).isSame(pageLastTripDeparture)) {
keepSearching = false;
}

departureMinTime = pageLastTripDeparture;
}
} catch (error) {
const status: number = get(error, 'response.status', ''); // tslint:disable-line
const statusText: string = get(error, 'response.statusText', ''); // tslint:disable-line
const label: string = get(error, 'response.data.label', ''); // tslint:disable-line
console.log(`SNCF API ERROR : ${status} ${statusText} ${label}`); // tslint:disable-line
}

/**
* 1/ filter out trains with no TGVmax seat available
* 2/ filter out trains leaving after toTime
*/
const tgvmaxTravels: ISncfMobileTrain[] = filter(results, (item: ISncfMobileTrain) => {
const departureDate: string = moment(item.departureDate).tz('Europe/Paris').format('YYYY-MM-DD[T]HH:mm:ss');

return isNil(item.unsellableReason)
&& item.price.value === 0
&& moment(departureDate).isSameOrBefore(departureMaxTime);
});

return map(tgvmaxTravels, (tgvmaxTravel: ISncfMobileTrain) => {
return moment(tgvmaxTravel.departureDate).tz('Europe/Paris').format('HH:mm');
});
}

}

export default new Sncf();
Loading

0 comments on commit fe28894

Please sign in to comment.