Skip to content

Commit

Permalink
Merge pull request #27 from PruvoNet/master
Browse files Browse the repository at this point in the history
Fix #22 and #26
  • Loading branch information
Jamie Sanson authored Mar 19, 2019
2 parents 45a67ee + ab41094 commit 98d92a5
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 130 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk

# End of https://www.gitignore.io/api/osx,node,windows,intellij
# End of https://www.gitignore.io/api/osx,node,windows,intellij

.idea
13 changes: 4 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ ReviewMe requires a config file. A simple config looks something like:
"slackHook": "https://hooks.slack.com/services/01234/5678",
"verbose": true,
"dryRun": false,
"botUsername": "ReviewMe",
"interval":300,
"apps": [
{
Expand All @@ -47,10 +46,8 @@ ReviewMe requires a config file. A simple config looks something like:
* **slackHook**: The slack hook for your Slack integration. Reviews will be posted here.
* **verbose**: When enabled, log messages will be printed to the console
* **dryRun**: When enabled, ReviewMe will post the latest app review for each app on startup. Useful for debugging
* **botUsername** The username of the Slack bot
* **botIcon** An image url to use for the bot avatar
* **botEmoji** A slack emoji to use for the bot avatar, e.g. `:apple:`
* **showAppIcon** Determines if app icon will be displayed
* **showAppIcon** Determines if app icon will be displayed (overrides botIcon)
* **channel** Overrides the default Slack channel messages will be posted to
* **interval** The interval (in seconds) to check for new reviews. Default: `300`.
* **apps** A list of apps to fetch reviews for. See App Options below
Expand All @@ -60,11 +57,9 @@ ReviewMe requires a config file. A simple config looks something like:
Note: Some options override the global configuration

* **appId** The Android app package name, or the iOS app ID.
* **regions** *iOS Only* The [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2#Current_codes) regions to fetch reviews for
* **botUsername** The username of the Slack bot
* **regions** *iOS Only* The [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2#Current_codes) regions to fetch reviews for (use `false` to include all regions)
* **botIcon** An image url to use for the bot avatar
* **botEmoji** A slack emoji to use for the bot avatar, e.g. `:apple:`
* **showAppIcon** Determines if app icon will be displayed
* **showAppIcon** Determines if app icon will be displayed (overrides botIcon)
* **channel** Overrides the default Slack channel messages will be posted to


Expand All @@ -73,7 +68,7 @@ ReviewMe requires access to the Google Play Publisher API to fetch reviews. You

* Go to the Google Play Developer Console -> Settings -> API Access
* Create a Google Play Android Developer project
* Create a Service Account
* Create a Service Account with "Service Accounts" -> "Service Account User" role
* Download the private key (`.json`)
* Supply the path to the private key in the `config.json`

Expand Down
176 changes: 94 additions & 82 deletions appstorereviews.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
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"];
}
Expand All @@ -13,42 +21,40 @@ exports.startReview = function (config, first_run) {
}

// Find the app information to get a icon URL
exports.fetchAppInformation(config, config.regions[0], function (iconUrl) {
exports.fetchAppInformation(config, function (globalAppInformation) {
for (var i = 0; i < config.regions.length; i++) {
const region = config.regions[i];
const appInformation = {};

const appInformation = Object.assign({},globalAppInformation);
appInformation.region = region;
appInformation.appName = config.appName;
appInformation.appIcon = iconUrl;


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);
});
Expand All @@ -58,48 +64,8 @@ exports.startReview = function (config, first_run) {
});
};

exports.fetchAppInformation = function (config, region, callback) {
const url = "https://itunes.apple.com/lookup?id=" + config.appId + "&country=" + region;

request(url, function (error, response, body) {
if (error) {
if (config.verbose) {
if (config.verbose) console.log("ERROR: Error fetching app information from App Store for (" + config.appId + ")");
console.log(error)
}
callback(null);
return;
}

var info;
try {
info = JSON.parse(body);
} catch(e) {
console.error("Error parsing app information");
console.error(e);

callback(null);
return;
}

var result = info.results[0];

if (result == null) {
if (config.verbose) console.log("INFO: Received no info from App Store for (" + config.appId + ")");
callback(null);
return;
}

if (config.verbose) console.log("INFO: Received info from App Store for (" + config.appId + ")");

if (config.verbose) console.log("INFO: Set icon URL (" + result.artworkUrl100 + ") for (" + config.appId + ")");

callback(result.artworkUrl100)
});
}

exports.fetchAppStoreReviews = function (config, appInformation, callback) {
const url = "https://itunes.apple.com/" + appInformation.region + "/rss/customerreviews/id=" + config.appId + "/sortBy=mostRecent/json";
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) {
Expand Down Expand Up @@ -132,21 +98,34 @@ exports.fetchAppStoreReviews = function (config, appInformation, callback) {

if (config.verbose) console.log("INFO: Received reviews from App Store for (" + config.appId + ") (" + appInformation.region + ")");

updateAppInformation(config, entries, appInformation);

var reviews = entries
.filter(function (review) {
return !isAppInformationEntry(review)
})
.reverse()
.map(function (review) {
return exports.parseAppStoreReview(review, config, appInformation);
});
.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 < 11){
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");
Expand All @@ -166,7 +145,7 @@ exports.parseAppStoreReview = function (rssItem, config, appInformation) {
review.text = rssItem.content.label;
review.rating = reviewRating(rssItem);
review.author = reviewAuthor(rssItem);
review.link = config.appLink ? config.appLink : appInformation.appLink;
review.link = reviewLink(rssItem) || appInformation.appLink;
review.storeName = "App Store";
return review;
};
Expand All @@ -190,29 +169,67 @@ 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
var updateAppInformation = function (config, entries, appInformation) {
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
exports.fetchAppInformation = function (config, callback) {
const url = "https://itunes.apple.com/lookup?id=" + config.appId;
const appInformation = {
appName: config.appName,
appIcon: config.appIcon,
appLink: config.appLink
};
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;
}

if (!isAppInformationEntry(entry)) continue;
var data;
try {
data = JSON.parse(body);
} catch(e) {
console.error("Error parsing app store data");
console.error(e);

if (!config.appName && entry['im:name']) {
appInformation.appName = entry['im:name'].label;
callback(appInformation);
return;
}

if (!config.appIcon && entry['im:image'] && entry['im:image'].length > 0) {
appInformation.appIcon = entry['im:image'][0].label;
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.appLink && entry['link']) {
appInformation.appLink = entry['link'].attributes.href;
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) {
Expand Down Expand Up @@ -250,18 +267,13 @@ var slackMessage = function (review, config, appInformation) {
}

return {
"username": config.botUsername,
"icon_url": config.botIcon,
"icon_emoji": config.botEmoji,
"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) : null,

"thumb_url": config.showAppIcon ? (review.appIcon ? review.appIcon : appInformation.appIcon) : config.botIcon,
"title": title,
"text": text,
"footer": footer
Expand Down
20 changes: 20 additions & 0 deletions bin/reviewme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /usr/bin/env node
var reviewme = require('../index');
var program = require('commander');

var configFile;

program
.arguments('<file>')
.action(function (file) {
configFile = file;
})
.parse(process.argv);

if (typeof configFile === 'undefined') {
console.error('No config file specified');
process.exit(1);
}

var config = require(configFile);
reviewme.start(config);
13 changes: 5 additions & 8 deletions googleplayreviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ exports.startReview = function (config, first_run) {
// reviews to slack, but new ones from now will be posted
if (first_run) {
var reviewLength = reviews.length;

for (var i = 0; i < reviewLength; i++) {
var initialReview = reviews[i];
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.handleFetchedGooglePlayReviews(config, appInformation, reviews);
}
}

var interval_seconds = config.interval ? config.interval : DEFAULT_INTERVAL_SECONDS;

Expand Down Expand Up @@ -179,9 +179,6 @@ var slackMessage = function (review, config, appInformation) {
}

return {
"username": config.botUsername,
"icon_url": config.botIcon,
"icon_emoji": config.botEmoji,
"channel": config.channel,
"attachments": [
{
Expand All @@ -190,7 +187,7 @@ var slackMessage = function (review, config, appInformation) {
"color": color,
"author_name": review.author,

"thumb_url": config.showAppIcon ? appInformation.appIcon : null,
"thumb_url": config.showAppIcon ? appInformation.appIcon : config.botIcon,

"title": title,

Expand All @@ -208,4 +205,4 @@ var getVersionNameForCode = function (versionCode) {
}

return "";
};
};
Loading

0 comments on commit 98d92a5

Please sign in to comment.