Unverified Commit 98d92a50 authored by Jamie Sanson's avatar Jamie Sanson Committed by GitHub
Browse files

Merge pull request #27 from PruvoNet/master

Fix #22 and #26
Showing with 295 additions and 130 deletions
+295 -130
......@@ -171,4 +171,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/osx,node,windows,intellij
\ No newline at end of file
# End of https://www.gitignore.io/api/osx,node,windows,intellij
.idea
......@@ -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": [
{
......@@ -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
......@@ -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
......@@ -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`
......
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"];
}
......@@ -13,27 +21,25 @@ 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);
......@@ -41,14 +47,14 @@ exports.startReview = function (config, first_run) {
}
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);
});
......@@ -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) {
......@@ -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");
......@@ -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;
};
......@@ -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) {
......@@ -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
......
#! /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);
......@@ -21,12 +21,12 @@ 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);
......@@ -34,7 +34,7 @@ exports.startReview = function (config, first_run) {
}
else {
exports.handleFetchedGooglePlayReviews(config, appInformation, reviews);
}
}
var interval_seconds = config.interval ? config.interval : DEFAULT_INTERVAL_SECONDS;
......@@ -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": [
{
......@@ -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,
......@@ -208,4 +205,4 @@ var getVersionNameForCode = function (versionCode) {
}
return "";
};
\ No newline at end of file
};
......@@ -9,9 +9,7 @@ module.exports.start = function start(config) {
verbose: config.verbose,
dryRun: config.dryRun,
interval: config.interval,
botUsername: app.botUsername || config.botUsername,
botIcon: app.botIcon || config.botIcon,
botEmoji: app.botEmoji || config.botEmoji,
showAppIcon: app.showAppIcon || config.showAppIcon,
channel: app.channel || config.channel,
publisherKey: app.publisherKey,
......
{
"name": "@trademe/reviewme",
"version": "1.1.2",
"version": "2.0.0",
"description": "Google Play and App Store reviews posted to Slack",
"main": "index.js",
"scripts": {
......
[
"cl",
"us",
"bo",
"bh",
"az",
"fm",
"dz",
"bd",
"co",
"bz",
"at",
"mz",
"ai",
"au",
"cz",
"ca",
"kn",
"bf",
"by",
"al",
"pa",
"fr",
"ie",
"bm",
"mu",
"ms",
"ne",
"bb",
"bw",
"ec",
"uy",
"sb",
"pt",
"ke",
"si",
"bg",
"sa",
"ag",
"cv",
"se",
"bj",
"mt",
"td",
"ye",
"cm",
"vn",
"lr",
"bs",
"is",
"il",
"ky",
"sl",
"no",
"hr",
"kh",
"ar",
"vc",
"ug",
"py",
"kg",
"nl",
"mx",
"tt",
"lt",
"cg",
"bt",
"my",
"jp",
"gy",
"ci",
"ph",
"sg",
"tz",
"na",
"lb",
"ly",
"ni",
"qa",
"pg",
"lv",
"kz",
"dk",
"la",
"bn",
"gh",
"tr",
"th",
"be",
"mg",
"uz",
"cy",
"md",
"ve",
"ee",
"kr",
"ro",
"mm",
"mv",
"ch",
"ao",
"gd",
"am",
"tj",
"de",
"mk",
"cn",
"hn",
"sv",
"mn",
"br",
"do",
"zw",
"id",
"sk",
"st",
"np",
"pl",
"vg",
"gb",
"gr",
"pk",
"hu",
"nz",
"dm",
"mo",
"et",
"tn",
"fi",
"sn",
"in",
"tc",
"gt",
"lk",
"jo",
"it",
"ru",
"fj",
"za",
"rs",
"kw",
"sr",
"ae",
"cr",
"mw",
"ml",
"gw",
"sc",
"li",
"eg",
"lu",
"sz",
"jm",
"es",
"ps",
"hk",
"ng",
"pe",
"ua",
"pw",
"lc",
"tm",
"gm",
"om",
"tw"
]
......@@ -42,12 +42,12 @@ exports.markReviewAsPublished = function (config, review) {
if (config.verbose) {
console.log("INFO: Checking if we need to prune published reviews have (" + published_reviews[config.appId].length + ") limit (" + REVIEWS_LIMIT + ")");
}
if (published_reviews[config.appId].length >= REVIEWS_LIMIT) {
if (published_reviews[config.appId].length >= REVIEWS_LIMIT) {
published_reviews[config.appId] = published_reviews[config.appId].slice(0, REVIEWS_LIMIT);
}
published_reviews[config.appId].unshift(review.id);
if (config.verbose) {
console.log("INFO: Review marked as published: " + JSON.stringify(published_reviews[config.appId]));
}
......@@ -68,25 +68,6 @@ exports.resetPublishedReviews = function () {
return published_reviews = {};
};
exports.welcomeMessage = function (config, appInformation) {
var storeName = appStoreName(config);
var appName = config.appName ? config.appName : (appInformation.appName ? appInformation.appName : config.appId);
return {
"username": config.botUsername,
"icon_url": config.botIcon,
"channel": config.channel,
"attachments": [
{
"mrkdwn_in": ["pretext", "author_name"],
"fallback": "This channel will now receive " + storeName + " reviews for " + appName,
"pretext": "This channel will now receive " + storeName + " reviews for ",
"author_name": appName,
"author_icon": config.appIcon ? config.appIcon : appInformation.appIcon
}
]
}
};
exports.postToSlack = function (message, config) {
var messageJSON = JSON.stringify(message);
if (config.verbose) {
......@@ -102,8 +83,3 @@ exports.postToSlack = function (message, config) {
body: messageJSON
});
};
var appStoreName = function (config) {
return config.store === REVIEWS_STORES.APP_STORE ? "App Store" : "Google Play";
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment