mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 05:37:22 +01:00
* Optimize database access during spell casting * load less data when casting spells * Begin migrating update calls to updateOne and updateMany * Only update user objects that don’t have notification yet * fix test * fix spy * Don’t unnecessarily update user when requesting invalid guild * fix sort order for middlewares to not load user twice every request * fix tests * fix integration test * fix skill usage not always deducting mp * addtest case for blessing spell * fix healAll * fix lint * Fix error for when some spells are used outside of party * Add check to not run bulk spells in web client * fix(tags): change const to let --------- Co-authored-by: SabreCat <sabe@habitica.com>
213 lines
5.0 KiB
JavaScript
213 lines
5.0 KiB
JavaScript
import got from 'got';
|
|
import { isURL } from 'validator';
|
|
import nconf from 'nconf';
|
|
import moment from 'moment';
|
|
import logger from './logger';
|
|
import { // eslint-disable-line import/no-cycle
|
|
model as User,
|
|
} from '../models/user';
|
|
|
|
const IS_PRODUCTION = nconf.get('IS_PROD');
|
|
|
|
function sendWebhook (webhook, body, user) {
|
|
const { url, lastFailureAt } = webhook;
|
|
|
|
got.post(url, {
|
|
json: body,
|
|
timeout: 30000, // wait up to 30s before timing out
|
|
retry: 3, // retry the request up to 3 times
|
|
// Not calling .json() to parse the response because we simply ignore it
|
|
}).catch(webhookErr => {
|
|
// Log the error
|
|
logger.error(webhookErr, {
|
|
extraMessage: 'Error while sending a webhook request.',
|
|
userId: user._id,
|
|
webhookId: webhook.id,
|
|
});
|
|
|
|
let _failuresReset = false;
|
|
|
|
// Reset failures if the last one happened more than 1 month ago
|
|
const oneMonthAgo = moment().subtract(1, 'months');
|
|
if (!lastFailureAt || moment(lastFailureAt).isBefore(oneMonthAgo)) {
|
|
webhook.failures = 0;
|
|
_failuresReset = true;
|
|
}
|
|
|
|
// Increase the number of failures
|
|
webhook.failures += 1;
|
|
webhook.lastFailureAt = new Date();
|
|
|
|
// Disable a webhook with too many failures
|
|
if (webhook.failures >= 10) {
|
|
webhook.enabled = false;
|
|
webhook.failures = 0;
|
|
webhook.lastFailureAt = undefined;
|
|
_failuresReset = true;
|
|
}
|
|
|
|
const update = {
|
|
$set: {
|
|
'webhooks.$.lastFailureAt': webhook.lastFailureAt,
|
|
'webhooks.$.enabled': webhook.enabled,
|
|
},
|
|
};
|
|
|
|
if (_failuresReset) {
|
|
update.$set['webhooks.$.failures'] = webhook.failures;
|
|
} else {
|
|
update.$inc = {
|
|
'webhooks.$.failures': 1,
|
|
};
|
|
}
|
|
|
|
return User.updateOne({
|
|
_id: user._id,
|
|
'webhooks.id': webhook.id,
|
|
}, update).exec();
|
|
}).catch(err => logger.error(err)); // log errors that might have happened in the previous catch
|
|
}
|
|
|
|
function isValidWebhook (hook) {
|
|
return hook.enabled && isURL(hook.url, {
|
|
require_tld: !!IS_PRODUCTION, // eslint-disable-line camelcase
|
|
});
|
|
}
|
|
|
|
export class WebhookSender {
|
|
constructor (options = {}) {
|
|
this.type = options.type;
|
|
this.transformData = options.transformData || WebhookSender.defaultTransformData;
|
|
this.webhookFilter = options.webhookFilter || WebhookSender.defaultWebhookFilter;
|
|
}
|
|
|
|
static defaultTransformData (data) {
|
|
return data;
|
|
}
|
|
|
|
static defaultWebhookFilter () {
|
|
return true;
|
|
}
|
|
|
|
attachDefaultData (user, body) {
|
|
body.webhookType = this.type;
|
|
body.user = body.user || {};
|
|
body.user._id = user._id;
|
|
}
|
|
|
|
send (user, data) {
|
|
const { webhooks } = user;
|
|
|
|
const hooks = webhooks.filter(hook => {
|
|
if (!isValidWebhook(hook)) return false;
|
|
if (hook.type === 'globalActivity') return true;
|
|
|
|
return this.type === hook.type && this.webhookFilter(hook, data);
|
|
});
|
|
|
|
if (hooks.length < 1) {
|
|
return; // prevents running the body creation code if there are no webhooks to send
|
|
}
|
|
|
|
const body = this.transformData(data);
|
|
this.attachDefaultData(user, body);
|
|
|
|
hooks.forEach(hook => {
|
|
sendWebhook(hook, body, user);
|
|
});
|
|
}
|
|
}
|
|
|
|
export const taskScoredWebhook = new WebhookSender({
|
|
type: 'taskActivity',
|
|
webhookFilter (hook) {
|
|
const scored = hook.options && hook.options.scored;
|
|
|
|
return scored;
|
|
},
|
|
transformData (data) {
|
|
const {
|
|
user, task, direction, delta,
|
|
} = data;
|
|
|
|
const extendedStats = User.addComputedStatsToJSONObj(user.stats.toJSON(), user);
|
|
|
|
const userData = {
|
|
// _id: user._id, added automatically when the webhook is sent
|
|
_tmp: user._tmp,
|
|
stats: extendedStats,
|
|
};
|
|
|
|
const dataToSend = {
|
|
type: 'scored',
|
|
direction,
|
|
delta,
|
|
task,
|
|
user: userData,
|
|
};
|
|
|
|
return dataToSend;
|
|
},
|
|
});
|
|
|
|
export const taskActivityWebhook = new WebhookSender({
|
|
type: 'taskActivity',
|
|
webhookFilter (hook, data) {
|
|
const { type } = data;
|
|
return hook.options[type];
|
|
},
|
|
});
|
|
|
|
export const userActivityWebhook = new WebhookSender({
|
|
type: 'userActivity',
|
|
webhookFilter (hook, data) {
|
|
const { type } = data;
|
|
return hook.options[type];
|
|
},
|
|
});
|
|
|
|
export const questActivityWebhook = new WebhookSender({
|
|
type: 'questActivity',
|
|
webhookFilter (hook, data) {
|
|
const { type } = data;
|
|
return hook.options[type];
|
|
},
|
|
transformData (data) {
|
|
const { group, quest, type } = data;
|
|
|
|
const dataToSend = {
|
|
type,
|
|
group: {
|
|
id: group.id,
|
|
name: group.name,
|
|
},
|
|
quest: {
|
|
key: quest.key,
|
|
questOwner: group.quest.leader,
|
|
},
|
|
};
|
|
|
|
return dataToSend;
|
|
},
|
|
});
|
|
|
|
export const groupChatReceivedWebhook = new WebhookSender({
|
|
type: 'groupChatReceived',
|
|
webhookFilter (hook, data) {
|
|
return hook.options.groupId === data.group.id;
|
|
},
|
|
transformData (data) {
|
|
const { group, chat } = data;
|
|
|
|
const dataToSend = {
|
|
group: {
|
|
id: group.id,
|
|
name: group.name,
|
|
},
|
|
chat,
|
|
};
|
|
|
|
return dataToSend;
|
|
},
|
|
});
|