todos-contra-el-fuego-web/imports/api/Comments/server/methods.js
vjrj 66bab7a43b meteor3: Comments methods and mail fan-out to async
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).
2026-07-14 06:52:32 +02:00

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
});