This repository has been archived by the owner on Sep 14, 2021. It is now read-only.
forked from carlosesilva/iphone-x-availability-node-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
232 lines (206 loc) · 6.73 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Require npm packages.
const fetch = require('node-fetch');
const commandLineArgs = require('command-line-args');
const getUsage = require('command-line-usage');
const AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: '', // TODO: FIXME
secretAccessKey: '', // TODO: FIXME
region: 'us-west-2'
});
var sns = new AWS.SNS();
// Get partNumbers from json file.
const partNumbers = require('./partNumbers.json');
// Define command line args accepted.
const optionDefinitions = [
{
name: 'carrier',
type: String,
defaultValue: 'TMOBILE',
description:
"Define which carrier to search for. Accepted options are: 'ATT', 'SPRING', 'TMOBILE', 'VERIZON'.",
},
{
name: 'model',
type: String,
defaultValue: 'x',
description:
"Define which model iPhone to search for. 'x' is the only option currently available.",
},
{
name: 'color',
type: String,
defaultValue: 'gray',
description:
"Define which color iPhone to search for. Accepted options are: 'silver', 'gray'.",
},
{
name: 'storage',
type: String,
defaultValue: 256,
description: "Define which storage size to search for. Accepted options are: '64', '256'.",
},
{
name: 'zip',
type: String,
defaultOption: true,
description: 'Define the area to search in by zip code. This option is required.',
},
{
name: 'delay',
type: Number,
defaultValue: 30,
description: 'Define the number of seconds between requests.',
},
{ name: 'help', type: Boolean, description: 'Display this help screen.' },
];
// Parse command line args.
const options = commandLineArgs(optionDefinitions);
// Define the help screen to be displayed if --help is present in options
const usageDefinition = [
{
header: 'iPhone X Availability Node CLI',
content:
"The app continously makes requests to Apple's availability api. When it finds some new stock near you, it displays the stores' name and distance from your zipcode then exits the program.",
},
{
header: 'Synopsis',
content: [
{
desc: 'Default arguments.',
example:
'$ node index.js [bold]{--carrier} TMOBILE [bold]{--model} x [bold]{--color} gray [bold]{--storage} 256 [bold]{--delay} 30',
},
{
desc: 'Simple example',
example: '$ node index.js [bold]{--zip} 10001 [bold]{--color} silver',
},
{
desc: 'Help screen.',
example: '$ node index.js [bold]{--help}',
},
],
},
{
header: 'Options',
optionList: optionDefinitions,
},
];
// if --help is present or --zip wasn't defined,
// then display the help screen and exit the program.
if (options.help || options.zip === undefined) {
console.log(getUsage(usageDefinition));
process.exit();
}
// Get part number for the specified device.
const partNumber =
partNumbers[options.model][options.carrier.toUpperCase()][options.color][options.storage];
// Construct the endpoint url with the options selected.
const endpoint = `https://www.apple.com/shop/retail/pickup-message?pl=true&cppart=${options.carrier}/US&parts.0=${partNumber}&location=${options.zip}`;
// Keep track of the last request time.
let lastRequestTimestamp = null;
/**
* Update program status display
*
* @param {String} str The string that will be outputed.
*/
function updateStatus() {
// If lastRequestTimestamp hasn't been update yet, do nothing.
if (lastRequestTimestamp === null) {
return;
}
// Get the amount of time elapsed since last request.
const timeDelta = Date.now() - lastRequestTimestamp;
const timeInSeconds = Math.floor(timeDelta / 1000);
process.stdout.write(`Status: Device not available. Last request made ${timeInSeconds} seconds ago\r`);
}
/**
* Parse the returned data and find stores where the device is available
*
* @param {Object} data The api response.
* @return {Array} The array of stores where the devices is available.
*/
function processResponse(data) {
// Destructure the stores object out of the body.
const { stores } = data.body;
// Filter out stores that do not have the device available.
const storesAvailable = stores.filter((store) => {
// Select the specified device partNumber.
const part = store.partsAvailability[partNumber];
// Check that the pickupDisplay property says 'available'.
const availability = part.pickupDisplay === 'available';
// Return true if the device is available or else false.
return availability;
});
// Return an array of stores where the device is available.
return storesAvailable;
}
/**
* Make a request to the endpoint and get list of stores available
*
* @return {Promise} A promise that should resolve to an array of stores available.
*/
function getStoresAvailable() {
// Update lastRequestTimestamp.
lastRequestTimestamp = Date.now();
return fetch(endpoint)
.then(stream => stream.json())
.catch(error => process.stderr.write('Fetch Error :-S', error))
.then(data => processResponse(data));
}
/**
* Output list of stores where the device is avaliable.
*
* @param {Array} storesAvailable The array of stores where the device is avaliable.
*/
function displayStoresAvailable(storesAvailable) {
// Construct the output string by reducing the storesAvailable array into a string.
const storesAvailableStr = storesAvailable.reduce(
(result, store) =>
`${result}\n${store.address.address} which is ${store.storeDistanceWithUnit} away`,
'',
);
// Output the message.
var result = `The device is currently available at ${storesAvailable.length} stores near you: \n${storesAvailableStr}`;
console.log(result);
return sns.publish({
TopicArn: '', // TODO: FIXME
Message: result
}, (err, data) => {
if(err) {
console.error('oh, shit', err);
process.exit();
} else {
console.log('message sent!', data);
process.exit();
}
});
}
/**
* The main program loop
*
* Continuously check for the device availability until it is available somewhere.
*/
async function requestLoop() {
// Fetch the storesAvailable array.
const storesAvailable = await getStoresAvailable();
if (storesAvailable.length === 0) {
// If the array is empty, update the status and after the
// specified options.delay amount of seconds, try again.
setTimeout(() => {
requestLoop();
}, options.delay * 1000);
} else {
// The device is available. Show that information to the user and exit the program.
await displayStoresAvailable(storesAvailable);
}
}
// Display program started message.
console.log('Starting program with the following settings:');
console.log(`${JSON.stringify(options, null, 2)}`);
// Update the display every second.
setInterval(() => {
updateStatus();
}, 1000);
// Kick off request recursion.
requestLoop();