comments.insert/edit/remove/like/dislike use *Async collection APIs; requireOwner/toggle helpers are async; onCommentAdd iterates users with forEachAsync. subscriptions.* methods were already async. REST smoke stays byte-identical (12/12).
102 lines
3.6 KiB
JavaScript
102 lines
3.6 KiB
JavaScript
/* eslint-disable import/no-absolute-path */
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { check } from 'meteor/check';
|
|
import Comments from '/imports/api/Comments/Comments';
|
|
import getMediaFromContent from '/imports/api/Comments/mediaAnalyzers';
|
|
import rateLimit from '/imports/modules/rate-limit';
|
|
import onCommentAdd from '/imports/api/Comments/server/onCommentAdd';
|
|
|
|
const MAX_LEN = 5000;
|
|
|
|
function usernameOf(user) {
|
|
return (user && user.profile && user.profile.name && user.profile.name.first)
|
|
? user.profile.name.first
|
|
: '';
|
|
}
|
|
|
|
async function requireOwner(commentId, userId) {
|
|
const comment = await Comments.findOneAsync(commentId);
|
|
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
|
if (comment.userId !== userId) throw new Meteor.Error('403', 'Not your comment');
|
|
return comment;
|
|
}
|
|
|
|
async function toggle(commentId, field, otherField, userId) {
|
|
check(commentId, String);
|
|
if (!userId) throw new Meteor.Error('403', 'Login required');
|
|
const comment = await Comments.findOneAsync(commentId);
|
|
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
|
const has = (comment[field] || []).includes(userId);
|
|
const modifier = has
|
|
? { $pull: { [field]: userId } }
|
|
: { $addToSet: { [field]: userId }, $pull: { [otherField]: userId } };
|
|
await Comments.updateAsync(commentId, modifier);
|
|
}
|
|
|
|
Meteor.methods({
|
|
'comments.insert': async function commentsInsert(referenceId, content) {
|
|
check(referenceId, String);
|
|
check(content, String);
|
|
if (!this.userId) throw new Meteor.Error('403', 'Login required to comment');
|
|
const text = content.trim();
|
|
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
|
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
|
|
|
const now = new Date();
|
|
const doc = {
|
|
referenceId,
|
|
content: text,
|
|
userId: this.userId,
|
|
username: usernameOf(await Meteor.users.findOneAsync(this.userId)),
|
|
media: getMediaFromContent(text),
|
|
likes: [],
|
|
dislikes: [],
|
|
status: 'approved',
|
|
createdAt: now,
|
|
updatedAt: now
|
|
};
|
|
const _id = await Comments.insertAsync(doc);
|
|
// Fire-and-forget: notify other commenters of this fire. Never let a mail
|
|
// failure break the insert.
|
|
try {
|
|
onCommentAdd({ ...doc, _id });
|
|
} catch (e) {
|
|
console.warn(`comments onCommentAdd failed: ${e}`);
|
|
}
|
|
return _id;
|
|
},
|
|
|
|
'comments.edit': async function commentsEdit(commentId, content) {
|
|
check(commentId, String);
|
|
check(content, String);
|
|
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
|
await requireOwner(commentId, this.userId);
|
|
const text = content.trim();
|
|
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
|
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
|
await Comments.updateAsync(commentId, {
|
|
$set: { content: text, media: getMediaFromContent(text), updatedAt: new Date() }
|
|
});
|
|
},
|
|
|
|
'comments.remove': async function commentsRemove(commentId) {
|
|
check(commentId, String);
|
|
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
|
await requireOwner(commentId, this.userId);
|
|
await Comments.removeAsync(commentId);
|
|
},
|
|
|
|
'comments.like': async function commentsLike(commentId) {
|
|
await toggle(commentId, 'likes', 'dislikes', this.userId);
|
|
},
|
|
|
|
'comments.dislike': async function commentsDislike(commentId) {
|
|
await toggle(commentId, 'dislikes', 'likes', this.userId);
|
|
}
|
|
});
|
|
|
|
rateLimit({
|
|
methods: ['comments.insert', 'comments.edit', 'comments.remove', 'comments.like', 'comments.dislike'],
|
|
limit: 5,
|
|
timeRange: 1000
|
|
});
|