From 9e137cb9cabde174bfcf2d949df580ca2cc4768c Mon Sep 17 00:00:00 2001 From: regevbr Date: Fri, 18 Jan 2019 16:30:43 +0200 Subject: [PATCH 1/4] no such file or directory, chmod '/usr/local/lib/node_modules/@trademe/reviewme/bin/reviewme.js' #22 --- README.md | 11 +++-------- appstorereviews.js | 25 ++++++++++--------------- bin/reviewme.js | 20 ++++++++++++++++++++ googleplayreviews.js | 13 +++++-------- index.js | 2 -- package-lock.json | 2 +- package.json | 2 +- reviews.js | 28 ++-------------------------- util.js | 1 - 9 files changed, 42 insertions(+), 62 deletions(-) create mode 100644 bin/reviewme.js delete mode 100644 util.js diff --git a/README.md b/README.md index 4fc59ac..e1911fe 100644 --- a/README.md +++ b/README.md @@ -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 @@ -61,10 +58,8 @@ 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 * **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` diff --git a/appstorereviews.js b/appstorereviews.js index c979913..498216e 100644 --- a/appstorereviews.js +++ b/appstorereviews.js @@ -16,24 +16,24 @@ exports.startReview = function (config, first_run) { exports.fetchAppInformation(config, config.regions[0], function (iconUrl) { for (var i = 0; i < config.regions.length; i++) { const region = config.regions[i]; - + const appInformation = {}; 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 +41,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); }); @@ -250,18 +250,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 diff --git a/bin/reviewme.js b/bin/reviewme.js new file mode 100644 index 0000000..b25344c --- /dev/null +++ b/bin/reviewme.js @@ -0,0 +1,20 @@ +#! /usr/bin/env node +var reviewme = require('../index'); +var program = require('commander'); + +var configFile; + +program + .arguments('') + .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); diff --git a/googleplayreviews.js b/googleplayreviews.js index e70042f..e5894a2 100644 --- a/googleplayreviews.js +++ b/googleplayreviews.js @@ -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 +}; diff --git a/index.js b/index.js index 62b4b98..f20003b 100644 --- a/index.js +++ b/index.js @@ -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, diff --git a/package-lock.json b/package-lock.json index 00e7995..76ee693 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@trademe/reviewme", - "version": "1.0.8", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ced1201..266fef0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trademe/reviewme", - "version": "1.1.0", + "version": "2.0.0", "description": "Google Play and App Store reviews posted to Slack", "main": "index.js", "scripts": { diff --git a/reviews.js b/reviews.js index 2d46c7d..bf8420f 100644 --- a/reviews.js +++ b/reviews.js @@ -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"; -}; diff --git a/util.js b/util.js deleted file mode 100644 index 8b13789..0000000 --- a/util.js +++ /dev/null @@ -1 +0,0 @@ - From d12e8506c3cb8d4225ce63756a4ff50bf04c6194 Mon Sep 17 00:00:00 2001 From: regevbr Date: Sat, 19 Jan 2019 12:27:38 +0200 Subject: [PATCH 2/4] no such file or directory, chmod '/usr/local/lib/node_modules/@trademe/reviewme/bin/reviewme.js' #22 --- appstorereviews.js | 151 +++++++++++++++++++++++------------------ index.js | 17 +++++ regions.json | 166 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 67 deletions(-) create mode 100644 regions.json diff --git a/appstorereviews.js b/appstorereviews.js index 498216e..b874fe8 100644 --- a/appstorereviews.js +++ b/appstorereviews.js @@ -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"]; } @@ -13,14 +21,12 @@ 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 @@ -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) { diff --git a/index.js b/index.js index f20003b..c2803dc 100644 --- a/index.js +++ b/index.js @@ -19,3 +19,20 @@ module.exports.start = function start(config) { }) } }; + +var config = { + "slackHook": "https://hooks.slack.com/services/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", + "verbose": true, + "dryRun": false, + "showAppIcon": false, + "interval":300, + "apps": [ + { + "botIcon": "https://d3j72de684fey1.cloudfront.net/resized/a450ccf60e0b0fc9aba5e1309daa9d5cffcf0f62.PjI1NngyNTY.png", + "appId": "1448299719", + regions: false, + } + ] +} + +module.exports.start(config); diff --git a/regions.json b/regions.json new file mode 100644 index 0000000..025472e --- /dev/null +++ b/regions.json @@ -0,0 +1,166 @@ +[ + "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" +] From 4e353933f8f0d94b6e89bb91725592b0acf60289 Mon Sep 17 00:00:00 2001 From: regevbr Date: Sat, 19 Jan 2019 12:28:49 +0200 Subject: [PATCH 3/4] no such file or directory, chmod '/usr/local/lib/node_modules/@trademe/reviewme/bin/reviewme.js' #22 --- .gitignore | 4 +++- index.js | 17 ----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index ffc40bf..1c675f5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/index.js b/index.js index c2803dc..f20003b 100644 --- a/index.js +++ b/index.js @@ -19,20 +19,3 @@ module.exports.start = function start(config) { }) } }; - -var config = { - "slackHook": "https://hooks.slack.com/services/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", - "verbose": true, - "dryRun": false, - "showAppIcon": false, - "interval":300, - "apps": [ - { - "botIcon": "https://d3j72de684fey1.cloudfront.net/resized/a450ccf60e0b0fc9aba5e1309daa9d5cffcf0f62.PjI1NngyNTY.png", - "appId": "1448299719", - regions: false, - } - ] -} - -module.exports.start(config); From ecb213ed50faa2fbff11becfdf0a49888a844d68 Mon Sep 17 00:00:00 2001 From: regevbr Date: Sat, 19 Jan 2019 12:29:43 +0200 Subject: [PATCH 4/4] no such file or directory, chmod '/usr/local/lib/node_modules/@trademe/reviewme/bin/reviewme.js' #22 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1911fe..9ff68fe 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ 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 +* **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 * **showAppIcon** Determines if app icon will be displayed (overrides botIcon) * **channel** Overrides the default Slack channel messages will be posted to