import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js'; import { parse, prettifyParseError } from '/imports/parser/parser.js'; import resolve, { toString } from '/imports/parser/resolve.js'; const PER_CREATURE_LOG_LIMIT = 100; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; if (Meteor.isServer) { var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; } let CreatureLogs = new Mongo.Collection('creatureLogs'); let CreatureLogSchema = new SimpleSchema({ content: { type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.logContentCount, }, 'content.$': { type: LogContentSchema, }, // The real-world date that it occured, usually sorted by date date: { type: Date, autoValue: function () { // If the date isn't set, set it to now if (!this.isSet) { return new Date(); } }, index: 1, }, creatureId: { type: String, regEx: SimpleSchema.RegEx.Id, index: 1, }, tabletopId: { type: String, regEx: SimpleSchema.RegEx.Id, index: 1, optional: true, }, creatureName: { type: String, optional: true, max: STORAGE_LIMITS.name, }, }); CreatureLogs.attachSchema(CreatureLogSchema); function removeOldLogs(creatureId) { // Find the first log that is over the limit let firstExpiredLog = CreatureLogs.find({ creatureId }, { sort: { date: -1 }, skip: PER_CREATURE_LOG_LIMIT, }); if (!firstExpiredLog) return; // Remove all logs older than the one over the limit CreatureLogs.remove({ creatureId, date: { $lte: firstExpiredLog.date }, }); } function logToMessageData(log) { let embed = { fields: [], }; log.content.forEach(field => { if (!field.name) field.name = '\u200b'; if (!field.value) field.value = '\u200b'; embed.fields.push(field); }); return { embeds: [embed] }; } function logWebhook({ log, creature }) { if (Meteor.isServer) { sendWebhookAsCreature({ creature, data: logToMessageData(log), }); } } const insertCreatureLog = new ValidatedMethod({ name: 'creatureLogs.methods.insert', mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, validate: new SimpleSchema({ log: CreatureLogSchema.omit('date'), }).validator(), run({ log }) { const creatureId = log.creatureId; const creature = Creatures.findOne(creatureId, { fields: { readers: 1, writers: 1, owner: 1, 'settings.discordWebhook': 1, name: 1, avatarPicture: 1, tabletop: 1, } }); assertEditPermission(creature, this.userId); // Build the new log let id = insertCreatureLogWork({ log, creature, method: this }) return id; }, }); const insertTabletopLog = new ValidatedMethod({ name: 'creatureLogs.methods.insertTabletopLog', mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, validate: new SimpleSchema({ log: CreatureLogSchema.omit('date'), }).validator(), run({ log }) { const tabletopId = log.tabletopId; assertUserInTabletop(tabletopId, this.userId); // Build the new log let id = insertCreatureLogWork({ log, method: this }) return id; }, }); export function insertCreatureLogWork({ log, creature, method }) { // Build the new log if (typeof log === 'string') { log = { content: [{ value: log }] }; } if (!log.content?.length) return; log.date = new Date(); if (creature) log.tabletopId = creature.tabletop; // Insert it let id = CreatureLogs.insert(log); if (Meteor.isServer) { method?.unblock(); if (creature) { removeOldLogs(creature._id); logWebhook({ log, creature }); } if (log.tabletopId) { // Todo remove old tabletop logs // Log webhook if it's different to creature webhook } } return id; } function equalIgnoringWhitespace(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return a === b; return a.replace(/\s/g, '') === b.replace(/\s/g, ''); } const logRoll = new ValidatedMethod({ name: 'creatureLogs.methods.logForCreature', mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, validate: new SimpleSchema({ roll: { type: String, }, creatureId: { type: String, regEx: SimpleSchema.RegEx.Id, }, }).validator(), run({ roll, creatureId }) { const creature = Creatures.findOne(creatureId, { fields: { readers: 1, writers: 1, owner: 1, 'settings.discordWebhook': 1, name: 1, avatarPicture: 1, } }); assertEditPermission(creature, this.userId); const variables = CreatureVariables.findOne({ _creatureId: creatureId }); let logContent = [] let parsedResult = undefined; try { parsedResult = parse(roll); } catch (e) { let error = prettifyParseError(e); logContent.push({ name: 'Parse Error', value: error }); } if (parsedResult) try { let { result: compiled, context } = resolve('compile', parsedResult, variables); const compiledString = toString(compiled); if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({ value: roll }); logContent.push({ value: compiledString }); let { result: rolled } = resolve('roll', compiled, variables, context); let rolledString = toString(rolled); if (rolledString !== compiledString) logContent.push({ value: rolledString }); let { result } = resolve('reduce', rolled, variables, context); let resultString = toString(result); if (resultString !== rolledString) logContent.push({ value: resultString }); } catch (e) { console.error(e); logContent = [{ name: 'Calculation error' }]; } const log = { content: logContent, creatureId, date: new Date(), }; let id = insertCreatureLogWork({ log, creature, method: this }); return id; }, }); export default CreatureLogs; export { CreatureLogSchema, insertCreatureLog, logRoll, insertTabletopLog, PER_CREATURE_LOG_LIMIT };