diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js new file mode 100644 index 00000000..9a31e99e --- /dev/null +++ b/app/imports/api/library/LibraryCollections.js @@ -0,0 +1,128 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import SimpleSchema from 'simpl-schema'; +import SharingSchema from '/imports/api/sharing/SharingSchema.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers.js' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +/** + * LibraryCollections are groups of libraries that are subscribed together at once + */ +let LibraryCollections = new Mongo.Collection('librarycollections'); + +let LibraryCollectionSchema = new SimpleSchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + description: { + type: String, + optional: true, + max: STORAGE_LIMITS.summary, + }, + libraries: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.libraryCollectionCount, + }, + 'libraries.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); + +LibraryCollectionSchema.extend(SharingSchema); +LibraryCollections.attachSchema(LibraryCollectionSchema); + +export default LibraryCollections; + +const insertLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.insert', + mixins: [ + simpleSchemaMixin, + ], + schema: LibraryCollectionSchema.omit('owner'), + run(libraryCollection) { + if (!this.userId) { + throw new Meteor.Error('LibraryCollections.methods.insert.denied', + 'You need to be logged in to insert a library'); + } + let tier = getUserTier(this.userId); + if (!tier.paidBenefits){ + throw new Meteor.Error('LibraryCollections.methods.insert.denied', + `The ${tier.name} tier does not allow you to insert a library collection`); + } + libraryCollection.owner = this.userId; + return LibraryCollections.insert(libraryCollection); + }, +}); + +const updateLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.update', + mixins: [ + simpleSchemaMixin, + ], + schema: { + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + update: { + type: LibraryCollectionSchema.pick('name', 'description', 'libraries') + } + }, + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, update}){ + const libraryCollection = LibraryCollections.findOne(_id, { + fields: { + owner: 1, + writers: 1, + } + }); + assertEditPermission(libraryCollection, this.userId); + return LibraryCollections.update(_id, {$set: update}); + }, +}); + +const removeLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.remove', + validate: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.id + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id}){ + const libraryCollection = LibraryCollections.findOne(_id, { + fields: { + owner: 1, + } + }); + assertOwnership(libraryCollection, this.userId); + return LibraryCollections.remove(_id); + } +}); + +function getLibraryIdsByCollectionId(libraryCollectionId) { + const libraryCollection = LibraryCollections.findOne(libraryCollectionId) + return libraryCollection?.libraries || []; +} + +export { + LibraryCollectionSchema, + insertLibraryCollection, + updateLibraryCollection, + removeLibraryCollection, + getLibraryIdsByCollectionId, +}; diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 31a4d818..6f228322 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -8,6 +8,7 @@ import '/imports/api/users/methods/updateFileStorageUsed.js'; import { some } from 'lodash'; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; +const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || []; const userSchema = new SimpleSchema({ username: { @@ -74,6 +75,15 @@ const userSchema = new SimpleSchema({ }, 'subscribedLibraries.$': { type: String, + regEx: SimpleSchema.RegEx.Id, + }, + subscribedLibraryCollections: { + type: Array, + defaultValue: defaultLibraryCollections, + max: 100, + }, + 'subscribedLibraryCollections.$': { + type: String, regEx: SimpleSchema.RegEx.Id, }, subscribedCharacters: { @@ -270,6 +280,36 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({ } }); +Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({ + name: 'users.subscribeToLibraryCollection', + validate: new SimpleSchema({ + libraryCollectionId:{ + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + subscribe: { + type: Boolean, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({libraryCollectionId, subscribe}){ + if (!this.userId) throw 'Can only subscribe if logged in'; + if (subscribe){ + return Meteor.users.update(this.userId, { + $addToSet: {subscribedLibraryCollections: libraryCollectionId}, + }); + } else { + return Meteor.users.update(this.userId, { + $pullAll: {subscribedLibraryCollections: libraryCollectionId}, + }); + } + } +}); + Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({ name: 'users.findUserByUsernameOrEmail', validate: new SimpleSchema({ diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index f2bd1e1f..6e116b07 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -31,6 +31,7 @@ const STORAGE_LIMITS = Object.freeze({ statsToTarget: 64, tagCount: 64, writersCount: 20, + libraryCollectionCount: 32, }); export default STORAGE_LIMITS;