-
Notifications
You must be signed in to change notification settings - Fork 53
/
appstorereviews.js
282 lines (234 loc) · 9.58 KB
/
appstorereviews.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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
const controller = require('./reviews');
const fs = require('fs');
var request = require('request');
require('./constants');
exports.startReview = function (config, first_run) {
if (config.regions === false){
try {
config.regions = JSON.parse(fs.readFileSync(__dirname + '/regions.json'));
} catch (err) {
config.regions = ["us"];
}
}
if (!config.regions) {
config.regions = ["us"];
}
if (!config.interval) {
config.interval = DEFAULT_INTERVAL_SECONDS
}
for (var i = 0; i < config.regions.length; i++) {
const region = config.regions[i];
// Find the app information to get a icon URL
exports.fetchAppInformation(config, region, function (globalAppInformation) {
const appInformation = Object.assign({}, globalAppInformation);
exports.fetchAppStoreReviews(config, appInformation, function (reviews) {
// If we don't have any published reviews, then treat this as a baseline fetch, we won't post any
// reviews to slack, but new ones from now will be posted
if (first_run) {
var reviewLength = reviews.length;
for (var j = 0; j < reviewLength; j++) {
var initialReview = reviews[j];
controller.markReviewAsPublished(config, initialReview);
}
if (config.dryRun && reviews.length > 0) {
// Force publish a review if we're doing a dry run
publishReview(appInformation, config, reviews[reviews.length - 1], config.dryRun);
}
}
else {
exports.handleFetchedAppStoreReviews(config, appInformation, reviews);
}
//calculate the interval with an offset, to avoid spamming the server
var interval_seconds = config.interval + (i * 10);
setInterval(function (config, appInformation) {
if (config.verbose) console.log("INFO: [" + config.appId + "] Fetching App Store reviews");
exports.fetchAppStoreReviews(config, appInformation, function (reviews) {
exports.handleFetchedAppStoreReviews(config, appInformation, reviews);
});
}, interval_seconds * 1000, config, appInformation);
});
});
}
};
var fetchAppStoreReviewsByPage = function(config, appInformation, page, callback){
const url = "https://itunes.apple.com/" + appInformation.region + "/rss/customerreviews/page="+page+"/id=" + config.appId + "/sortBy=mostRecent/json";
request(url, function (error, response, body) {
if (error) {
if (config.verbose) {
if (config.verbose) console.log("ERROR: Error fetching reviews from App Store for (" + config.appId + ") (" + appInformation.region + ")");
console.log(error)
}
callback([]);
return;
}
var rss;
try {
rss = JSON.parse(body);
} catch(e) {
console.error("Error parsing app store reviews");
console.error(e);
callback([]);
return;
}
var entries = rss.feed.entry;
if (entries == null || !entries.length > 0) {
if (config.verbose) console.log("INFO: Received no reviews from App Store for (" + config.appId + ") (" + appInformation.region + ")");
callback([]);
return;
}
if (config.verbose) console.log("INFO: Received reviews from App Store for (" + config.appId + ") (" + appInformation.region + ")");
var reviews = entries
.filter(function (review) {
return !isAppInformationEntry(review)
})
.reverse()
.map(function (review) {
return exports.parseAppStoreReview(review, config, appInformation);
});
callback(reviews)
});
};
exports.fetchAppStoreReviews = function (config, appInformation, callback) {
var page = 1;
var allReviews = [];
function pageCallback(reviews){
allReviews = allReviews.concat(reviews);
if (reviews.length > 0 && page < 10){
page++;
fetchAppStoreReviewsByPage(config, appInformation, page, pageCallback);
} else {
callback(allReviews);
}
}
fetchAppStoreReviewsByPage(config, appInformation, page, pageCallback);
};
exports.handleFetchedAppStoreReviews = function (config, appInformation, reviews) {
if (config.verbose) console.log("INFO: [" + config.appId + "(" + appInformation.region + ")] Handling fetched reviews");
for (var n = 0; n < reviews.length; n++) {
var review = reviews[n];
publishReview(appInformation, config, review, false)
}
};
exports.parseAppStoreReview = function (rssItem, config, appInformation) {
var review = {};
review.id = rssItem.id.label;
review.version = reviewAppVersion(rssItem);
review.title = rssItem.title.label;
review.appIcon = appInformation.appIcon;
review.text = rssItem.content.label;
review.rating = reviewRating(rssItem);
review.author = reviewAuthor(rssItem);
review.link = reviewLink(rssItem) || appInformation.appLink;
review.storeName = "App Store";
return review;
};
function publishReview(appInformation, config, review, force) {
if (!controller.reviewPublished(config, review) || force) {
if (config.verbose) console.log("INFO: Received new review: " + JSON.stringify(review));
var message = slackMessage(review, config, appInformation);
controller.postToSlack(message, config);
controller.markReviewAsPublished(config, review);
} else {
if (config.verbose) console.log("INFO: Review already published: " + review.text);
}
}
var reviewRating = function (review) {
return review['im:rating'] && !isNaN(review['im:rating'].label) ? parseInt(review['im:rating'].label) : -1;
};
var reviewAuthor = function (review) {
return review.author ? review.author.name.label : '';
};
var reviewLink = function (review) {
return review.author ? review.author.uri.label : '';
};
var reviewAppVersion = function (review) {
return review['im:version'] ? review['im:version'].label : '';
};
// App Store app information
exports.fetchAppInformation = function (config, region, callback) {
const url = "https://itunes.apple.com/lookup?id=" + config.appId + "&country=" + region;
const appInformation = {
appName: config.appName,
appIcon: config.appIcon,
appLink: config.appLink,
region, region
};
request(url, function (error, response, body) {
if (error) {
if (config.verbose) {
if (config.verbose) console.log("ERROR: Error fetching app data from App Store for (" + config.appId + ")");
console.log(error)
}
callback(appInformation);
return;
}
var data;
try {
data = JSON.parse(body);
} catch(e) {
console.error("Error parsing app store data");
console.error(e);
callback(appInformation);
return;
}
var entries = data.results;
if (entries == null || !entries.length > 0) {
if (config.verbose) console.log("INFO: Received no data from App Store for (" + config.appId + ")");
callback(appInformation);
return;
}
if (config.verbose) console.log("INFO: Received data from App Store for (" + config.appId + ")");
var entry = entries[0];
if (!config.appName && entry.trackCensoredName) {
appInformation.appName = entry.trackCensoredName;
}
if (!config.appIcon && entry.artworkUrl100 ) {
appInformation.appIcon = entry.artworkUrl100;
}
if (!config.appLink && entry.trackViewUrl) {
appInformation.appLink = entry.trackViewUrl;
}
callback(appInformation)
});
};
var isAppInformationEntry = function (entry) {
// App information is available in an entry with some special fields
return entry && entry['im:name'];
};
var slackMessage = function (review, config, appInformation) {
if (config.verbose) console.log("INFO: Creating message for review " + review.title);
var stars = "";
for (var i = 0; i < 5; i++) {
stars += i < review.rating ? "★" : "☆";
}
var color = review.rating >= 4 ? "good" : (review.rating >= 2 ? "warning" : "danger");
var text = "";
text += review.text + "\n";
var footer = "";
if (review.version) {
footer += " for v" + review.version;
}
if (review.link) {
footer += " - " + "<" + review.link + "|" + appInformation.appName + ", " + review.storeName + " (" + appInformation.region + ") >";
} else {
footer += " - " + appInformation.appName + ", " + review.storeName + " (" + appInformation.region + ")";
}
var title = stars;
if (review.title) {
title += " – " + review.title;
}
return {
"channel": config.channel,
"attachments": [
{
"mrkdwn_in": ["text", "pretext", "title"],
"color": color,
"author_name": review.author,
"thumb_url": config.showAppIcon ? (review.appIcon ? review.appIcon : appInformation.appIcon) : config.botIcon,
"title": title,
"text": text,
"footer": footer
}
]
};
};