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

This commit is contained in:
Stefan Zermatten
2022-10-22 19:29:31 +02:00
437 changed files with 18762 additions and 8849 deletions

View File

View File

@@ -1,4 +1,4 @@
import { SyncedCron } from 'meteor/percolate:synced-cron';
import { SyncedCron } from 'meteor/littledata:synced-cron';
SyncedCron.config({
// Log job run details to console

View File

@@ -1,50 +1,50 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
import { SyncedCron } from 'meteor/percolate:synced-cron';
import { SyncedCron } from 'meteor/littledata:synced-cron';
Meteor.startup(() => {
const collections = [
CreatureProperties,
const collections = [
CreatureProperties,
LibraryNodes,
];
];
/**
* Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function(){
const now = new Date();
/**
* Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function () {
const now = new Date();
const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: {$lt: yesterday} // dates *before* yesterday
}, function(error){
if (error){
console.error(JSON.stringify(error, null, 2));
}
});
});
};
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: { $lt: yesterday } // dates *before* yesterday
}, function (error) {
if (error) {
console.error(JSON.stringify(error, null, 2));
}
});
});
};
SyncedCron.add({
name: 'deleteSoftRemovedDocs',
schedule: function(parser) {
return parser.text('every 10 minutes');
},
job: deleteOldSoftRemovedDocs,
});
SyncedCron.add({
name: 'deleteSoftRemovedDocs',
schedule: function (parser) {
return parser.text('every 10 minutes');
},
job: deleteOldSoftRemovedDocs,
});
SyncedCron.start();
SyncedCron.start();
// Add a method to manually trigger removal
Meteor.methods({
deleteOldSoftRemovedDocs() {
// Add a method to manually trigger removal
Meteor.methods({
deleteOldSoftRemovedDocs() {
assertAdmin(this.userId);
this.unblock();
deleteOldSoftRemovedDocs();
},
});
this.unblock();
deleteOldSoftRemovedDocs();
},
});
});

View File

@@ -0,0 +1,8 @@
import * as sharp from 'sharp';
export default async function createThumbnail(image) {
await sharp(image)
.resize(320, 240)
.png()
.toBuffer();
}

View File

@@ -1,19 +0,0 @@
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
Meteor.publish('archivedCreatures', function(){
this.autorun(function (){
var userId = this.userId;
if (!userId) {
return [];
}
return ArchivedCreatures.find({
owner: userId,
}, {
fields: {
creature: 1,
owner: 1,
}
}
);
});
});

View File

@@ -0,0 +1,35 @@
import { propsByDocsPath } from '/imports/constants/PROPERTIES.js';
// Manual doc paths
const docPaths = [
'computed-fields',
'inline-calculations',
'dependency-loops',
'docs',
'tags',
'walkthroughs/create-a-class',
];
const docs = new Map();
docPaths.forEach(path => {
docs.set(path, Assets.getText(`docs/${path}.md`))
});
// Doc paths for properties
propsByDocsPath.forEach(prop => {
docs.set(prop.docsPath, Assets.getText(`docs/${prop.docsPath}.md`));
});
Meteor.publish('docs', function (path) {
if (!path) {
docs.forEach((text, path) => {
this.added('docs', path, { text });
});
} else {
const text = docs.get(path);
if (text) {
this.added('docs', path, { text });
}
}
this.ready();
});

View File

@@ -1,16 +1,16 @@
import Icons from '/imports/api/icons/Icons.js';
Meteor.publish('sampleIcons', function(){
return Icons.find({}, {limit: 50});
Meteor.publish('sampleIcons', function () {
return Icons.find({}, { limit: 50 });
});
Meteor.publish('searchIcons', function(searchValue) {
Meteor.publish('searchIcons', function (searchValue) {
// Don't publish anything if there's no search value
if (!searchValue) {
return [];
}
return Icons.find(
{ $text: {$search: searchValue} },
{ $text: { $search: searchValue } },
{
// relevant documents have a higher score.
fields: {

View File

@@ -8,6 +8,7 @@ import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js';
import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/archivedCreatures.js';
import '/imports/server/publications/searchLibraryNodes.js';
import '/imports/server/publications/archiveFiles.js';
import '/imports/server/publications/userImages.js';
import '/imports/server/publications/docs.js';

View File

@@ -1,27 +1,136 @@
import SimpleSchema from 'simpl-schema';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
import { union } from 'lodash';
Meteor.publish('libraries', function(){
this.autorun(function (){
const LIBRARY_NODE_TREE_FIELDS = {
_id: 1,
name: 1,
type: 1,
icon: 1,
color: 1,
order: 1,
parent: 1,
ancestors: 1,
tags: 1,
slotFillerCondition: 1,
removed: 1,
removedAt: 1,
// SlotFillers
slotQuantityFilled: 1,
slotFillerType: 1,
// Effect
operation: 1,
targetTags: 1,
stats: 1,
// Item
quantity: 1,
plural: 1,
equipped: 1,
// Branch
branchType: 1,
// Damage:
damageType: 1,
stat: 1,
amount: 1,
// Class level
level: 1,
variableName: 1,
// Proficiency
value: 1,
// Reference
cache: 1,
// Saving throw
dc: 1,
}
export { LIBRARY_NODE_TREE_FIELDS };
Meteor.publish('libraryCollection', function (libraryCollectionId) {
this.autorun(function () {
let userId = this.userId;
if (!userId) return [];
this.autorun(function () {
const libraryCollectionCursor = LibraryCollections.find({
_id: libraryCollectionId,
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
});
const libraryCollection = libraryCollectionCursor.fetch()[0];
if (!libraryCollection) return [ libraryCollectionCursor ];
this.autorun(function () {
const libraryCursor = Libraries.find({
_id: {$in: libraryCollection.libraries},
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
}, {
sort: { name: 1 }
});
return [ libraryCollectionCursor, libraryCursor ];
});
});
})
});
Meteor.publish('libraries', function () {
this.autorun(function () {
let userId = this.userId;
if (!userId) {
return [];
}
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
fields: { subscribedLibraries: 1, subscribedLibraryCollections: 1 }
});
const subs = user && user.subscribedLibraries || [];
return Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{ _id: {$in: subs}, public: true },
]
}, {
sort: {name: 1}
this.autorun(function () {
// Get the collections the user is subscribed to
const subCollections = user && user.subscribedLibraryCollections || [];
const libraryCollectionsCursor = LibraryCollections.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: subCollections }, public: true },
]
}, {
sort: { name: 1 }
});
// Collate all the libraryIds in those collections
let collectionLibIds = [];
libraryCollectionsCursor.forEach(libCollection => {
collectionLibIds = union(collectionLibIds, libCollection.libraries);
});
// Get the libraries the user is subscribed to directly
const subs = user && user.subscribedLibraries || [];
// Combine all the library Ids
const libIds = union(collectionLibIds, subs);
this.autorun(function () {
const librariesCursor = Libraries.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: libIds }, public: true },
]
}, {
sort: { name: 1 }
});
return [librariesCursor, libraryCollectionsCursor];
});
});
});
});
@@ -63,12 +172,35 @@ Meteor.publish('libraryNodes', function(libraryId){
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
sort: {order: 1},
sort: { order: 1 },
fields: LIBRARY_NODE_TREE_FIELDS,
}),
];
});
});
const nodeIdSchema = new SimpleSchema({
libraryNodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Meteor.publish('libraryNode', function (libraryNodeId) {
if (!libraryNodeId) return [];
nodeIdSchema.validate({ libraryNodeId });
this.autorun(function () {
const userId = this.userId;
const nodeCursor = LibraryNodes.find({_id: libraryNodeId});
let node = nodeCursor.fetch()[0];
try { assertDocViewPermission(node, userId) }
catch (e) {
return this.error(e);
}
return [ nodeCursor ];
});
});
Meteor.publish('softRemovedLibraryNodes', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});

View File

@@ -1,6 +1,8 @@
import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
@@ -37,7 +39,7 @@ Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
})];
});
Meteor.publish('searchLibraryNodes', function(){
Meteor.publish('searchLibraryNodes', function(creatureId){
let self = this;
this.autorun(function (){
let type = self.data('type');
@@ -49,23 +51,12 @@ Meteor.publish('searchLibraryNodes', function(){
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
if (!user) return [];
const subs = user.subscribedLibraries || [];
let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
]
}, {
fields: {_id: 1, name: 1},
});
let libraryIds = libraries.map(lib => lib._id);
let libraryIds;
if (creatureId) {
libraryIds = getCreatureLibraryIds(creatureId, userId)
} else {
libraryIds = getUserLibraryIds(userId)
}
// Build a filter for nodes in those libraries that match the type
let filter = {
@@ -122,6 +113,7 @@ Meteor.publish('searchLibraryNodes', function(){
});
let cursor = LibraryNodes.find(filter, options);
const libraries = Libraries.find({ _id: { $in: libraryIds } });
Mongo.Collection._publishCursor(libraries, self, 'libraries');

View File

@@ -1,10 +1,12 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
import { loadCreature } from '/imports/api/engine/loadCreatures.js';
let schema = new SimpleSchema({
creatureId: {
@@ -13,7 +15,8 @@ let schema = new SimpleSchema({
},
});
Meteor.publish('singleCharacter', function(creatureId){
Meteor.publish('singleCharacter', function (creatureId) {
const self = this;
try {
schema.validate({ creatureId });
} catch (e){
@@ -21,21 +24,27 @@ Meteor.publish('singleCharacter', function(creatureId){
}
this.autorun(function (computation){
let userId = this.userId;
let creatureCursor
creatureCursor = Creatures.find({
let permissionCreature = Creatures.findOne({
_id: creatureId,
}, {
fields: { owner: 1, readers: 1, writers: 1, public: 1, computeVersion: 1 }
});
let creature = creatureCursor.fetch()[0];
try { assertViewPermission(creature, userId) }
catch(e){ return [] }
if (creature.computeVersion !== VERSION && computation.firstRun){
try { assertViewPermission(permissionCreature, userId) }
catch (e) { return [] }
loadCreature(creatureId, self);
if (permissionCreature.computeVersion !== VERSION && computation.firstRun){
try {
computeCreature(creatureId)
}
catch(e){ console.error(e) }
}
return [
creatureCursor,
Creatures.find({
_id: creatureId,
}),
CreatureVariables.find({
_creatureId: creatureId,
}),
CreatureProperties.find({
'ancestors.id': creatureId,
}),

View File

@@ -3,6 +3,8 @@ import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js';
Meteor.publish('slotFillers', function(slotId, searchTerm){
if (searchTerm) check(searchTerm, String);
@@ -20,21 +22,18 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
const subs = user && user.subscribedLibraries || [];
let libraries = Libraries.find({
const creatureId = slot.ancestors[0].id;
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: libraryIds }, public: true },
]
}, {
fields: {_id: 1, name: 1},
sort: { name: 1 }
});
let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the slot
let filter = getSlotFillFilter({slot, libraryIds});
@@ -50,7 +49,8 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
options = {
// relevant documents have a higher score.
fields: {
_score: { $meta: 'textScore' }
_score: { $meta: 'textScore' },
...LIBRARY_NODE_TREE_FIELDS,
},
sort: {
// `score` property specified in the projection fields above.
@@ -61,10 +61,13 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
}
} else {
delete filter.$text
options = {sort: {
name: 1,
order: 1,
}};
options = {
sort: {
name: 1,
order: 1,
},
fields: LIBRARY_NODE_TREE_FIELDS,
};
}
options.limit = limit;
@@ -72,7 +75,65 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
self.setData('countAll', LibraryNodes.find(filter).count());
});
self.autorun(function () {
Meteor._sleepForMs(1000);
return [
LibraryNodes.find(filter, options),
libraries
];
});
});
});
});
Meteor.publish('classFillers', function(classId){
let self = this;
if (!classId) return [];
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
// Get the class
let classProp = CreatureProperties.findOne(classId);
if (!classProp){
return [];
}
// Get all the ids of libraries the user can access
const creatureId = classProp.ancestors[0].id;
const libraryIds = getCreatureLibraryIds(creatureId, userId);
const libraries = Libraries.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: libraryIds }, public: true },
]
}, {
sort: { name: 1 }
});
// Build a filter for nodes in those libraries that match the slot
let filter = getSlotFillFilter({slot: classProp, libraryIds});
this.autorun(function(){
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 50;
check(limit, Number);
let options = {
sort: {
name: 1,
order: 1,
},
fields: LIBRARY_NODE_TREE_FIELDS,
limit,
};
self.autorun(function () {
self.setData('countAll', LibraryNodes.find(filter).count());
});
self.autorun(function () {
return [LibraryNodes.find(filter, options), libraries];
});
});

View File

@@ -0,0 +1,7 @@
import UserImages from '/imports/api/files/UserImages.js';
Meteor.publish('userImages', function () {
return UserImages.find({
userId: this.userId,
}).cursor;
});

View File

@@ -2,30 +2,33 @@ import SimpleSchema from 'simpl-schema';
import '/imports/api/users/Users.js';
import Invites from '/imports/api/users/Invites.js';
Meteor.publish('user', function(){
return [
Meteor.users.find(this.userId, {fields: {
roles: 1,
username: 1,
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
fileStorageUsed: 1,
profile: 1,
preferences: 1,
'services.patreon.id': 1,
'services.patreon.entitledCents': 1,
'services.patreon.entitledCentsOverride': 1,
'services.google.id': 1,
'services.google.picture': 1,
'services.google.name': 1,
'services.google.email': 1,
'services.google.locale': 1,
}}),
Meteor.publish('user', function () {
return [
Meteor.users.find(this.userId, {
fields: {
roles: 1,
username: 1,
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
subscribedLibraryCollections: 1,
fileStorageUsed: 1,
profile: 1,
preferences: 1,
'services.patreon.id': 1,
'services.patreon.entitledCents': 1,
'services.patreon.entitledCentsOverride': 1,
'services.google.id': 1,
'services.google.picture': 1,
'services.google.name': 1,
'services.google.email': 1,
'services.google.locale': 1,
}
}),
Invites.find({
$or: [
{inviter: this.userId},
{invitee: this.userId}
{ inviter: this.userId },
{ invitee: this.userId }
],
}, {
fields: {
@@ -40,19 +43,19 @@ let userIdsSchema = new SimpleSchema({
type: Array,
optional: true,
},
'ids.$':{
'ids.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
})
Meteor.publish('userPublicProfiles', function(ids){
userIdsSchema.validate({ids});
if (!this.userId || !ids) return this.ready();
return Meteor.users.find({
_id: {$in: ids}
},{
fields: {username: 1},
sort: {username: 1},
});
Meteor.publish('userPublicProfiles', function (ids) {
userIdsSchema.validate({ ids });
if (!this.userId || !ids) return this.ready();
return Meteor.users.find({
_id: { $in: ids }
}, {
fields: { username: 1 },
sort: { username: 1 },
});
});

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
@@ -40,6 +41,9 @@ Meteor.publish('api-creature', function(creatureId){
CreatureProperties.find({
'ancestors.id': creatureId,
}),
CreatureVariables.find({
_creatureId: creatureId,
}),
];
}, {
url: 'api/creature/:0'