Merge branch 'version-2-dev' into version-2-tabletop

This commit is contained in:
Stefan Zermatten
2022-04-23 15:18:03 +02:00
33 changed files with 727 additions and 156 deletions

View File

@@ -9,6 +9,7 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
export function getArchiveObj(creatureId){
// Build the archive document
@@ -31,7 +32,7 @@ export function getArchiveObj(creatureId){
return archiveCreature;
}
export function archiveCreature(creatureId){
export function archiveCreature(creatureId, userId){
const archive = getArchiveObj(creatureId);
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
ArchiveCreatureFiles.write(buffer, {
@@ -43,11 +44,12 @@ export function archiveCreature(creatureId){
creatureId: archive.creature._id,
creatureName: archive.creature.name,
},
}, (error) => {
}, (error, file) => {
if (error){
throw error;
} else {
removeCreatureWork(creatureId);
incrementFileStorageUsed(userId, file.size);
}
}, true);
}
@@ -68,7 +70,7 @@ const archiveCreatureToFile = new ValidatedMethod({
async run({creatureId}) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId);
archiveCreature(creatureId, this.userId);
} else {
removeCreatureWork(creatureId);
}

View File

@@ -2,3 +2,4 @@
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
import '/imports/api/creature/archive/methods/restoreCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';

View File

@@ -0,0 +1,47 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
const removeArchiveCreature = new ValidatedMethod({
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
async run({ fileId }) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
if (!file) {
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId) {
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
},
});
export default removeArchiveCreature;

View File

@@ -8,6 +8,9 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
let migrateArchive;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
@@ -69,13 +72,18 @@ const restoreCreaturefromFile = new ValidatedMethod({
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
assertHasCharactersSlots(this.userId);
if (Meteor.isServer){
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive);
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({_id: fileId});
ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
},
});

View File

@@ -0,0 +1,22 @@
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function assertHasCharactersSlots(userId) {
if (characterSlotsRemaining(userId) <= 0) {
throw new Meteor.Error('characterSlotLimit',
'No character slots left')
}
}
export function characterSlotsRemaining(userId) {
let tier = getUserTier(userId);
const currentCharacterCount = Creatures.find({
owner: userId,
}, {
fields: { _id: 1 },
}).count();
if (tier.characterSlots === -1) {
return Number.POSITIVE_INFINITY;
}
return tier.characterSlots - currentCharacterCount;
}

View File

@@ -2,9 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
const insertCreature = new ValidatedMethod({
@@ -23,21 +23,8 @@ const insertCreature = new ValidatedMethod({
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
}
assertHasCharactersSlots(this.userId);
// Create the creature document
let creatureId = Creatures.insert({

View File

@@ -0,0 +1,18 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
const UserImages = createS3FilesCollection({
collectionName: 'userImages',
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
onBeforeUpload(file) {
// Allow upload files under 10MB
if (file.size > 10485760) {
return 'Please upload with size equal or less than 10MB';
}
// Allow common image extensions
if (/gif|png|jpe?g|webp/i.test(file.extension || '')) {
return 'Please upload an image file only';
}
}
});
export default UserImages;

View File

@@ -213,9 +213,6 @@ if (Meteor.isServer && Meteor.settings.useS3) {
return collection;
}
} else {
if (Meteor.isServer){
// console.log('No S3 details specified, files will be stored in the local filesystem');
}
createS3FilesCollection = function({
collectionName,
storagePath,

View File

@@ -4,6 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/methods/deleteMyAccount.js';
import '/imports/api/users/methods/addEmail.js';
import '/imports/api/users/methods/removeEmail.js';
import '/imports/api/users/methods/updateFileStorageUsed.js';
import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
@@ -84,6 +85,10 @@ const userSchema = new SimpleSchema({
type: String,
regEx: SimpleSchema.RegEx.Id,
},
fileStorageUsed: {
type: Number,
optional: true,
},
profile: {
type: Object,
blackbox: true,

View File

@@ -0,0 +1,71 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import UserImages from '/imports/api/files/UserImages.js';
const fileCollections = [ArchiveCreatureFiles, UserImages];
const updateFileStorageUsed = new ValidatedMethod({
name: 'users.recalculateFileStorageUsed',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
const userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged in to recalculate your file use');
const user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('noUser', 'User not found');
}
updateFileStorageUsedWork(userId);
}
});
export default updateFileStorageUsed;
export function updateFileStorageUsedWork(userId) {
if (!userId) {
throw new Meteor.Error('idRequired',
'No user ID was provided to update file storage used')
}
let sum = 0;
fileCollections.forEach(collection => {
collection.find({ userId }, { fields: { size: 1 } }).forEach(file => {
sum += file.size;
});
});
Meteor.users.update(userId, {
$set: {
fileStorageUsed: sum,
}
});
}
export function incrementFileStorageUsed(userId, amount) {
if (!userId) {
throw new Meteor.Error('idRequired',
'No user ID was provided to update file storage used')
}
const user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('noUser', 'User not found');
}
if (user.fileStorageUsed === undefined) {
// The user doesn't have a current value for storage used, calculate it
// from scratch
updateFileStorageUsedWork(userId);
} else {
Meteor.users.update(userId, {
$inc: {
fileStorageUsed: amount,
}
});
}
}

View File

@@ -9,18 +9,21 @@ const TIERS = Object.freeze([
minimumEntitledCents: 0,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
name: 'Dreamer',
minimumEntitledCents: 100,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
name: 'Wanderer',
minimumEntitledCents: 300,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
//cost per user $5
@@ -28,6 +31,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 500,
invites: 0,
characterSlots: 20,
fileStorage: 200,
paidBenefits: true,
}, {
//cost per user $3.33
@@ -35,6 +39,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 1000,
invites: 2,
characterSlots: 50,
fileStorage: 500,
paidBenefits: true,
}, {
//cost per user $3.333
@@ -42,6 +47,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 2000,
invites: 5,
characterSlots: 120,
fileStorage: 1000,
paidBenefits: true,
}, {
//cost per user $3.125
@@ -49,6 +55,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 5000,
invites: 15,
characterSlots: -1, // Unlimited characters
fileStorage: 2000,
paidBenefits: true,
},
]);
@@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({
guest: true,
invites: 0,
characterSlots: 20,
fileStorage: 200,
paidBenefits: true,
});
@@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
name: 'Outlander',
invites: 0,
characterSlots: -1, // Can have infinitely many characters
fileStorage: 1000000, // 1TB file storage
paidBenefits: true,
});