Skip to content
This repository has been archived by the owner on Mar 25, 2022. It is now read-only.

Commit

Permalink
Merge pull request #47 from trustpilot/multi-domain-support
Browse files Browse the repository at this point in the history
Multi domain support
  • Loading branch information
miklosaubert authored Oct 26, 2018
2 parents 64a6e7b + df0844c commit 9447115
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 343 deletions.
2 changes: 0 additions & 2 deletions .prettierrc.yaml

This file was deleted.

25 changes: 13 additions & 12 deletions app/config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
module.exports = {
// Minimal configuration needed
'SLACK_CLIENT_ID': process.env.SLACK_CLIENT_ID || 'YOUR_SLACK_CLIENT_ID',
'SLACK_SECRET': process.env.SLACK_SECRET || 'YOUR_SLACK_SECRET',
'VERIFICATION_TOKEN': process.env.VERIFICATION_TOKEN || 'YOUR_VERIFICATION_TOKEN',
'API_KEY': process.env.API_KEY || 'YOUR_TRUSTPILOT_API_KEY',
'API_SECRET': process.env.API_SECRET || 'YOUR_TRUSTPILOT_API_SECRET',
'BUSINESS_USER_NAME': process.env.BUSINESS_USER_NAME || 'YOUR_TRUSTPILOT_BUSINESS_USER_NAME',
'BUSINESS_USER_PASS': process.env.BUSINESS_USER_PASS || 'YOUR_TRUSTPILOT_BUSINESS_USER_PASS',
'BUSINESS_UNIT_ID': process.env.BUSINESS_UNIT_ID || 'YOUR_TRUSTPILOT_BUSINESS_UNIT_ID',
SLACK_CLIENT_ID: process.env.SLACK_CLIENT_ID || 'YOUR_SLACK_CLIENT_ID',
SLACK_SECRET: process.env.SLACK_SECRET || 'YOUR_SLACK_SECRET',
VERIFICATION_TOKEN: process.env.VERIFICATION_TOKEN || 'YOUR_VERIFICATION_TOKEN',
API_KEY: process.env.API_KEY || 'YOUR_TRUSTPILOT_API_KEY',
API_SECRET: process.env.API_SECRET || 'YOUR_TRUSTPILOT_API_SECRET',
BUSINESS_USER_NAME: process.env.BUSINESS_USER_NAME || 'YOUR_TRUSTPILOT_BUSINESS_USER_NAME',
BUSINESS_USER_PASS: process.env.BUSINESS_USER_PASS || 'YOUR_TRUSTPILOT_BUSINESS_USER_PASS',
BUSINESS_UNIT_ID: process.env.BUSINESS_UNIT_ID || 'YOUR_TRUSTPILOT_BUSINESS_UNIT_ID',

// Extra configuration (storage etc.)
'BOTKIT_STORAGE_TYPE': process.env.BOTKIT_STORAGE_TYPE || 'file',
'PORT': process.env.PORT || '7142',
'API_HOST': process.env.API_HOST || 'https://api.trustpilot.com',
'ENABLE_LOCAL_TUNNEL': process.env.ENABLE_LOCAL_TUNNEL,
BOTKIT_STORAGE_TYPE: process.env.BOTKIT_STORAGE_TYPE || 'file',
PORT: process.env.PORT || '7142',
API_HOST: process.env.API_HOST || 'https://api.trustpilot.com',
ENABLE_LOCAL_TUNNEL: process.env.ENABLE_LOCAL_TUNNEL,
ENABLE_REVIEW_QUERIES: true,
};
4 changes: 1 addition & 3 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ const run = (config, trustpilotClient, webserverExtensions) => {
// Set up a web server to expose oauth and webhook endpoints
slackapp.setupWebserver(config.PORT, () => {
// Middleware mounting and the like needs to happen before we set up the endpoints
const {
oAuthCallback,
} = webserverExtensions(slackapp, config);
const { oAuthCallback } = webserverExtensions(slackapp, config);
slackapp.createWebhookEndpoints(slackapp.webserver, config.VERIFICATION_TOKEN);
slackapp.createOauthEndpoints(slackapp.webserver, oAuthCallback);
});
Expand Down
71 changes: 40 additions & 31 deletions app/slackapp/feed-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ const getTeamFeeds = (team) => {
const feeds = team.feeds || [];
// Add in the incoming webhook settings, for backwards compatibility
if (team.incoming_webhook) {
const { businessUnitId, incoming_webhook: { channel_id: webhookChannelId } } = team;
const incomingWebhookDefaults = { businessUnitId, channelId: webhookChannelId, canReply: true };
const {
businessUnitId,
incoming_webhook: { channel_id: webhookChannelId },
} = team;
const incomingWebhookDefaults = {
businessUnitId,
channelId: webhookChannelId,
canReply: true,
};
const existingSettings = feeds.find((f) => f.channelId === webhookChannelId);
if (!existingSettings) {
return feeds.concat(incomingWebhookDefaults);
Expand All @@ -18,10 +25,12 @@ const getTeamFeeds = (team) => {
const getBusinessUnitFeedsForStarRating = (team, targetBusinessUnitId, starRating) => {
const feeds = getTeamFeeds(team);
return feeds.filter(({ businessUnitId, starFilter = 'all' }) => {
return businessUnitId === targetBusinessUnitId &&
(starFilter === 'all'
|| starFilter === 'positive' && starRating >= 4
|| starFilter === 'negative' && starRating < 4);
return (
businessUnitId === targetBusinessUnitId &&
(starFilter === 'all' ||
(starFilter === 'positive' && starRating >= 4) ||
(starFilter === 'negative' && starRating < 4))
);
});
};

Expand Down Expand Up @@ -102,28 +111,23 @@ const showFeedSettings = (bot, message) => {
channel: message.channel,
};
const dialog = bot
.createDialog('Review settings', JSON.stringify({ dialogType: 'feed_settings', sourceMessage }), 'Save')
.addSelect(
'Filter by star rating',
'starFilter',
starFilter,
[
{ label: 'None - post all reviews', value: 'all' },
{ label: 'Only post 4 and 5-star reviews', value: 'positive' },
{ label: 'Only post reviews with 1, 2 or 3 stars', value: 'negative' },
]
.createDialog(
'Review settings',
JSON.stringify({ dialogType: 'feed_settings', sourceMessage }),
'Save'
)
.addSelect(
'In-channel reply',
'replyFeature',
canReply ? 'on' : 'off',
[
{ label: 'Allow users to reply to reviews', value: 'on' },
{
label: 'Do not allow users to reply to reviews', value: 'off',
},
]
);
.addSelect('Filter by star rating', 'starFilter', starFilter, [
{ label: 'None - post all reviews', value: 'all' },
{ label: 'Only post 4 and 5-star reviews', value: 'positive' },
{ label: 'Only post reviews with 1, 2 or 3 stars', value: 'negative' },
])
.addSelect('In-channel reply', 'replyFeature', canReply ? 'on' : 'off', [
{ label: 'Allow users to reply to reviews', value: 'on' },
{
label: 'Do not allow users to reply to reviews',
value: 'off',
},
]);
bot.replyWithDialog(message, dialog.asObject(), (err, res) => {
if (err) {
console.log(err, res);
Expand All @@ -145,8 +149,9 @@ const handleNewFeedSettings = async (bot, message, slackapp) => {
canReply: replyFeature === 'on',
};
await upsertFeedSettings(team, channelId, newSettings, slackapp);
const privateChannelWarning = channelId.startsWith('G') ? '\nJust one last thing:'
+ ' this looks like a private channel, so *please /invite me* so I can post reviews here!' : '';
const privateChannelWarning = channelId.startsWith('G')
? '\nJust one last thing: this looks like a private channel, so *please /invite me* so I can post reviews here!'
: '';
bot.replyPrivateDelayedAsync = bot.replyPrivateDelayedAsync || promisify(bot.replyPrivateDelayed);
if (newSettings.canReply) {
await bot.replyPrivateDelayedAsync(
Expand Down Expand Up @@ -185,8 +190,12 @@ const handleSettingsCommand = async (bot, message) => {
user: message.user_id,
});
bot.replyPrivateDelayedAsync = bot.replyPrivateDelayedAsync || promisify(bot.replyPrivateDelayed);
if (message.channel_id.startsWith('D')) { // Direct message
await bot.replyPrivateDelayedAsync(message, 'Sorry, I can only post your reviews in a proper channel');
if (message.channel_id.startsWith('D')) {
// Direct message
await bot.replyPrivateDelayedAsync(
message,
'Sorry, I can only post your reviews in a proper channel'
);
} else if (userOk && user.is_admin) {
await showIntroMessage(message, bot);
} else {
Expand Down
61 changes: 38 additions & 23 deletions app/slackapp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ const { composeReviewMessage } = require('./lib/review-message');
const reviewReply = require('./review-reply');
const feedSettings = require('./feed-settings');

const setupAppHandlers = (slackapp, trustpilotApi) => {

const setupAppHandlers = (slackapp, trustpilotApi, enableReviewQueries) => {
const slashCommandType = (text) => {
if (/^[1-5] stars?$/i.test(text) || /^la(te)?st$/i.test(text)) {
return 'review_query';
Expand All @@ -30,7 +29,8 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
businessUnitId,
});

bot.replyPrivateDelayedAsync = bot.replyPrivateDelayedAsync || promisify(bot.replyPrivateDelayed);
bot.replyPrivateDelayedAsync =
bot.replyPrivateDelayedAsync || promisify(bot.replyPrivateDelayed);
if (lastReview) {
await bot.replyPrivateDelayedAsync(
sourceMessage,
Expand All @@ -39,7 +39,10 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
})
);
} else {
await bot.replyPrivateDelayedAsync(sourceMessage, 'Sorry, I could not find a matching review.');
await bot.replyPrivateDelayedAsync(
sourceMessage,
'Sorry, I could not find a matching review.'
);
}
return true;
};
Expand All @@ -58,17 +61,23 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
};

const handleReplyButton = async (bot, message) => {
const { canReply } = feedSettings.getChannelFeedSettingsOrDefault(bot.team_info, message.channel);
const { canReply } = feedSettings.getChannelFeedSettingsOrDefault(
bot.team_info,
message.channel
);
if (!canReply) {
bot.replyPublicDelayedAsync = bot.replyPublicDelayedAsync || promisify(bot.replyPublicDelayed);
await bot.replyPublicDelayedAsync(message, 'Sorry, it’s no longer possible to reply to reviews'
+ ' from this channel.');
bot.replyPublicDelayedAsync =
bot.replyPublicDelayedAsync || promisify(bot.replyPublicDelayed);
await bot.replyPublicDelayedAsync(
message,
'Sorry, it’s no longer possible to reply to reviews from this channel.'
);
} else {
reviewReply.showReplyDialog(bot, message);
}
};

slackapp.on('tick', () => { }); // Avoid filling the logs on each tick
slackapp.on('tick', () => {}); // Avoid filling the logs on each tick

slackapp.on('create_bot', async (bot, botConfig) => {
// We're not using the RTM API so we need to tell Botkit to start processing conversations
Expand All @@ -79,9 +88,11 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
user: botConfig.createdBy,
});
convo.say('Hi there,');
convo.say('Receive and reply to reviews directly from Slack at your convenience. ' +
'Select the private or public channel of your choice, and use the `/trustpilot settings` ' +
'or `/trustpilot feed` command to get started.');
convo.say(
'Receive and reply to reviews directly from Slack at your convenience. ' +
'Select the private or public channel of your choice, and use the `/trustpilot settings` ' +
'or `/trustpilot feed` command to get started.'
);
convo.say('Enjoy! Trustpilot');
});

Expand All @@ -92,23 +103,23 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
slackapp.on('slash_command', async (bot, message) => {
bot.replyAcknowledge();
const type = slashCommandType(message.text);
const noop = () => {};
const commandHandlers = {
'review_query': handleReviewQuery,
'feed_settings': feedSettings.handleSettingsCommand,
'test_feeds': testFeeds,
['review_query']: enableReviewQueries ? handleReviewQuery : noop,
['feed_settings']: feedSettings.handleSettingsCommand,
['test_feeds']: testFeeds,
};
await commandHandlers[type](bot, message);
return true;
});


slackapp.on('interactive_message_callback', async (bot, message) => {
bot.replyAcknowledge();
const action = message.actions[0].value;
const actionHandlers = {
'step_1_write_reply': handleReplyButton,
'open_feed_settings': feedSettings.showFeedSettings,
'delete_feed_settings': feedSettings.deleteFeedSettings(slackapp),
['step_1_write_reply']: handleReplyButton,
['open_feed_settings']: feedSettings.showFeedSettings,
['delete_feed_settings']: feedSettings.deleteFeedSettings(slackapp),
};
await actionHandlers[action](bot, message);
return true;
Expand All @@ -118,8 +129,8 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
bot.dialogOk();
const { dialogType } = JSON.parse(message.callback_id);
const dialogHandlers = {
'review_reply': reviewReply.handleReply(trustpilotApi),
'feed_settings': feedSettings.handleDialogSubmission(slackapp),
['review_reply']: reviewReply.handleReply(trustpilotApi),
['feed_settings']: feedSettings.handleDialogSubmission(slackapp),
};
await dialogHandlers[dialogType](bot, message);
return true;
Expand All @@ -135,7 +146,11 @@ const setupAppHandlers = (slackapp, trustpilotApi) => {
const bot = slackapp.spawn(team);
bot.team_info = team; // eslint-disable-line camelcase
bot.sendAsync = bot.sendAsync || promisify(bot.send);
const feeds = feedSettings.getBusinessUnitFeedsForStarRating(team, businessUnitId, review.stars);
const feeds = feedSettings.getBusinessUnitFeedsForStarRating(
team,
businessUnitId,
review.stars
);

feeds.forEach(async ({ channelId, canReply }) => {
const message = composeReviewMessage(review, { canReply });
Expand Down Expand Up @@ -172,6 +187,6 @@ module.exports = (config, trustpilotApi, storage) => {
rtm_receive_messages: false, // eslint-disable-line camelcase
scopes: ['bot', 'incoming-webhook', 'commands'],
});
setupAppHandlers(slackapp, trustpilotApi);
setupAppHandlers(slackapp, trustpilotApi, config.ENABLE_REVIEW_QUERIES);
return slackapp;
};
8 changes: 4 additions & 4 deletions app/slackapp/lib/interactive-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const fillInConfirm = ({ title, text, ...restOfConfirm }) => {
return {
title,
text,
'ok_text': 'Yes',
'dismiss_text': 'No',
['ok_text']: 'Yes',
['dismiss_text']: 'No',
...restOfConfirm,
};
};
Expand All @@ -34,8 +34,8 @@ const fillInAttachment = ({ text, actions, ...restOfAttachment }) => {
return {
text,
actions: actions ? actions.map(fillInAction) : null,
'callback_id': 'default',
'attachment_type': 'default',
['callback_id']: 'default',
['attachment_type']: 'default',
...restOfAttachment,
};
};
Expand Down
46 changes: 25 additions & 21 deletions app/slackapp/lib/review-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ const { fillInInteractiveMessage } = require('./interactive-message');
const makeReviewAttachment = (review, ...partBuilders) => {
const stars = _S.repeat('★', review.stars) + _S.repeat('✩', 5 - review.stars);
const reviewMoment = moment(review.createdAt);
const color = (review.stars >= 4) ? 'good' : (review.stars <= 2) ? 'danger' : 'warning';
const color = review.stars >= 4 ? 'good' : review.stars <= 2 ? 'danger' : 'warning';
const basicAttachment = {
'author_name': review.consumer.displayName,
'title': review.title,
'text': review.text,
'color': color,
'footer': stars,
'ts': reviewMoment.format('X'),
'fields': [{
title: 'Source',
value: review.referenceId ? `Reference number ${review.referenceId}` : 'Organic',
}],
['author_name']: review.consumer.displayName,
title: review.title,
text: review.text,
color: color,
footer: stars,
ts: reviewMoment.format('X'),
fields: [
{
title: 'Source',
value: review.referenceId ? `Reference number ${review.referenceId}` : 'Organic',
},
],
};

return partBuilders.reduce((attachment, builder) => {
Expand All @@ -25,26 +27,28 @@ const makeReviewAttachment = (review, ...partBuilders) => {
};

const actionsPartBuilder = (actionsMap) => (review) => {
const actions = [...actionsMap].filter(([, isPermitted]) => isPermitted).map(([action]) => action);
return actions.length ? {
'callback_id': review.id,
actions,
} : {};
const actions = [...actionsMap]
.filter(([, isPermitted]) => isPermitted)
.map(([action]) => action);
return actions.length
? {
['callback_id']: review.id,
actions,
}
: {};
};

const replyAction = {
'value': 'step_1_write_reply',
'text': ':writing_hand: Reply',
value: 'step_1_write_reply',
text: ':writing_hand: Reply',
};

const composeReviewMessage = (review, { canReply }) => {
const actionsMap = new Map();
actionsMap.set(replyAction, canReply);

return fillInInteractiveMessage({
'attachments': [
makeReviewAttachment(review, actionsPartBuilder(actionsMap)),
],
attachments: [makeReviewAttachment(review, actionsPartBuilder(actionsMap))],
});
};

Expand Down
Loading

0 comments on commit 9447115

Please sign in to comment.