@tmlmobilidade/external 20260604.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/ccfl/index.d.ts +15 -0
- package/dist/clients/ccfl/index.js +41 -0
- package/dist/clients/cp/auth.d.ts +27 -0
- package/dist/clients/cp/auth.js +171 -0
- package/dist/clients/cp/index.d.ts +21 -0
- package/dist/clients/cp/index.js +65 -0
- package/dist/clients/crtm-aisa/index.d.ts +21 -0
- package/dist/clients/crtm-aisa/index.js +52 -0
- package/dist/clients/fertagus/auth.d.ts +17 -0
- package/dist/clients/fertagus/auth.js +76 -0
- package/dist/clients/fertagus/index.d.ts +8 -0
- package/dist/clients/fertagus/index.js +33 -0
- package/dist/clients/fertagus/types.d.ts +12 -0
- package/dist/clients/fertagus/types.js +2 -0
- package/dist/clients/ml/auth.d.ts +18 -0
- package/dist/clients/ml/auth.js +77 -0
- package/dist/clients/ml/index.d.ts +48 -0
- package/dist/clients/ml/index.js +101 -0
- package/dist/clients/ml/types.d.ts +38 -0
- package/dist/clients/ml/types.js +2 -0
- package/dist/clients/mobi/index.d.ts +15 -0
- package/dist/clients/mobi/index.js +42 -0
- package/dist/clients/tcb/index.d.ts +9 -0
- package/dist/clients/tcb/index.js +30 -0
- package/dist/clients/ttsl/index.d.ts +9 -0
- package/dist/clients/ttsl/index.js +30 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +34 -0
- package/package.json +52 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const CcflClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches GTFS-RT Vehicle Positions feed from the CCFL API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
7
|
+
*/
|
|
8
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetches GTFS-RT Schedule feed from the CCFL API.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Schedule feed message.
|
|
13
|
+
*/
|
|
14
|
+
schedule: () => Promise<Buffer>;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.CCFL_API_URL;
|
|
5
|
+
async function fetcher(endpoint) {
|
|
6
|
+
if (!BASE_URL) {
|
|
7
|
+
throw new Error('Missing CCFL_API_URL environment variable.');
|
|
8
|
+
}
|
|
9
|
+
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
const endpoints = {
|
|
16
|
+
schedule: '/GTFS',
|
|
17
|
+
vehiclePositions: '/GTFS/realtime/vehiclepositions',
|
|
18
|
+
};
|
|
19
|
+
export const CcflClient = Object.freeze({
|
|
20
|
+
//
|
|
21
|
+
/**
|
|
22
|
+
* Fetches GTFS-RT Vehicle Positions feed from the CCFL API.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
25
|
+
*/
|
|
26
|
+
vehiclePositions: async () => {
|
|
27
|
+
const response = await fetcher(endpoints.vehiclePositions);
|
|
28
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
29
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* Fetches GTFS-RT Schedule feed from the CCFL API.
|
|
33
|
+
*
|
|
34
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Schedule feed message.
|
|
35
|
+
*/
|
|
36
|
+
schedule: async () => {
|
|
37
|
+
const response = await fetcher(endpoints.schedule);
|
|
38
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
39
|
+
return Buffer.from(arrayBuffer);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class CPAuthClient {
|
|
2
|
+
private static _instance;
|
|
3
|
+
private expiresAt;
|
|
4
|
+
private token;
|
|
5
|
+
private tunnel;
|
|
6
|
+
/**
|
|
7
|
+
* Disallow direct instantiation of the service.
|
|
8
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
9
|
+
*/
|
|
10
|
+
private constructor();
|
|
11
|
+
getToken(): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the singleton instance of the subclass.
|
|
14
|
+
*/
|
|
15
|
+
static getInstance(): Promise<CPAuthClient>;
|
|
16
|
+
private connect;
|
|
17
|
+
/**
|
|
18
|
+
* Constructs the authentication URL based on environment variables
|
|
19
|
+
* and SSH tunneling configuration, and handles both direct connections and SSH-tunneled
|
|
20
|
+
* connections, validating the necessary environment variables for each case.
|
|
21
|
+
* This method is called internally by the service and should not be used directly.
|
|
22
|
+
* @throws Will throw an error if required environment variables are missing or if the SSH tunnel setup fails.
|
|
23
|
+
* @returns A promise that resolves to the authentication URL.
|
|
24
|
+
*/
|
|
25
|
+
private getAuthenticationPort;
|
|
26
|
+
}
|
|
27
|
+
export declare const cpAuthClient: CPAuthClient;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { Logger } from '@tmlmobilidade/logger';
|
|
3
|
+
import { SshTunnelService } from '@tmlmobilidade/ssh';
|
|
4
|
+
import { asyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import https from 'node:https';
|
|
7
|
+
/* * */
|
|
8
|
+
export class CPAuthClient {
|
|
9
|
+
//
|
|
10
|
+
static _instance = null;
|
|
11
|
+
expiresAt = 0;
|
|
12
|
+
token = null;
|
|
13
|
+
tunnel = null;
|
|
14
|
+
/**
|
|
15
|
+
* Disallow direct instantiation of the service.
|
|
16
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
17
|
+
*/
|
|
18
|
+
constructor() { }
|
|
19
|
+
async getToken() {
|
|
20
|
+
if (!this.token) {
|
|
21
|
+
Logger.info('[CPAuthClient] No token found, fetching a new one...');
|
|
22
|
+
await this.connect();
|
|
23
|
+
}
|
|
24
|
+
if (this.expiresAt - Date.now() < 60 * 1000) {
|
|
25
|
+
Logger.info('[CPAuthClient] Token is about to expire, refreshing...');
|
|
26
|
+
await this.connect();
|
|
27
|
+
}
|
|
28
|
+
return this.token;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns the singleton instance of the subclass.
|
|
32
|
+
*/
|
|
33
|
+
static async getInstance() {
|
|
34
|
+
// If no instance exists, create one and store the promise.
|
|
35
|
+
// This ensures that if multiple calls to getInstance() happen concurrently,
|
|
36
|
+
// they will all await the same initialization process.
|
|
37
|
+
if (!this._instance) {
|
|
38
|
+
this._instance = (async () => {
|
|
39
|
+
const instance = new CPAuthClient();
|
|
40
|
+
// This behaves like the constructor,
|
|
41
|
+
// but allows for async initialization.
|
|
42
|
+
await instance.connect();
|
|
43
|
+
return instance;
|
|
44
|
+
})();
|
|
45
|
+
}
|
|
46
|
+
// Await the instance if it's still initializing,
|
|
47
|
+
// or return it immediately if ready.
|
|
48
|
+
return await this._instance;
|
|
49
|
+
}
|
|
50
|
+
async connect() {
|
|
51
|
+
//
|
|
52
|
+
Logger.info('[CPAuthClient] Connecting and fetching token...');
|
|
53
|
+
//
|
|
54
|
+
// Get the authentication URL, which also sets up the SSH tunnel if needed.
|
|
55
|
+
const port = await this.getAuthenticationPort();
|
|
56
|
+
//
|
|
57
|
+
// Make the POST request to the Authentication API through the SSH tunnel,
|
|
58
|
+
// and handle the response, extracting the access token or throwing an error if the request fails.
|
|
59
|
+
const responseResult = await new Promise((resolve, reject) => {
|
|
60
|
+
//
|
|
61
|
+
const requestBody = new URLSearchParams({
|
|
62
|
+
client_id: process.env.CP_AUTH_CLIENT_ID,
|
|
63
|
+
client_secret: process.env.CP_AUTH_CLIENT_SECRET,
|
|
64
|
+
grant_type: 'client_credentials',
|
|
65
|
+
}).toString();
|
|
66
|
+
const requestOptions = {
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Length': Buffer.byteLength(requestBody),
|
|
69
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
70
|
+
'host': process.env.CP_AUTH_HOST,
|
|
71
|
+
},
|
|
72
|
+
host: 'localhost',
|
|
73
|
+
method: 'POST',
|
|
74
|
+
path: process.env.CP_AUTH_PATH,
|
|
75
|
+
port: port,
|
|
76
|
+
rejectUnauthorized: false,
|
|
77
|
+
servername: process.env.CP_AUTH_HOST,
|
|
78
|
+
};
|
|
79
|
+
const callback = (response) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
82
|
+
response.on('end', () => {
|
|
83
|
+
const responseText = Buffer.concat(chunks).toString('utf8');
|
|
84
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
85
|
+
reject(new Error(`[CPAuthClient] Token request failed (${response.statusCode}): ${responseText.slice(0, 500)}`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
resolve(JSON.parse(responseText));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
reject(new Error(`[CPAuthClient] Token response is not JSON: ${responseText.slice(0, 500)}`));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const request = https.request(requestOptions, callback);
|
|
97
|
+
request.on('error', reject);
|
|
98
|
+
request.write(requestBody);
|
|
99
|
+
request.end();
|
|
100
|
+
});
|
|
101
|
+
//
|
|
102
|
+
// With the response data, set the token and calculate the expiration time
|
|
103
|
+
// based on the current time and the expires_in value from the response.
|
|
104
|
+
this.expiresAt = Date.now() + (responseResult.expires_in * 1000);
|
|
105
|
+
this.token = responseResult.access_token;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Constructs the authentication URL based on environment variables
|
|
109
|
+
* and SSH tunneling configuration, and handles both direct connections and SSH-tunneled
|
|
110
|
+
* connections, validating the necessary environment variables for each case.
|
|
111
|
+
* This method is called internally by the service and should not be used directly.
|
|
112
|
+
* @throws Will throw an error if required environment variables are missing or if the SSH tunnel setup fails.
|
|
113
|
+
* @returns A promise that resolves to the authentication URL.
|
|
114
|
+
*/
|
|
115
|
+
async getAuthenticationPort() {
|
|
116
|
+
//
|
|
117
|
+
//
|
|
118
|
+
// Validate required environment variables
|
|
119
|
+
if (!process.env.CP_AUTH_HOST || !process.env.CP_AUTH_PATH) {
|
|
120
|
+
throw new Error('Missing CP_AUTH_HOST or CP_AUTH_PATH environment variables.');
|
|
121
|
+
}
|
|
122
|
+
if (!process.env.CP_AUTH_CLIENT_ID || !process.env.CP_AUTH_CLIENT_SECRET) {
|
|
123
|
+
throw new Error('Missing CP_AUTH_CLIENT_ID or CP_AUTH_CLIENT_SECRET environment variables.');
|
|
124
|
+
}
|
|
125
|
+
if (!process.env.CP_TUNNEL_LOCAL_PORT) {
|
|
126
|
+
throw new Error('Missing CP_TUNNEL_LOCAL_PORT environment variable.');
|
|
127
|
+
}
|
|
128
|
+
if (!process.env.CP_TUNNEL_SSH_HOST || !process.env.CP_TUNNEL_SSH_USERNAME) {
|
|
129
|
+
throw new Error('Missing CP_TUNNEL_SSH_HOST or CP_TUNNEL_SSH_USERNAME environment variables.');
|
|
130
|
+
}
|
|
131
|
+
const sshConfig = {
|
|
132
|
+
forwardOptions: {
|
|
133
|
+
dstAddr: process.env.CP_AUTH_HOST,
|
|
134
|
+
dstPort: 443,
|
|
135
|
+
srcAddr: 'localhost',
|
|
136
|
+
srcPort: Number(process.env.CP_TUNNEL_LOCAL_PORT),
|
|
137
|
+
},
|
|
138
|
+
serverOptions: {
|
|
139
|
+
port: Number(process.env.CP_TUNNEL_LOCAL_PORT),
|
|
140
|
+
},
|
|
141
|
+
sshOptions: {
|
|
142
|
+
agent: process.env.CP_TUNNEL_SSH_KEY_PATH ? undefined : process.env.SSH_AUTH_SOCK,
|
|
143
|
+
host: process.env.CP_TUNNEL_SSH_HOST,
|
|
144
|
+
keepaliveCountMax: 20,
|
|
145
|
+
keepaliveInterval: 10_000,
|
|
146
|
+
port: 22,
|
|
147
|
+
privateKey: process.env.CP_TUNNEL_SSH_KEY_PATH ? readFileSync(process.env.CP_TUNNEL_SSH_KEY_PATH) : process.env.CP_TUNNEL_SSH_KEY,
|
|
148
|
+
username: process.env.CP_TUNNEL_SSH_USERNAME,
|
|
149
|
+
},
|
|
150
|
+
tunnelOptions: {
|
|
151
|
+
autoClose: false,
|
|
152
|
+
reconnectOnError: true,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const sshOptions = {
|
|
156
|
+
maxRetries: 3,
|
|
157
|
+
};
|
|
158
|
+
if (!this.tunnel) {
|
|
159
|
+
this.tunnel = new SshTunnelService(sshConfig, sshOptions);
|
|
160
|
+
await this.tunnel.connect();
|
|
161
|
+
}
|
|
162
|
+
Logger.info('[CPAuthClient] Setting up SSH Tunnel...');
|
|
163
|
+
const addr = this.tunnel.server.address();
|
|
164
|
+
if (!addr || typeof addr !== 'object') {
|
|
165
|
+
throw new Error('[CPAuthClient] Failed to retrieve SSH tunnel address.');
|
|
166
|
+
}
|
|
167
|
+
return addr.port;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/* * */
|
|
171
|
+
export const cpAuthClient = asyncSingletonProxy(CPAuthClient);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const CpClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches the latest GTFS static schedule feed (gtfs.zip) from the CP Partner API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<Buffer>} A promise that resolves with the GTFS zip file as a Buffer.
|
|
7
|
+
*/
|
|
8
|
+
schedule: () => Promise<Buffer>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetches the GTFS-RT Trip Updates feed from the CP Partner API.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
13
|
+
*/
|
|
14
|
+
tripUpdates: () => Promise<GtfsRtFeedMessage>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetches the GTFS-RT Vehicle Positions feed from the CP Partner API.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
19
|
+
*/
|
|
20
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
import { cpAuthClient } from './auth.js';
|
|
4
|
+
/* * */
|
|
5
|
+
const BASE_URL = process.env.CP_API_URL;
|
|
6
|
+
async function fetcher(endpoint) {
|
|
7
|
+
if (!BASE_URL) {
|
|
8
|
+
throw new Error('Missing CP_API_URL environment variable.');
|
|
9
|
+
}
|
|
10
|
+
//
|
|
11
|
+
// Get the API token
|
|
12
|
+
const apiToken = await cpAuthClient.getToken();
|
|
13
|
+
//
|
|
14
|
+
// Fetch the CP Trip Updates data from API and decode it.
|
|
15
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
16
|
+
headers: {
|
|
17
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
18
|
+
'x-cp-connect-id': process.env.CP_API_KEY,
|
|
19
|
+
'x-cp-connect-secret': process.env.CP_API_SECRET,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
const endpoints = {
|
|
28
|
+
schedule: '/schedule/gtfs.zip',
|
|
29
|
+
tripUpdates: '/realtime/TripUpdates.pb',
|
|
30
|
+
vehiclePositions: '/realtime/VehiclePositions.pb',
|
|
31
|
+
};
|
|
32
|
+
export const CpClient = Object.freeze({
|
|
33
|
+
//
|
|
34
|
+
/**
|
|
35
|
+
* Fetches the latest GTFS static schedule feed (gtfs.zip) from the CP Partner API.
|
|
36
|
+
*
|
|
37
|
+
* @returns {Promise<Buffer>} A promise that resolves with the GTFS zip file as a Buffer.
|
|
38
|
+
*/
|
|
39
|
+
schedule: async () => {
|
|
40
|
+
const response = await fetcher(endpoints.schedule);
|
|
41
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
42
|
+
return Buffer.from(arrayBuffer);
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Fetches the GTFS-RT Trip Updates feed from the CP Partner API.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
48
|
+
*/
|
|
49
|
+
tripUpdates: async () => {
|
|
50
|
+
const response = await fetcher(endpoints.tripUpdates);
|
|
51
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
52
|
+
const decodedMessage = await decodeGtfsRtFeed(arrayBuffer);
|
|
53
|
+
return decodedMessage;
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Fetches the GTFS-RT Vehicle Positions feed from the CP Partner API.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
59
|
+
*/
|
|
60
|
+
vehiclePositions: async () => {
|
|
61
|
+
const response = await fetcher(endpoints.vehiclePositions);
|
|
62
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
63
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const CrtmAisaClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches GTFS-RT Vehicle Positions feed from the CRTM AISA API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
7
|
+
*/
|
|
8
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetches GTFS-RT Trip Updates feed from the CRTM AISA API.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
13
|
+
*/
|
|
14
|
+
tripUpdates: () => Promise<GtfsRtFeedMessage>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetches GTFS-RT Schedule feed from the CRTM AISA API.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<Buffer>} A promise that resolves with the GTFS zip file as a Buffer.
|
|
19
|
+
*/
|
|
20
|
+
schedule: () => Promise<Buffer>;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.CRTM_AISA_API_URL;
|
|
5
|
+
async function fetcher(endpoint) {
|
|
6
|
+
if (!BASE_URL) {
|
|
7
|
+
throw new Error('Missing CRTM_AISA_API_URL environment variable.');
|
|
8
|
+
}
|
|
9
|
+
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
const endpoints = {
|
|
16
|
+
schedule: '/GTFS-SC/gtfs_sc.zip',
|
|
17
|
+
tripUpdates: '/GTFS-RT/actualizacionViaje',
|
|
18
|
+
vehiclePositions: '/GTFS-RT/vehiculosPosicion',
|
|
19
|
+
};
|
|
20
|
+
export const CrtmAisaClient = Object.freeze({
|
|
21
|
+
//
|
|
22
|
+
/**
|
|
23
|
+
* Fetches GTFS-RT Vehicle Positions feed from the CRTM AISA API.
|
|
24
|
+
*
|
|
25
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
26
|
+
*/
|
|
27
|
+
vehiclePositions: async () => {
|
|
28
|
+
const response = await fetcher(endpoints.vehiclePositions);
|
|
29
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
30
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Fetches GTFS-RT Trip Updates feed from the CRTM AISA API.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
36
|
+
*/
|
|
37
|
+
tripUpdates: async () => {
|
|
38
|
+
const response = await fetcher(endpoints.tripUpdates);
|
|
39
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
40
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Fetches GTFS-RT Schedule feed from the CRTM AISA API.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<Buffer>} A promise that resolves with the GTFS zip file as a Buffer.
|
|
46
|
+
*/
|
|
47
|
+
schedule: async () => {
|
|
48
|
+
const response = await fetcher(endpoints.schedule);
|
|
49
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
50
|
+
return Buffer.from(arrayBuffer);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class FertagusAuthClient {
|
|
2
|
+
private static _instance;
|
|
3
|
+
private expiresAt;
|
|
4
|
+
private token;
|
|
5
|
+
/**
|
|
6
|
+
* Disallow direct instantiation of the service.
|
|
7
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
8
|
+
*/
|
|
9
|
+
private constructor();
|
|
10
|
+
getToken(): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the singleton instance of the subclass.
|
|
13
|
+
*/
|
|
14
|
+
static getInstance(): Promise<FertagusAuthClient>;
|
|
15
|
+
private connect;
|
|
16
|
+
}
|
|
17
|
+
export declare const fertagusAuthClient: FertagusAuthClient;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { Logger } from '@tmlmobilidade/logger';
|
|
3
|
+
import { asyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
4
|
+
/* * */
|
|
5
|
+
export class FertagusAuthClient {
|
|
6
|
+
//
|
|
7
|
+
static _instance = null;
|
|
8
|
+
expiresAt = 0;
|
|
9
|
+
token = null;
|
|
10
|
+
/**
|
|
11
|
+
* Disallow direct instantiation of the service.
|
|
12
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
13
|
+
*/
|
|
14
|
+
constructor() { }
|
|
15
|
+
async getToken() {
|
|
16
|
+
if (!this.token) {
|
|
17
|
+
Logger.info('[FertagusAuthClient] No token found, fetching a new one...');
|
|
18
|
+
await this.connect();
|
|
19
|
+
}
|
|
20
|
+
if (this.expiresAt - Date.now() < 60 * 1000) {
|
|
21
|
+
Logger.info('[FertagusAuthClient] Token is about to expire, refreshing...');
|
|
22
|
+
await this.connect();
|
|
23
|
+
}
|
|
24
|
+
return this.token;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns the singleton instance of the subclass.
|
|
28
|
+
*/
|
|
29
|
+
static async getInstance() {
|
|
30
|
+
// If no instance exists, create one and store the promise.
|
|
31
|
+
// This ensures that if multiple calls to getInstance() happen concurrently,
|
|
32
|
+
// they will all await the same initialization process.
|
|
33
|
+
if (!this._instance) {
|
|
34
|
+
this._instance = (async () => {
|
|
35
|
+
const instance = new FertagusAuthClient();
|
|
36
|
+
// This behaves like the constructor,
|
|
37
|
+
// but allows for async initialization.
|
|
38
|
+
await instance.connect();
|
|
39
|
+
return instance;
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
// Await the instance if it's still initializing,
|
|
43
|
+
// or return it immediately if ready.
|
|
44
|
+
return await this._instance;
|
|
45
|
+
}
|
|
46
|
+
async connect() {
|
|
47
|
+
//
|
|
48
|
+
Logger.info('[FertagusAuthClient] Connecting and fetching token...');
|
|
49
|
+
//
|
|
50
|
+
// Make the POST request to the Authentication API through the SSH tunnel,
|
|
51
|
+
// and handle the response, extracting the access token or throwing an error if the request fails.
|
|
52
|
+
const requestBody = new URLSearchParams({
|
|
53
|
+
grant_type: 'client_credentials',
|
|
54
|
+
password: process.env.FERTAGUS_AUTH_PASSWORD,
|
|
55
|
+
username: process.env.FERTAGUS_AUTH_USERNAME,
|
|
56
|
+
}).toString();
|
|
57
|
+
const response = await fetch(process.env.FERTAGUS_AUTH_URL, {
|
|
58
|
+
body: requestBody,
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
},
|
|
62
|
+
method: 'POST',
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`[FertagusAuthClient] Token request failed (${response.status}): ${response.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
//
|
|
69
|
+
// With the response data, set the token and calculate the expiration time
|
|
70
|
+
// based on the current time and the expires_in value from the response.
|
|
71
|
+
this.expiresAt = Date.now() + (data.expires_in * 1000);
|
|
72
|
+
this.token = data.access_token;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/* * */
|
|
76
|
+
export const fertagusAuthClient = asyncSingletonProxy(FertagusAuthClient);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TrainsResponse } from './types.js';
|
|
2
|
+
export declare const FertagusClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Gets the status of all Fertagus trains.
|
|
5
|
+
* @returns TrainsResponse object with the current status of all trains.
|
|
6
|
+
*/
|
|
7
|
+
trains: () => Promise<TrainsResponse>;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { fertagusAuthClient } from './auth.js';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.FERTAGUS_API_URL;
|
|
5
|
+
async function fetcher(endpoint) {
|
|
6
|
+
if (!BASE_URL) {
|
|
7
|
+
throw new Error('Missing FERTAGUS_API_URL environment variable.');
|
|
8
|
+
}
|
|
9
|
+
const apiToken = await fertagusAuthClient.getToken();
|
|
10
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${apiToken}`,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
17
|
+
}
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
/* * */
|
|
21
|
+
const endpoints = {
|
|
22
|
+
trains: '/trains',
|
|
23
|
+
};
|
|
24
|
+
export const FertagusClient = Object.freeze({
|
|
25
|
+
/**
|
|
26
|
+
* Gets the status of all Fertagus trains.
|
|
27
|
+
* @returns TrainsResponse object with the current status of all trains.
|
|
28
|
+
*/
|
|
29
|
+
trains: async () => {
|
|
30
|
+
const response = await fetcher(endpoints.trains);
|
|
31
|
+
return await response.json();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see GET /trains
|
|
3
|
+
*/
|
|
4
|
+
export type TrainsResponse = Array<{
|
|
5
|
+
date: string;
|
|
6
|
+
latitude: null | number;
|
|
7
|
+
longitude: null | number;
|
|
8
|
+
startsAt: null | string;
|
|
9
|
+
stop_id_end: null | string;
|
|
10
|
+
stop_id_start: null | string;
|
|
11
|
+
train_id: null | string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class MLAuthClient {
|
|
2
|
+
private static _instance;
|
|
3
|
+
private expiresAt;
|
|
4
|
+
private token;
|
|
5
|
+
private tunnel;
|
|
6
|
+
/**
|
|
7
|
+
* Disallow direct instantiation of the service.
|
|
8
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
9
|
+
*/
|
|
10
|
+
private constructor();
|
|
11
|
+
getToken(): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the singleton instance of the subclass.
|
|
14
|
+
*/
|
|
15
|
+
static getInstance(): Promise<MLAuthClient>;
|
|
16
|
+
private connect;
|
|
17
|
+
}
|
|
18
|
+
export declare const mlAuthClient: MLAuthClient;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { Logger } from '@tmlmobilidade/logger';
|
|
3
|
+
import { asyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
4
|
+
/* * */
|
|
5
|
+
export class MLAuthClient {
|
|
6
|
+
//
|
|
7
|
+
static _instance = null;
|
|
8
|
+
expiresAt = 0;
|
|
9
|
+
token = null;
|
|
10
|
+
tunnel = null;
|
|
11
|
+
/**
|
|
12
|
+
* Disallow direct instantiation of the service.
|
|
13
|
+
* Use getToken() instead to ensure singleton behavior.
|
|
14
|
+
*/
|
|
15
|
+
constructor() { }
|
|
16
|
+
async getToken() {
|
|
17
|
+
if (!this.token) {
|
|
18
|
+
Logger.info('[MLAuthClient] No token found, fetching a new one...');
|
|
19
|
+
await this.connect();
|
|
20
|
+
}
|
|
21
|
+
if (this.expiresAt - Date.now() < 60 * 1000) {
|
|
22
|
+
Logger.info('[MLAuthClient] Token is about to expire, refreshing...');
|
|
23
|
+
await this.connect();
|
|
24
|
+
}
|
|
25
|
+
return this.token;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the singleton instance of the subclass.
|
|
29
|
+
*/
|
|
30
|
+
static async getInstance() {
|
|
31
|
+
// If no instance exists, create one and store the promise.
|
|
32
|
+
// This ensures that if multiple calls to getInstance() happen concurrently,
|
|
33
|
+
// they will all await the same initialization process.
|
|
34
|
+
if (!this._instance) {
|
|
35
|
+
this._instance = (async () => {
|
|
36
|
+
const instance = new MLAuthClient();
|
|
37
|
+
// This behaves like the constructor,
|
|
38
|
+
// but allows for async initialization.
|
|
39
|
+
await instance.connect();
|
|
40
|
+
return instance;
|
|
41
|
+
})();
|
|
42
|
+
}
|
|
43
|
+
// Await the instance if it's still initializing,
|
|
44
|
+
// or return it immediately if ready.
|
|
45
|
+
return await this._instance;
|
|
46
|
+
}
|
|
47
|
+
async connect() {
|
|
48
|
+
//
|
|
49
|
+
Logger.info('[MLAuthClient] Connecting and fetching token...');
|
|
50
|
+
//
|
|
51
|
+
// Make the POST request to the Authentication API through the SSH tunnel,
|
|
52
|
+
// and handle the response, extracting the access token or throwing an error if the request fails.
|
|
53
|
+
const requestBody = new URLSearchParams({
|
|
54
|
+
grant_type: 'password',
|
|
55
|
+
password: process.env.ML_AUTH_PASSWORD,
|
|
56
|
+
username: process.env.ML_AUTH_USERNAME,
|
|
57
|
+
}).toString();
|
|
58
|
+
const response = await fetch(process.env.ML_AUTH_URL, {
|
|
59
|
+
body: requestBody,
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
62
|
+
},
|
|
63
|
+
method: 'POST',
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`[MLAuthClient] Token request failed (${response.status}): ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
//
|
|
70
|
+
// With the response data, set the token and calculate the expiration time
|
|
71
|
+
// based on the current time and the expires_in value from the response.
|
|
72
|
+
this.expiresAt = Date.now() + (data.expires_in * 1000);
|
|
73
|
+
this.token = data.access_token;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/* * */
|
|
77
|
+
export const mlAuthClient = asyncSingletonProxy(MLAuthClient);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ServiceAlertResponse } from '@tmlmobilidade/types';
|
|
2
|
+
import { BaseResponse, EstadoLinha, InfoEstacao, TempoEspera } from './types.js';
|
|
3
|
+
export declare const MlClient: Readonly<{
|
|
4
|
+
/**
|
|
5
|
+
* Gets the status of a specific Metro Lisboa line.
|
|
6
|
+
* @param linha Line identifier as string.
|
|
7
|
+
* @returns EstadoLinha object with the current status of the line.
|
|
8
|
+
*/
|
|
9
|
+
estadoLinha: (linha: string) => Promise<BaseResponse<EstadoLinha>>;
|
|
10
|
+
/**
|
|
11
|
+
* Gets the status of all Metro Lisboa lines.
|
|
12
|
+
* @returns An array of EstadoLinha objects, each representing the status of a line.
|
|
13
|
+
*/
|
|
14
|
+
estadoLinhaTodas: () => Promise<BaseResponse<EstadoLinha[]>>;
|
|
15
|
+
/**
|
|
16
|
+
* Gets information about a specific Metro Lisboa station.
|
|
17
|
+
* @param estacao Station identifier as string.
|
|
18
|
+
* @returns InfoEstacao object with details about the station.
|
|
19
|
+
*/
|
|
20
|
+
infoEstacao: (estacao: string) => Promise<BaseResponse<InfoEstacao>>;
|
|
21
|
+
/**
|
|
22
|
+
* Gets information about all Metro Lisboa stations.
|
|
23
|
+
* @returns An array of InfoEstacao objects, one for each station.
|
|
24
|
+
*/
|
|
25
|
+
infoEstacaoTodas: () => Promise<BaseResponse<InfoEstacao[]>>;
|
|
26
|
+
/**
|
|
27
|
+
* Retrieves current Metro Lisboa service alerts.
|
|
28
|
+
* @returns A ServiceAlertResponse containing the active service alerts in GTFS-realtime format.
|
|
29
|
+
*/
|
|
30
|
+
serviceAlerts: () => Promise<BaseResponse<ServiceAlertResponse>>;
|
|
31
|
+
/**
|
|
32
|
+
* Gets the current waiting time estimates for a specific station.
|
|
33
|
+
* @param estacao Station identifier as string.
|
|
34
|
+
* @returns TempoEspera object with estimated waiting times for the station.
|
|
35
|
+
*/
|
|
36
|
+
tempoEsperaEstacao: (estacao: string) => Promise<BaseResponse<TempoEspera>>;
|
|
37
|
+
/**
|
|
38
|
+
* Gets the current waiting time estimates for a specific line.
|
|
39
|
+
* @param linha Line identifier as string.
|
|
40
|
+
* @returns An array of TempoEspera objects, one for each station in the line.
|
|
41
|
+
*/
|
|
42
|
+
tempoEsperaLinha: (linha: string) => Promise<BaseResponse<TempoEspera[]>>;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the current waiting time estimates for all stations.
|
|
45
|
+
* @returns An array of TempoEspera objects, one for each station.
|
|
46
|
+
*/
|
|
47
|
+
tempoEsperaTodasEstacoes: () => Promise<BaseResponse<TempoEspera[]>>;
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { mlAuthClient } from './auth.js';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.ML_API_URL;
|
|
5
|
+
const ALERTS_URL = process.env.ML_ALERTS_URL;
|
|
6
|
+
async function fetcher(endpoint) {
|
|
7
|
+
if (!BASE_URL) {
|
|
8
|
+
throw new Error('Missing ML_API_URL environment variable.');
|
|
9
|
+
}
|
|
10
|
+
const apiToken = await mlAuthClient.getToken();
|
|
11
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${apiToken}`,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
/* * */
|
|
22
|
+
const endpoints = {
|
|
23
|
+
estadoLinha: (linha) => `/estadoLinha/${linha}`,
|
|
24
|
+
estadoLinhaTodas: '/estadoLinha/todos',
|
|
25
|
+
infoEstacao: (estacao) => `/infoEstacao/${estacao}`,
|
|
26
|
+
infoEstacaoTodas: '/infoEstacao/todos',
|
|
27
|
+
serviceAlerts: ALERTS_URL,
|
|
28
|
+
tempoEsperaEstacao: (estacao) => `/tempoEspera/Estacao/${estacao}`,
|
|
29
|
+
tempoEsperaLinha: (linha) => `/tempoEspera/Linha/${linha}`,
|
|
30
|
+
tempoEsperaTodasEstacoes: '/tempoEspera/Estacao/todos',
|
|
31
|
+
};
|
|
32
|
+
export const MlClient = Object.freeze({
|
|
33
|
+
/**
|
|
34
|
+
* Gets the status of a specific Metro Lisboa line.
|
|
35
|
+
* @param linha Line identifier as string.
|
|
36
|
+
* @returns EstadoLinha object with the current status of the line.
|
|
37
|
+
*/
|
|
38
|
+
estadoLinha: async (linha) => {
|
|
39
|
+
const response = await fetcher(endpoints.estadoLinha(linha));
|
|
40
|
+
return await response.json();
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Gets the status of all Metro Lisboa lines.
|
|
44
|
+
* @returns An array of EstadoLinha objects, each representing the status of a line.
|
|
45
|
+
*/
|
|
46
|
+
estadoLinhaTodas: async () => {
|
|
47
|
+
const response = await fetcher(endpoints.estadoLinhaTodas);
|
|
48
|
+
return await response.json();
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Gets information about a specific Metro Lisboa station.
|
|
52
|
+
* @param estacao Station identifier as string.
|
|
53
|
+
* @returns InfoEstacao object with details about the station.
|
|
54
|
+
*/
|
|
55
|
+
infoEstacao: async (estacao) => {
|
|
56
|
+
const response = await fetcher(endpoints.infoEstacao(estacao));
|
|
57
|
+
return await response.json();
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Gets information about all Metro Lisboa stations.
|
|
61
|
+
* @returns An array of InfoEstacao objects, one for each station.
|
|
62
|
+
*/
|
|
63
|
+
infoEstacaoTodas: async () => {
|
|
64
|
+
const response = await fetcher(endpoints.infoEstacaoTodas);
|
|
65
|
+
return await response.json();
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Retrieves current Metro Lisboa service alerts.
|
|
69
|
+
* @returns A ServiceAlertResponse containing the active service alerts in GTFS-realtime format.
|
|
70
|
+
*/
|
|
71
|
+
serviceAlerts: async () => {
|
|
72
|
+
const response = await fetcher(endpoints.serviceAlerts);
|
|
73
|
+
return await response.json();
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Gets the current waiting time estimates for a specific station.
|
|
77
|
+
* @param estacao Station identifier as string.
|
|
78
|
+
* @returns TempoEspera object with estimated waiting times for the station.
|
|
79
|
+
*/
|
|
80
|
+
tempoEsperaEstacao: async (estacao) => {
|
|
81
|
+
const response = await fetcher(endpoints.tempoEsperaEstacao(estacao));
|
|
82
|
+
return await response.json();
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Gets the current waiting time estimates for a specific line.
|
|
86
|
+
* @param linha Line identifier as string.
|
|
87
|
+
* @returns An array of TempoEspera objects, one for each station in the line.
|
|
88
|
+
*/
|
|
89
|
+
tempoEsperaLinha: async (linha) => {
|
|
90
|
+
const response = await fetcher(endpoints.tempoEsperaLinha(linha));
|
|
91
|
+
return await response.json();
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* Gets the current waiting time estimates for all stations.
|
|
95
|
+
* @returns An array of TempoEspera objects, one for each station.
|
|
96
|
+
*/
|
|
97
|
+
tempoEsperaTodasEstacoes: async () => {
|
|
98
|
+
const response = await fetcher(endpoints.tempoEsperaTodasEstacoes);
|
|
99
|
+
return await response.json();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface BaseResponse<T> {
|
|
2
|
+
codigo: number;
|
|
3
|
+
resposta: T;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* @see GET /tempoEspera/Estacao/todos
|
|
7
|
+
* @see GET /tempoEspera/Estacao/{estacao}
|
|
8
|
+
* @see GET /tempoEspera/Linha/{linha}
|
|
9
|
+
*/
|
|
10
|
+
export interface TempoEspera {
|
|
11
|
+
comboio: string;
|
|
12
|
+
destino: string;
|
|
13
|
+
estacao: string;
|
|
14
|
+
linha: string;
|
|
15
|
+
nomeEstacao: string;
|
|
16
|
+
tempoEspera: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @see GET /infoEstacao/todos
|
|
20
|
+
* @see GET /infoEstacao/{estacao}
|
|
21
|
+
*/
|
|
22
|
+
export interface InfoEstacao {
|
|
23
|
+
id: string;
|
|
24
|
+
latitude: number;
|
|
25
|
+
linha: string;
|
|
26
|
+
longitude: number;
|
|
27
|
+
nome: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @see GET /estadoLinha/todos
|
|
32
|
+
* @see GET /estadoLinha/{linha}
|
|
33
|
+
*/
|
|
34
|
+
export interface EstadoLinha {
|
|
35
|
+
descricao: string;
|
|
36
|
+
estado: string;
|
|
37
|
+
linha: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const MobiClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches GTFS-RT Trip Updates feed from the Mobi API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
7
|
+
*/
|
|
8
|
+
tripUpdates: () => Promise<GtfsRtFeedMessage>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetches GTFS-RT Vehicle Positions feed from the Mobi API.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
13
|
+
*/
|
|
14
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.MOBI_API_URL;
|
|
5
|
+
const AUTH_TOKEN = Buffer.from(`${process.env.MOBI_API_USERNAME}:${process.env.MOBI_API_PASSWORD}`).toString('base64');
|
|
6
|
+
async function mobiFetch(endpoint) {
|
|
7
|
+
if (!BASE_URL) {
|
|
8
|
+
throw new Error('Missing MOBI_API_URL environment variable.');
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, { headers: { Authorization: `Basic ${AUTH_TOKEN}` } });
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
const endpoints = {
|
|
17
|
+
tripUpdates: '/gtfs-rt/tripUpdates',
|
|
18
|
+
vehiclePositions: '/gtfs-rt/vehiclePositions',
|
|
19
|
+
};
|
|
20
|
+
export const MobiClient = Object.freeze({
|
|
21
|
+
//
|
|
22
|
+
/**
|
|
23
|
+
* Fetches GTFS-RT Trip Updates feed from the Mobi API.
|
|
24
|
+
*
|
|
25
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Trip Updates feed message.
|
|
26
|
+
*/
|
|
27
|
+
tripUpdates: async () => {
|
|
28
|
+
const response = await mobiFetch(endpoints.tripUpdates);
|
|
29
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
30
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Fetches GTFS-RT Vehicle Positions feed from the Mobi API.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
36
|
+
*/
|
|
37
|
+
vehiclePositions: async () => {
|
|
38
|
+
const response = await mobiFetch(endpoints.vehiclePositions);
|
|
39
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
40
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const TcbClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches GTFS-RT Vehicle Positions feed from the TCB API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
7
|
+
*/
|
|
8
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.TCB_API_URL;
|
|
5
|
+
async function fetcher(endpoint) {
|
|
6
|
+
if (!BASE_URL) {
|
|
7
|
+
throw new Error('Missing TCB_API_URL environment variable.');
|
|
8
|
+
}
|
|
9
|
+
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
const endpoints = {
|
|
16
|
+
vehiclePositions: '/gtfs-realtime',
|
|
17
|
+
};
|
|
18
|
+
export const TcbClient = Object.freeze({
|
|
19
|
+
//
|
|
20
|
+
/**
|
|
21
|
+
* Fetches GTFS-RT Vehicle Positions feed from the TCB API.
|
|
22
|
+
*
|
|
23
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
24
|
+
*/
|
|
25
|
+
vehiclePositions: async () => {
|
|
26
|
+
const response = await fetcher(endpoints.vehiclePositions);
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type GtfsRtFeedMessage } from '@tmlmobilidade/types';
|
|
2
|
+
export declare const TtslClient: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Fetches GTFS-RT Vehicle Positions feed from the TTSL API.
|
|
5
|
+
*
|
|
6
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
7
|
+
*/
|
|
8
|
+
vehiclePositions: () => Promise<GtfsRtFeedMessage>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { decodeGtfsRtFeed } from '@tmlmobilidade/gtfs-rt';
|
|
3
|
+
/* * */
|
|
4
|
+
const BASE_URL = process.env.TTSL_API_URL;
|
|
5
|
+
async function fetcher(endpoint) {
|
|
6
|
+
if (!BASE_URL) {
|
|
7
|
+
throw new Error('Missing TTSL_API_URL environment variable.');
|
|
8
|
+
}
|
|
9
|
+
const response = await fetch(`${BASE_URL}${endpoint}`);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Request failed (${response.status}): ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
const endpoints = {
|
|
16
|
+
vehiclePositions: '/files/gtfs_rt_vehicles.pb',
|
|
17
|
+
};
|
|
18
|
+
export const TtslClient = Object.freeze({
|
|
19
|
+
//
|
|
20
|
+
/**
|
|
21
|
+
* Fetches GTFS-RT Vehicle Positions feed from the TTSL API.
|
|
22
|
+
*
|
|
23
|
+
* @returns {Promise<GtfsRtFeedMessage>} A promise that resolves with the decoded GTFS-RT Vehicle Positions feed message.
|
|
24
|
+
*/
|
|
25
|
+
vehiclePositions: async () => {
|
|
26
|
+
const response = await fetcher(endpoints.vehiclePositions);
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
return decodeGtfsRtFeed(arrayBuffer);
|
|
29
|
+
},
|
|
30
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection of external transport agency clients.
|
|
3
|
+
*
|
|
4
|
+
* Each property provides a typed client to interact with the public API
|
|
5
|
+
* of a specific transport agency. These clients offer methods to fetch
|
|
6
|
+
* GTFS-RT feeds and other relevant live data where available.
|
|
7
|
+
*
|
|
8
|
+
* - ccfl: Companhia dos Caminhos de Ferro de Lisboa (CCFL) AKA Carris Munícipal API Client.
|
|
9
|
+
* - cp: Comboios de Portugal (CP) API Client
|
|
10
|
+
* - crtmAisa: Consorcio Regional de Transportes de Madrid (AISACRTM) API Client
|
|
11
|
+
* - mobi: MobiCascais API Client
|
|
12
|
+
* - ml: Metro Lisboa (ML) API Client
|
|
13
|
+
* - tcb: Transportes Colectivos do Barreiro (TCB) API Client
|
|
14
|
+
* - ttsl: Transtejo Soflusa (TTSL) API Client
|
|
15
|
+
*/
|
|
16
|
+
export declare const externalClients: Readonly<{
|
|
17
|
+
ccfl: Readonly<{
|
|
18
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
19
|
+
schedule: () => Promise<Buffer>;
|
|
20
|
+
}>;
|
|
21
|
+
cp: Readonly<{
|
|
22
|
+
schedule: () => Promise<Buffer>;
|
|
23
|
+
tripUpdates: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
24
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
25
|
+
}>;
|
|
26
|
+
crtmAisa: Readonly<{
|
|
27
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
28
|
+
tripUpdates: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
29
|
+
schedule: () => Promise<Buffer>;
|
|
30
|
+
}>;
|
|
31
|
+
fertagus: Readonly<{
|
|
32
|
+
trains: () => Promise<import("./clients/fertagus/types.js").TrainsResponse>;
|
|
33
|
+
}>;
|
|
34
|
+
ml: Readonly<{
|
|
35
|
+
estadoLinha: (linha: string) => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").EstadoLinha>>;
|
|
36
|
+
estadoLinhaTodas: () => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").EstadoLinha[]>>;
|
|
37
|
+
infoEstacao: (estacao: string) => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").InfoEstacao>>;
|
|
38
|
+
infoEstacaoTodas: () => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").InfoEstacao[]>>;
|
|
39
|
+
serviceAlerts: () => Promise<import("./clients/ml/types.js").BaseResponse<import("@tmlmobilidade/types").ServiceAlertResponse>>;
|
|
40
|
+
tempoEsperaEstacao: (estacao: string) => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").TempoEspera>>;
|
|
41
|
+
tempoEsperaLinha: (linha: string) => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").TempoEspera[]>>;
|
|
42
|
+
tempoEsperaTodasEstacoes: () => Promise<import("./clients/ml/types.js").BaseResponse<import("./clients/ml/types.js").TempoEspera[]>>;
|
|
43
|
+
}>;
|
|
44
|
+
mobi: Readonly<{
|
|
45
|
+
tripUpdates: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
46
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
47
|
+
}>;
|
|
48
|
+
tcb: Readonly<{
|
|
49
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
50
|
+
}>;
|
|
51
|
+
ttsl: Readonly<{
|
|
52
|
+
vehiclePositions: () => Promise<import("@tmlmobilidade/types").GtfsRtFeedMessage>;
|
|
53
|
+
}>;
|
|
54
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { CcflClient } from './clients/ccfl/index.js';
|
|
3
|
+
import { CpClient } from './clients/cp/index.js';
|
|
4
|
+
import { CrtmAisaClient } from './clients/crtm-aisa/index.js';
|
|
5
|
+
import { FertagusClient } from './clients/fertagus/index.js';
|
|
6
|
+
import { MlClient } from './clients/ml/index.js';
|
|
7
|
+
import { MobiClient } from './clients/mobi/index.js';
|
|
8
|
+
import { TcbClient } from './clients/tcb/index.js';
|
|
9
|
+
import { TtslClient } from './clients/ttsl/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Collection of external transport agency clients.
|
|
12
|
+
*
|
|
13
|
+
* Each property provides a typed client to interact with the public API
|
|
14
|
+
* of a specific transport agency. These clients offer methods to fetch
|
|
15
|
+
* GTFS-RT feeds and other relevant live data where available.
|
|
16
|
+
*
|
|
17
|
+
* - ccfl: Companhia dos Caminhos de Ferro de Lisboa (CCFL) AKA Carris Munícipal API Client.
|
|
18
|
+
* - cp: Comboios de Portugal (CP) API Client
|
|
19
|
+
* - crtmAisa: Consorcio Regional de Transportes de Madrid (AISACRTM) API Client
|
|
20
|
+
* - mobi: MobiCascais API Client
|
|
21
|
+
* - ml: Metro Lisboa (ML) API Client
|
|
22
|
+
* - tcb: Transportes Colectivos do Barreiro (TCB) API Client
|
|
23
|
+
* - ttsl: Transtejo Soflusa (TTSL) API Client
|
|
24
|
+
*/
|
|
25
|
+
export const externalClients = Object.freeze({
|
|
26
|
+
ccfl: CcflClient,
|
|
27
|
+
cp: CpClient,
|
|
28
|
+
crtmAisa: CrtmAisaClient,
|
|
29
|
+
fertagus: FertagusClient,
|
|
30
|
+
ml: MlClient,
|
|
31
|
+
mobi: MobiClient,
|
|
32
|
+
tcb: TcbClient,
|
|
33
|
+
ttsl: TtslClient,
|
|
34
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmlmobilidade/external",
|
|
3
|
+
"version": "20260604.9.7",
|
|
4
|
+
"author": {
|
|
5
|
+
"email": "iso@tmlmobilidade.pt",
|
|
6
|
+
"name": "TML-ISO"
|
|
7
|
+
},
|
|
8
|
+
"license": "AGPL-3.0-or-later",
|
|
9
|
+
"homepage": "https://go.tmlmobilidade.pt",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/tmlmobilidade/go/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/tmlmobilidade/go.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"external",
|
|
19
|
+
"tml",
|
|
20
|
+
"transportes metropolitanos de lisboa",
|
|
21
|
+
"tmlmobilidade"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc && resolve-tspaths",
|
|
34
|
+
"lint": "eslint ./src/ && tsc --noEmit",
|
|
35
|
+
"lint:fix": "eslint ./src/ --fix",
|
|
36
|
+
"watch": "tsc-watch --onSuccess 'resolve-tspaths'"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@tmlmobilidade/gtfs-rt": "*",
|
|
40
|
+
"@tmlmobilidade/logger": "*",
|
|
41
|
+
"@tmlmobilidade/ssh": "*",
|
|
42
|
+
"@tmlmobilidade/types": "*",
|
|
43
|
+
"@tmlmobilidade/utils": "*"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@tmlmobilidade/tsconfig": "*",
|
|
47
|
+
"@types/node": "25.9.1",
|
|
48
|
+
"resolve-tspaths": "0.8.23",
|
|
49
|
+
"tsc-watch": "7.2.0",
|
|
50
|
+
"typescript": "5.9.3"
|
|
51
|
+
}
|
|
52
|
+
}
|