Webhooks v2 (and other fixes) (#10265)
* begin implementing global webhooks * add checklist item scored webhook * add pet hatched and mount raised webhooks (no tests) * fix typo * add lvl up webhooks, remove corrupt notifications and reorganize pre-save hook * fix typo * add some tests, globalActivity webhook * fix bug in global activiy webhook and add more tests * add tests and fix typo for petHatched and mountRaised webhooks * fix errors and add tests for level up webhook * wip: add default data to all webhooks, change signature for WebhookSender.send (missing tests) * remove unused code * fix unit tests * fix chat webhooks * remove console * fix lint * add and fix webhook tests * add questStarted webhook and questActivity type * add unit tests * add finial tests and features
This commit is contained in:
@@ -176,7 +176,7 @@ api.createUserTasks = {
|
||||
});
|
||||
}
|
||||
|
||||
taskActivityWebhook.send(user.webhooks, {
|
||||
taskActivityWebhook.send(user, {
|
||||
type: 'created',
|
||||
task,
|
||||
});
|
||||
@@ -502,7 +502,7 @@ api.updateTask = {
|
||||
} else if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask);
|
||||
} else {
|
||||
taskActivityWebhook.send(user.webhooks, {
|
||||
taskActivityWebhook.send(user, {
|
||||
type: 'updated',
|
||||
task: savedTask,
|
||||
});
|
||||
@@ -654,7 +654,7 @@ api.scoreTask = {
|
||||
let resJsonData = _.assign({delta, _tmp: user._tmp}, userStats);
|
||||
res.respond(200, resJsonData);
|
||||
|
||||
taskScoredWebhook.send(user.webhooks, {
|
||||
taskScoredWebhook.send(user, {
|
||||
task,
|
||||
direction,
|
||||
delta,
|
||||
@@ -860,6 +860,12 @@ api.scoreCheckListItem = {
|
||||
let savedTask = await task.save();
|
||||
|
||||
res.respond(200, savedTask);
|
||||
|
||||
taskActivityWebhook.send(user, {
|
||||
type: 'checklistScored',
|
||||
task: savedTask,
|
||||
item,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1326,7 +1332,7 @@ api.deleteTask = {
|
||||
if (challenge) {
|
||||
challenge.removeTask(task);
|
||||
} else {
|
||||
taskActivityWebhook.send(user.webhooks, {
|
||||
taskActivityWebhook.send(user, {
|
||||
type: 'deleted',
|
||||
task,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
import * as Tasks from '../../models/task';
|
||||
import _ from 'lodash';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import {
|
||||
userActivityWebhook,
|
||||
} from '../../libs/webhook';
|
||||
import {
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
@@ -906,8 +909,19 @@ api.hatch = {
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let hatchRes = common.ops.hatch(user, req);
|
||||
|
||||
await user.save();
|
||||
|
||||
res.respond(200, ...hatchRes);
|
||||
|
||||
// Send webhook
|
||||
const petKey = `${req.params.egg}-${req.params.hatchingPotion}`;
|
||||
|
||||
userActivityWebhook.send(user, {
|
||||
type: 'petHatched',
|
||||
pet: petKey,
|
||||
message: hatchRes[1],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -982,8 +996,21 @@ api.feed = {
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let feedRes = common.ops.feed(user, req);
|
||||
|
||||
await user.save();
|
||||
|
||||
res.respond(200, ...feedRes);
|
||||
|
||||
// Send webhook
|
||||
const petValue = feedRes[0];
|
||||
|
||||
if (petValue === -1) { // evolved to mount
|
||||
userActivityWebhook.send(user, {
|
||||
type: 'mountRaised',
|
||||
pet: req.params.pet,
|
||||
message: feedRes[1],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ let api = {};
|
||||
* @apiParam (Body) {String} url The webhook's URL
|
||||
* @apiParam (Body) {String} [label] A label to remind you what this webhook does
|
||||
* @apiParam (Body) {Boolean} [enabled=true] If the webhook should be enabled
|
||||
* @apiParam (Body) {Sring="taskActivity","groupChatReceived"} [type="taskActivity"] The webhook's type.
|
||||
* @apiParam (Body) {Sring="taskActivity","groupChatReceived","userActivity"} [type="taskActivity"] The webhook's type.
|
||||
* @apiParam (Body) {Object} [options] The webhook's options. Wil differ depending on type. Required for `groupChatReceived` type. If a webhook supports options, the default values are displayed in the examples below
|
||||
* @apiParamExample {json} Task Activity Example
|
||||
* {
|
||||
|
||||
@@ -33,11 +33,20 @@ export class WebhookSender {
|
||||
return true;
|
||||
}
|
||||
|
||||
send (webhooks, data) {
|
||||
attachDefaultData (user, body) {
|
||||
body.webhookType = this.type;
|
||||
body.user = body.user || {};
|
||||
body.user._id = user._id;
|
||||
}
|
||||
|
||||
send (user, data) {
|
||||
const webhooks = user.webhooks;
|
||||
|
||||
let hooks = webhooks.filter((hook) => {
|
||||
return isValidWebhook(hook) &&
|
||||
this.type === hook.type &&
|
||||
this.webhookFilter(hook, data);
|
||||
if (!isValidWebhook(hook)) return false;
|
||||
if (hook.type === 'globalActivity') return true;
|
||||
|
||||
return this.type === hook.type && this.webhookFilter(hook, data);
|
||||
});
|
||||
|
||||
if (hooks.length < 1) {
|
||||
@@ -45,6 +54,7 @@ export class WebhookSender {
|
||||
}
|
||||
|
||||
let body = this.transformData(data);
|
||||
this.attachDefaultData(user, body);
|
||||
|
||||
hooks.forEach((hook) => {
|
||||
sendWebhook(hook.url, body);
|
||||
@@ -65,7 +75,7 @@ export let taskScoredWebhook = new WebhookSender({
|
||||
let extendedStats = user.addComputedStatsToJSONObj(user.stats.toJSON());
|
||||
|
||||
let userData = {
|
||||
_id: user._id,
|
||||
// _id: user._id, added automatically when the webhook is sent
|
||||
_tmp: user._tmp,
|
||||
stats: extendedStats,
|
||||
};
|
||||
@@ -90,6 +100,38 @@ export let taskActivityWebhook = new WebhookSender({
|
||||
},
|
||||
});
|
||||
|
||||
export let userActivityWebhook = new WebhookSender({
|
||||
type: 'userActivity',
|
||||
webhookFilter (hook, data) {
|
||||
let { type } = data;
|
||||
return hook.options[type];
|
||||
},
|
||||
});
|
||||
|
||||
export let questActivityWebhook = new WebhookSender({
|
||||
type: 'questActivity',
|
||||
webhookFilter (hook, data) {
|
||||
let { type } = data;
|
||||
return hook.options[type];
|
||||
},
|
||||
transformData (data) {
|
||||
let { group, quest, type } = data;
|
||||
|
||||
let dataToSend = {
|
||||
type,
|
||||
group: {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
},
|
||||
quest: {
|
||||
key: quest.key,
|
||||
},
|
||||
};
|
||||
|
||||
return dataToSend;
|
||||
},
|
||||
});
|
||||
|
||||
export let groupChatReceivedWebhook = new WebhookSender({
|
||||
type: 'groupChatReceived',
|
||||
webhookFilter (hook, data) {
|
||||
|
||||
@@ -12,7 +12,10 @@ import * as Tasks from './task';
|
||||
import validator from 'validator';
|
||||
import { removeFromArray } from '../libs/collectionManipulators';
|
||||
import payments from '../libs/payments/payments';
|
||||
import { groupChatReceivedWebhook } from '../libs/webhook';
|
||||
import {
|
||||
groupChatReceivedWebhook,
|
||||
questActivityWebhook,
|
||||
} from '../libs/webhook';
|
||||
import {
|
||||
InternalServerError,
|
||||
BadRequest,
|
||||
@@ -648,20 +651,24 @@ schema.methods.startQuest = async function startQuest (user) {
|
||||
removeFromArray(nonUserQuestMembers, user._id);
|
||||
|
||||
// remove any users from quest.members who aren't in the party
|
||||
let partyId = this._id;
|
||||
let questMembers = this.quest.members;
|
||||
await Promise.all(Object.keys(this.quest.members).map(memberId => {
|
||||
return User.findOne({_id: memberId, 'party._id': partyId})
|
||||
.select('_id')
|
||||
.lean()
|
||||
.exec()
|
||||
.then((member) => {
|
||||
if (!member) {
|
||||
delete questMembers[memberId];
|
||||
// and get the data necessary to send webhooks
|
||||
const members = [];
|
||||
|
||||
await User.find({
|
||||
_id: {$in: Object.keys(this.quest.members)},
|
||||
})
|
||||
.select('party.quest party._id items.quests auth preferences.emailNotifications preferences.pushNotifications pushDevices profile.name webhooks')
|
||||
.lean()
|
||||
.exec()
|
||||
.then(partyMembers => {
|
||||
partyMembers.forEach(member => {
|
||||
if (!member.party || member.party._id !== this._id) {
|
||||
delete this.quest.members[member._id];
|
||||
} else {
|
||||
members.push(member);
|
||||
}
|
||||
return;
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
if (userIsParticipating) {
|
||||
user.party.quest.key = this.quest.key;
|
||||
@@ -670,20 +677,23 @@ schema.methods.startQuest = async function startQuest (user) {
|
||||
user.markModified('party.quest');
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Remove the quest from the quest leader items (if they are the current user)
|
||||
if (this.quest.leader === user._id) {
|
||||
user.items.quests[this.quest.key] -= 1;
|
||||
user.markModified('items.quests');
|
||||
promises.push(user.save());
|
||||
} else { // another user is starting the quest, update the leader separately
|
||||
await User.update({_id: this.quest.leader}, {
|
||||
promises.push(User.update({_id: this.quest.leader}, {
|
||||
$inc: {
|
||||
[`items.quests.${this.quest.key}`]: -1,
|
||||
},
|
||||
}).exec();
|
||||
}).exec());
|
||||
}
|
||||
|
||||
// update the remaining users
|
||||
await User.update({
|
||||
promises.push(User.update({
|
||||
_id: { $in: nonUserQuestMembers },
|
||||
}, {
|
||||
$set: {
|
||||
@@ -691,7 +701,9 @@ schema.methods.startQuest = async function startQuest (user) {
|
||||
'party.quest.progress.down': 0,
|
||||
'party.quest.completed': null,
|
||||
},
|
||||
}, { multi: true }).exec();
|
||||
}, { multi: true }).exec());
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// update the users who are not participating
|
||||
// Do not block updates
|
||||
@@ -703,38 +715,45 @@ schema.methods.startQuest = async function startQuest (user) {
|
||||
},
|
||||
}, { multi: true }).exec();
|
||||
|
||||
// send notifications in the background without blocking
|
||||
User.find(
|
||||
{ _id: { $in: nonUserQuestMembers } },
|
||||
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications preferences.pushNotifications pushDevices profile.name'
|
||||
).exec().then((membersToNotify) => {
|
||||
let membersToEmail = _.filter(membersToNotify, (member) => {
|
||||
// send push notifications and filter users that disabled emails
|
||||
return member.preferences.emailNotifications.questStarted !== false &&
|
||||
member._id !== user._id;
|
||||
});
|
||||
sendTxnEmail(membersToEmail, 'quest-started', [
|
||||
{ name: 'PARTY_URL', content: '/party' },
|
||||
]);
|
||||
let membersToPush = _.filter(membersToNotify, (member) => {
|
||||
// send push notifications and filter users that disabled emails
|
||||
return member.preferences.pushNotifications.questStarted !== false &&
|
||||
member._id !== user._id;
|
||||
});
|
||||
_.each(membersToPush, (member) => {
|
||||
sendPushNotification(member,
|
||||
{
|
||||
title: quest.text(),
|
||||
message: `${shared.i18n.t('questStarted')}: ${quest.text()}`,
|
||||
identifier: 'questStarted',
|
||||
});
|
||||
});
|
||||
});
|
||||
const newMessage = this.sendChat(`\`Your quest, ${quest.text('en')}, has started.\``, null, {
|
||||
participatingMembers: this.getParticipatingQuestMembers().join(', '),
|
||||
});
|
||||
|
||||
await newMessage.save();
|
||||
|
||||
const membersToEmail = [];
|
||||
const pushTitle = quest.text();
|
||||
const pushMessage = `${shared.i18n.t('questStarted')}: ${quest.text()}`;
|
||||
|
||||
// send notifications and webhooks in the background without blocking
|
||||
members.forEach(member => {
|
||||
if (member._id !== user._id) {
|
||||
// send push notifications and filter users that disabled emails
|
||||
if (member.preferences.emailNotifications.questStarted !== false) {
|
||||
membersToEmail.push(member);
|
||||
}
|
||||
|
||||
// send push notifications and filter users that disabled emails
|
||||
if (member.preferences.pushNotifications.questStarted !== false) {
|
||||
sendPushNotification(member, {
|
||||
title: pushTitle,
|
||||
message: pushMessage,
|
||||
identifier: 'questStarted',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send webhooks
|
||||
questActivityWebhook.send(member, {
|
||||
type: 'questStarted',
|
||||
group: this,
|
||||
quest,
|
||||
});
|
||||
});
|
||||
|
||||
// Send emails in bulk
|
||||
sendTxnEmail(membersToEmail, 'quest-started', [
|
||||
{ name: 'PARTY_URL', content: '/party' },
|
||||
]);
|
||||
};
|
||||
|
||||
schema.methods.sendGroupChatReceivedWebhooks = function sendGroupChatReceivedWebhooks (chat) {
|
||||
@@ -755,8 +774,7 @@ schema.methods.sendGroupChatReceivedWebhooks = function sendGroupChatReceivedWeb
|
||||
|
||||
User.find(query).select({webhooks: 1}).lean().exec().then((users) => {
|
||||
users.forEach((user) => {
|
||||
let { webhooks } = user;
|
||||
groupChatReceivedWebhook.send(webhooks, {
|
||||
groupChatReceivedWebhook.send(user, {
|
||||
group: this,
|
||||
chat,
|
||||
});
|
||||
@@ -907,6 +925,31 @@ schema.methods.finishQuest = async function finishQuest (quest) {
|
||||
}));
|
||||
}
|
||||
|
||||
// Send webhooks in background
|
||||
// @TODO move the find users part to a worker as well, not just the http request
|
||||
User.find({
|
||||
_id: {$in: participants},
|
||||
webhooks: {
|
||||
$elemMatch: {
|
||||
type: 'questActivity',
|
||||
'options.questFinished': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.select('_id webhooks')
|
||||
.lean()
|
||||
.exec()
|
||||
.then(participantsWithWebhook => {
|
||||
participantsWithWebhook.forEach(participantWithWebhook => {
|
||||
// Send webhooks
|
||||
questActivityWebhook.send(participantWithWebhook, {
|
||||
type: 'questFinished',
|
||||
group: this,
|
||||
quest,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import * as Tasks from '../task';
|
||||
import {
|
||||
model as UserNotification,
|
||||
} from '../userNotification';
|
||||
import {
|
||||
userActivityWebhook,
|
||||
} from '../../libs/webhook';
|
||||
|
||||
import schema from './schema';
|
||||
|
||||
@@ -241,41 +244,73 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
// this.items.pets['JackOLantern-Base'] = 5;
|
||||
}
|
||||
|
||||
// Manage unallocated stats points notifications
|
||||
if (this.isDirectSelected('stats') && this.isDirectSelected('notifications') && this.isDirectSelected('flags') && this.isDirectSelected('preferences')) {
|
||||
// Filter notifications, remove unvalid and not necessary, handle the ones that have special requirements
|
||||
if ( // Make sure all the data is loaded
|
||||
this.isDirectSelected('notifications') &&
|
||||
this.isDirectSelected('webhooks') &&
|
||||
this.isDirectSelected('stats') &&
|
||||
this.isDirectSelected('flags') &&
|
||||
this.isDirectSelected('preferences')
|
||||
) {
|
||||
const lvlUpNotifications = [];
|
||||
const unallocatedPointsNotifications = [];
|
||||
|
||||
this.notifications = this.notifications.filter(notification => {
|
||||
// Remove corrupt notifications
|
||||
if (!notification || !notification.type) return false;
|
||||
|
||||
// Remove level up notifications, as they're only used to send webhooks
|
||||
// Sometimes there can be more than 1 notification
|
||||
if (notification && notification.type === 'LEVELED_UP') {
|
||||
lvlUpNotifications.push(notification);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove all unsallocated stats points
|
||||
if (notification && notification.type === 'UNALLOCATED_STATS_POINTS') {
|
||||
unallocatedPointsNotifications.push(notification);
|
||||
return false;
|
||||
}
|
||||
// Keep all the others
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Send lvl up notifications
|
||||
if (lvlUpNotifications.length > 0) {
|
||||
const firstLvlNotification = lvlUpNotifications[0];
|
||||
const lastLvlNotification = lvlUpNotifications[lvlUpNotifications.length - 1];
|
||||
|
||||
const initialLvl = firstLvlNotification.data.initialLvl;
|
||||
const finalLvl = lastLvlNotification.data.newLvl;
|
||||
|
||||
// Delayed so we don't block the user saving
|
||||
setTimeout(() => {
|
||||
userActivityWebhook.send(this, {
|
||||
type: 'leveledUp',
|
||||
initialLvl,
|
||||
finalLvl,
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Handle unallocated stats points notifications (keep only one and up to date)
|
||||
const pointsToAllocate = this.stats.points;
|
||||
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
|
||||
|
||||
// Sometimes there can be more than 1 notification
|
||||
const existingNotifications = this.notifications.filter(notification => {
|
||||
return notification && notification.type === 'UNALLOCATED_STATS_POINTS';
|
||||
});
|
||||
|
||||
const existingNotificationsLength = existingNotifications.length;
|
||||
// Take the most recent notification
|
||||
const lastExistingNotification = existingNotificationsLength > 0 ? existingNotifications[existingNotificationsLength - 1] : null;
|
||||
const lastExistingNotification = unallocatedPointsNotifications[unallocatedPointsNotifications.length - 1];
|
||||
|
||||
// Decide if it's outdated or not
|
||||
const outdatedNotification = !lastExistingNotification || lastExistingNotification.data.points !== pointsToAllocate;
|
||||
|
||||
// If the notification is outdated, remove all the existing notifications, otherwise all of them except the last
|
||||
let notificationsToRemove = outdatedNotification ? existingNotificationsLength : existingNotificationsLength - 1;
|
||||
|
||||
// If there are points to allocate and the notification is outdated, add a new notifications
|
||||
if (pointsToAllocate > 0 && !classNotEnabled && outdatedNotification) {
|
||||
this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate });
|
||||
}
|
||||
|
||||
// Remove the outdated notifications
|
||||
if (notificationsToRemove > 0) {
|
||||
let notificationsRemoved = 0;
|
||||
|
||||
this.notifications = this.notifications.filter(notification => {
|
||||
if (notification && notification.type !== 'UNALLOCATED_STATS_POINTS') return true;
|
||||
if (notificationsRemoved === notificationsToRemove) return true;
|
||||
|
||||
notificationsRemoved++;
|
||||
return false;
|
||||
});
|
||||
if (pointsToAllocate > 0 && !classNotEnabled) {
|
||||
if (outdatedNotification) {
|
||||
this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate });
|
||||
} else { // otherwise add back the last one
|
||||
this.notifications.push(lastExistingNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const NOTIFICATION_TYPES = [
|
||||
'NEW_INBOX_MESSAGE',
|
||||
'NEW_STUFF',
|
||||
'NEW_CHAT_MESSAGE',
|
||||
'LEVELED_UP',
|
||||
];
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
@@ -14,9 +14,21 @@ const TASK_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
checklistScored: false,
|
||||
scored: true,
|
||||
});
|
||||
|
||||
const USER_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||
petHatched: false,
|
||||
mountRaised: false,
|
||||
leveledUp: false,
|
||||
});
|
||||
|
||||
const QUEST_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||
questStarted: false,
|
||||
questFinished: false,
|
||||
});
|
||||
|
||||
export let schema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
@@ -27,7 +39,11 @@ export let schema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['taskActivity', 'groupChatReceived'],
|
||||
enum: [
|
||||
'globalActivity', // global webhooks send a request for every type of event
|
||||
'taskActivity', 'groupChatReceived',
|
||||
'userActivity', 'questActivity',
|
||||
],
|
||||
default: 'taskActivity',
|
||||
},
|
||||
label: {
|
||||
@@ -67,7 +83,7 @@ schema.plugin(baseModel, {
|
||||
schema.methods.formatOptions = function formatOptions (res) {
|
||||
if (this.type === 'taskActivity') {
|
||||
_.defaults(this.options, TASK_ACTIVITY_DEFAULT_OPTIONS);
|
||||
this.options = _.pick(this.options, 'created', 'updated', 'deleted', 'scored');
|
||||
this.options = _.pick(this.options, Object.keys(TASK_ACTIVITY_DEFAULT_OPTIONS));
|
||||
|
||||
let invalidOption = Object.keys(this.options)
|
||||
.find(option => typeof this.options[option] !== 'boolean');
|
||||
@@ -81,6 +97,29 @@ schema.methods.formatOptions = function formatOptions (res) {
|
||||
if (!validator.isUUID(String(this.options.groupId))) {
|
||||
throw new BadRequest(res.t('groupIdRequired'));
|
||||
}
|
||||
} else if (this.type === 'userActivity') {
|
||||
_.defaults(this.options, USER_ACTIVITY_DEFAULT_OPTIONS);
|
||||
this.options = _.pick(this.options, Object.keys(USER_ACTIVITY_DEFAULT_OPTIONS));
|
||||
|
||||
let invalidOption = Object.keys(this.options)
|
||||
.find(option => typeof this.options[option] !== 'boolean');
|
||||
|
||||
if (invalidOption) {
|
||||
throw new BadRequest(res.t('webhookBooleanOption', { option: invalidOption }));
|
||||
}
|
||||
} else if (this.type === 'questActivity') {
|
||||
_.defaults(this.options, QUEST_ACTIVITY_DEFAULT_OPTIONS);
|
||||
this.options = _.pick(this.options, Object.keys(QUEST_ACTIVITY_DEFAULT_OPTIONS));
|
||||
|
||||
let invalidOption = Object.keys(this.options)
|
||||
.find(option => typeof this.options[option] !== 'boolean');
|
||||
|
||||
if (invalidOption) {
|
||||
throw new BadRequest(res.t('webhookBooleanOption', { option: invalidOption }));
|
||||
}
|
||||
} else {
|
||||
// Discard all options
|
||||
this.options = {};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user