mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
commit3aba0abeddAuthor: SabreCat <sabe@habitica.com> Date: Mon Oct 2 20:51:20 2023 -0500 fix(router): use state to pass modal launch info commit541eadd319Merge:c0bb56c8c289fff49d02Author: SabreCat <sabe@habitica.com> Date: Mon Oct 2 20:12:40 2023 -0500 Merge branch 'release' into report-profile-modal commitc0bb56c8c2Author: SabreCat <sabe@habitica.com> Date: Wed Sep 27 16:15:28 2023 -0500 test(profiles): add integrations commit9b644e9ad8Author: SabreCat <sabe@habitica.com> Date: Tue Sep 26 17:17:22 2023 -0500 fix(profile): adjust margin commitbfefe5dfa9Author: SabreCat <sabe@habitica.com> Date: Tue Sep 26 17:12:24 2023 -0500 fix(profiles): moar layout fixes commit8f211ee3e2Author: SabreCat <sabe@habitica.com> Date: Mon Sep 25 17:32:04 2023 -0500 fix(profile): fix admin actions Correct "user is banned" banner Fix bouncing modal Add "Days" smart plural Fix leaky CSS on Market page Refactor some redundant functions commitb1d23ec88bMerge:ee9709a9e1a63cc84779Author: SabreCat <sabe@habitica.com> Date: Mon Sep 25 15:37:54 2023 -0500 Merge branch 'release' into report-profile-modal commitee9709a9e1Author: CuriousMagpie <eilatan@gmail.com> Date: Mon Sep 18 16:30:30 2023 -0400 WIP(profile): add banned banner, toggle switches now toggle, add "days" to Next Login Reward commitf80928a895Author: CuriousMagpie <eilatan@gmail.com> Date: Mon Sep 18 13:43:34 2023 -0400 update(node): update node modules commit1d552f7e80Author: SabreCat <sabe@habitica.com> Date: Fri Sep 15 16:52:22 2023 -0500 fix(import): remove empty import commitf55d74a95dAuthor: SabreCat <sabe@habitica.com> Date: Fri Sep 15 16:39:50 2023 -0500 refactor(profiles): remove email feature also still more visual cleanup of profile modal commit311c743284Author: SabreCat <sabe@habitica.com> Date: Fri Sep 15 15:44:56 2023 -0500 refactor(profile): remove page view commitf8632bf50dMerge:ec85159c659e25360102Author: SabreCat <sabe@habitica.com> Date: Fri Sep 15 15:23:21 2023 -0500 Merge branch 'release' into report-profile-modal commitec85159c65Author: SabreCat <sabe@habitica.com> Date: Mon Sep 11 22:53:14 2023 -0500 feat(profiles): load modal instead of page? commit9986082914Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 8 14:49:57 2023 -0400 WIP(profile): fixed a comment, woohoo commit6262a9ba0cMerge:ae2b614df2ea2b007b1aAuthor: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 8 13:40:23 2023 -0400 Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal commitea2b007b1aAuthor: SabreCat <sabe@habitica.com> Date: Thu Sep 7 16:54:19 2023 -0500 fix(profile): focus behavior commitae2b614df2Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Sep 7 17:47:08 2023 -0400 WIP(profile): styling updates commit2e0723f1b9Author: SabreCat <sabe@habitica.com> Date: Thu Sep 7 15:37:59 2023 -0500 feat(moderation): unflag profile Also a few stylistic tweaks commitedcf8113deAuthor: SabreCat <sabe@habitica.com> Date: Wed Sep 6 16:39:02 2023 -0500 WIP(profile): dropdown draft commit0691483d63Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Sep 6 16:33:30 2023 -0400 WIP(profile): Styling and string updates commit7e9d57d10aAuthor: SabreCat <sabe@habitica.com> Date: Wed Sep 6 11:40:31 2023 -0500 feat(profile): functional dropdown buttons commita2989b2833Merge:af6575e40ce072d7c09cAuthor: SabreCat <sabe@habitica.com> Date: Wed Sep 6 10:04:57 2023 -0500 Merge branch 'release' into report-profile-modal commitaf6575e40cAuthor: CuriousMagpie <eilatan@gmail.com> Date: Wed Sep 6 11:01:05 2023 -0400 WIP(profile): comment cleanup commit7b1de37202Author: CuriousMagpie <eilatan@gmail.com> Date: Tue Sep 5 17:22:14 2023 -0400 WIP(profile): remove shadowban tooltip commitd1177c32b9Merge:321a01b08131f821021bAuthor: CuriousMagpie <eilatan@gmail.com> Date: Tue Sep 5 17:02:40 2023 -0400 Merge branch 'sabrecat/report-profile' into report-profile-modal commit321a01b081Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 1 16:14:36 2023 -0400 WIP(profile): close button finally workinating commite143d36d28Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 1 15:52:38 2023 -0400 WIP(profile): close icon moved to profile.vue commit31f821021bMerge:a8f5e25d388957c5c009Author: SabreCat <sabe@habitica.com> Date: Fri Sep 1 14:52:31 2023 -0500 Merge branch 'report-profile-modal' into sabrecat/report-profile commit8957c5c009Merge:d340f06a220aec3866a4Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 1 15:38:12 2023 -0400 Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal commitd340f06a22Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Sep 1 15:37:57 2023 -0400 WIP(profile): fixed user not found error commit0aec3866a4Merge:b01f323b14ac7c8e0eb6Author: Natalie <78037386+CuriousMagpie@users.noreply.github.com> Date: Fri Sep 1 15:28:58 2023 -0400 Merge branch 'HabitRPG:develop' into report-profile-modal commita8f5e25d38Author: SabreCat <sabe@habitica.com> Date: Thu Aug 31 17:02:07 2023 -0500 feat(community): basic "report profile" commitb01f323b14Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Aug 31 17:42:12 2023 -0400 WIP(profile): removed refactoring crud, located where close icon should be (profileModal.vue) commitce7d51a20cMerge:010f2299f0ac7c8e0eb6Author: SabreCat <sabe@habitica.com> Date: Thu Aug 31 14:20:37 2023 -0500 Merge branch 'release' into sabrecat/report-profile commit18b41acd94Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Aug 31 12:23:41 2023 -0400 WIP(profile): moar buttonz commit9387b3a6bcAuthor: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 30 17:21:36 2023 -0400 WIP(profile): buttons commitb3ea48c4f5Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Aug 25 15:52:41 2023 -0400 WIP(profile): work on achievement component commita1ceb2ea75Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Aug 25 14:39:12 2023 -0400 WIP(profile): create achievements component commit4a24d9b80bMerge:8fe263a3771e05297e96Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 23 13:14:39 2023 -0400 Merge branch 'develop' into report-profile-modal commit1e05297e96Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 23 13:12:52 2023 -0400 package updates commit8fe263a377Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 23 12:12:36 2023 -0400 update(dependencies): ran npm install to update dependencies commit190fe048a1Merge:3ea48ab5cbfa83d1a9cfAuthor: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 23 11:52:08 2023 -0400 Merge branch 'develop' into report-profile-modal commit3ea48ab5cbAuthor: CuriousMagpie <eilatan@gmail.com> Date: Fri Aug 11 17:12:31 2023 -0400 WIP(user profile): dropdown menu and toggles and colors oh my commitc301a2b460Merge:1da6af11b5647b27c55fAuthor: CuriousMagpie <eilatan@gmail.com> Date: Fri Aug 11 12:40:07 2023 -0400 Merge branch 'develop' into report-profile-modal commit1da6af11b5Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Aug 10 16:50:07 2023 -0400 WIP(user profile): moved some CSS classes out of unscoped and into the scoped section, started on toggle buttons commitdd55cbc928Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 9 15:38:46 2023 -0400 WIP(user profile): workin on the hamburger (kebab?) menu commit3834093207Author: CuriousMagpie <eilatan@gmail.com> Date: Tue Aug 8 14:14:40 2023 -0400 WIP(user profiles): working on the drop down menu commitf2be588195Author: CuriousMagpie <eilatan@gmail.com> Date: Mon Aug 7 16:10:30 2023 -0400 WIP(user profile): options menu commit010f2299f0Author: SabreCat <sabe@habitica.com> Date: Mon Aug 7 11:49:04 2023 -0500 fix(lint): eof and const commit4551dbf4b3Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Aug 4 15:34:05 2023 -0400 WIP(user profile): styling the top portion of the modal commit19a9fe3644Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Aug 3 15:06:51 2023 -0400 WIP(user profile): adding buttons commitdfdb305b1cAuthor: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 2 14:41:20 2023 -0400 WIP(user profile): layout commitded4eee693Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Aug 2 12:04:02 2023 -0400 WIP(user profile): start flex grid & tidy up CSS commitaaca48be32Author: CuriousMagpie <eilatan@gmail.com> Date: Fri Jul 28 16:44:06 2023 -0400 WIP(user profile): mostly css updates commite531985b87Author: CuriousMagpie <eilatan@gmail.com> Date: Thu Jul 27 16:49:44 2023 -0400 WIP(user profile): one infinitesimal change that's hardly worth the electricity it's made from commiteb4021fcc7Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Jul 26 16:33:05 2023 -0400 feat(content): upgrade profile page commit1b25394f3eMerge:c50cee0d888558dcc3a8Author: CuriousMagpie <eilatan@gmail.com> Date: Wed Jul 26 11:50:12 2023 -0400 Merge branch 'develop' into report-profile-modal commitc50cee0d88Author: SabreCat <sabe@habitica.com> Date: Wed Jul 12 16:32:25 2023 -0500 fix(flagging): debug params issue Also add and document the "source" body param commit55848c58beAuthor: SabreCat <sabe@habitica.com> Date: Mon Jul 10 16:24:20 2023 -0500 WIP(members): basic report a user API commitdda6180792Author: SabreCat <sabe@habitica.com> Date: Thu Jul 6 10:05:07 2023 -0500 fix(lint): remove console.info
575 lines
19 KiB
JavaScript
575 lines
19 KiB
JavaScript
import moment from 'moment';
|
|
import nconf from 'nconf';
|
|
import { authWithHeaders } from '../../middlewares/auth';
|
|
import { model as Group } from '../../models/group';
|
|
import { model as User } from '../../models/user';
|
|
import {
|
|
chatModel as Chat,
|
|
sanitizeText as sanitizeMessageText,
|
|
} from '../../models/message';
|
|
import common from '../../../common';
|
|
import {
|
|
BadRequest,
|
|
NotFound,
|
|
NotAuthorized,
|
|
} from '../../libs/errors';
|
|
import { removeFromArray } from '../../libs/collectionManipulators';
|
|
import { getUserInfo, getGroupUrl } from '../../libs/email';
|
|
import * as slack from '../../libs/slack';
|
|
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
|
|
import { getAuthorEmailFromMessage } from '../../libs/chat';
|
|
import bannedWords from '../../libs/bannedWords';
|
|
import { getMatchesByWordArray } from '../../libs/stringUtils';
|
|
import bannedSlurs from '../../libs/bannedSlurs';
|
|
import apiError from '../../libs/apiError';
|
|
import highlightMentions from '../../libs/highlightMentions';
|
|
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
|
|
|
|
const analytics = getAnalyticsServiceByEnvironment();
|
|
|
|
const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE'));
|
|
|
|
/**
|
|
* @apiDefine MessageNotFound
|
|
* @apiError (404) {NotFound} MessageNotFound The specified message could not be found.
|
|
*/
|
|
|
|
/**
|
|
* @apiDefine GroupIdRequired
|
|
* @apiError (400) {badRequest} groupIdRequired A group ID is required
|
|
*/
|
|
|
|
/**
|
|
* @apiDefine ChatIdRequired
|
|
* @apiError (400) {badRequest} chatIdRequired A chat ID is required
|
|
*/
|
|
|
|
/**
|
|
* @apiDefine MessageIdRequired
|
|
* @apiError (400) {badRequest} messageIdRequired A message ID is required
|
|
*/
|
|
|
|
const api = {};
|
|
|
|
function textContainsBannedSlur (message) {
|
|
const bannedSlursMatched = getMatchesByWordArray(message, bannedSlurs);
|
|
return bannedSlursMatched.length > 0;
|
|
}
|
|
|
|
/**
|
|
* @api {get} /api/v3/groups/:groupId/chat Get chat messages from a group
|
|
* @apiName GetChat
|
|
* @apiGroup Chat
|
|
* @apiDescription Fetches an array of messages from a group
|
|
*
|
|
* @apiParam (Path) {String} groupId The group _id ('party' for the user party and
|
|
* 'habitrpg' for tavern are accepted).
|
|
*
|
|
* @apiSuccess {Array} data An array of <a href='https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js#L51' target='_blank'>chat messages</a>
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse GroupIdRequired
|
|
*/
|
|
api.getChat = {
|
|
method: 'GET',
|
|
url: '/groups/:groupId/chat',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const { groupId } = req.params;
|
|
const group = await Group.getGroup({ user, groupId, fields: 'chat' });
|
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const groupChat = await Group.toJSONCleanChat(group, user);
|
|
res.respond(200, groupChat.chat);
|
|
},
|
|
};
|
|
|
|
function getBannedWordsFromText (message) {
|
|
return getMatchesByWordArray(message, bannedWords);
|
|
}
|
|
|
|
/**
|
|
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
|
|
* @apiName PostChat
|
|
* @apiGroup Chat
|
|
* @apiDescription Posts a chat message to a group
|
|
*
|
|
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted)
|
|
* @apiParam (Body) {String} message Message The message to post
|
|
* @apiParam (Query) {UUID} previousMsg The previous chat message's UUID which will
|
|
* force a return of the full group chat.
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse GroupIdRequired
|
|
* @apiError (400) {NotAuthorized} chatPriviledgesRevoked You cannot do that because
|
|
* your chat privileges have been revoked.
|
|
*/
|
|
api.postChat = {
|
|
method: 'POST',
|
|
url: '/groups/:groupId/chat',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { groupId } = req.params;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
req.sanitize('message').trim();
|
|
req.checkBody('message', res.t('messageGroupChatBlankMessage')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const group = await Group.getGroup({ user, groupId });
|
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const { purchased } = group;
|
|
const isUpgraded = purchased && purchased.plan && purchased.plan.customerId
|
|
&& (!purchased.plan.dateTerminated || moment().isBefore(purchased.plan.dateTerminated));
|
|
|
|
if (group.type !== 'party' && !isUpgraded) {
|
|
throw new BadRequest(res.t('featureRetired'));
|
|
}
|
|
|
|
// Check message for banned slurs
|
|
if (group && group.privacy !== 'private' && textContainsBannedSlur(req.body.message)) {
|
|
const { message } = req.body;
|
|
user.flags.chatRevoked = true;
|
|
await user.save();
|
|
|
|
// Email the mods
|
|
const authorEmail = getUserInfo(user, ['email']).email;
|
|
const groupUrl = getGroupUrl(group);
|
|
|
|
const report = [
|
|
{ name: 'MESSAGE_TIME', content: (new Date()).toString() },
|
|
{ name: 'MESSAGE_TEXT', content: message },
|
|
|
|
{ name: 'AUTHOR_USERNAME', content: user.profile.name },
|
|
{ name: 'AUTHOR_UUID', content: user._id },
|
|
{ name: 'AUTHOR_EMAIL', content: authorEmail },
|
|
{ name: 'AUTHOR_MODAL_URL', content: `/profile/${user._id}` },
|
|
|
|
{ name: 'GROUP_NAME', content: group.name },
|
|
{ name: 'GROUP_TYPE', content: group.type },
|
|
{ name: 'GROUP_ID', content: group._id },
|
|
{ name: 'GROUP_URL', content: groupUrl },
|
|
];
|
|
|
|
// Slack the mods
|
|
slack.sendSlurNotification({
|
|
authorEmail,
|
|
author: user,
|
|
group,
|
|
message,
|
|
});
|
|
|
|
throw new BadRequest(res.t('bannedSlurUsed'));
|
|
}
|
|
|
|
if (group.privacy === 'public' && user.flags.chatRevoked) {
|
|
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
|
}
|
|
|
|
// prevent banned words being posted, except in private guilds/parties
|
|
// and in certain public guilds with specific topics
|
|
if (group.privacy === 'public' && !group.bannedWordsAllowed) {
|
|
const matchedBadWords = getBannedWordsFromText(req.body.message);
|
|
if (matchedBadWords.length > 0) {
|
|
throw new BadRequest(res.t('bannedWordUsed', { swearWordsUsed: matchedBadWords.join(', ') }));
|
|
}
|
|
}
|
|
|
|
const chatRes = await Group.toJSONCleanChat(group, user);
|
|
const lastClientMsg = req.query.previousMsg;
|
|
const chatUpdated = !!(
|
|
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
|
|
);
|
|
|
|
if (group.checkChatSpam(user)) {
|
|
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
|
}
|
|
|
|
// Check if account is newer than the minimum age for chat participation
|
|
if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) {
|
|
analytics.track('chat age error', {
|
|
uuid: user._id,
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
headers: req.headers,
|
|
});
|
|
throw new BadRequest(res.t('chatTemporarilyUnavailable'));
|
|
}
|
|
|
|
const sanitizedMessageText = sanitizeMessageText(req.body.message);
|
|
const [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText);
|
|
let client = req.headers['x-client'] || '3rd Party';
|
|
if (client) {
|
|
client = client.replace('habitica-', '');
|
|
}
|
|
|
|
let flagCount = 0;
|
|
if (group.privacy === 'public' && user.flags.chatShadowMuted) {
|
|
flagCount = common.constants.CHAT_FLAG_FROM_SHADOW_MUTE;
|
|
|
|
// Email the mods
|
|
const authorEmail = getUserInfo(user, ['email']).email;
|
|
const groupUrl = getGroupUrl(group);
|
|
|
|
const report = [
|
|
{ name: 'MESSAGE_TIME', content: (new Date()).toString() },
|
|
{ name: 'MESSAGE_TEXT', content: message },
|
|
|
|
{ name: 'AUTHOR_USERNAME', content: user.profile.name },
|
|
{ name: 'AUTHOR_UUID', content: user._id },
|
|
{ name: 'AUTHOR_EMAIL', content: authorEmail },
|
|
{ name: 'AUTHOR_MODAL_URL', content: `/profile/${user._id}` },
|
|
|
|
{ name: 'GROUP_NAME', content: group.name },
|
|
{ name: 'GROUP_TYPE', content: group.type },
|
|
{ name: 'GROUP_ID', content: group._id },
|
|
{ name: 'GROUP_URL', content: groupUrl },
|
|
];
|
|
|
|
// Slack the mods
|
|
slack.sendShadowMutedPostNotification({
|
|
authorEmail,
|
|
author: user,
|
|
group,
|
|
message,
|
|
});
|
|
}
|
|
|
|
const newChatMessage = group.sendChat({
|
|
message,
|
|
user,
|
|
flagCount,
|
|
metaData: null,
|
|
client,
|
|
translate: res.t,
|
|
mentions,
|
|
mentionedMembers,
|
|
});
|
|
const toSave = [newChatMessage.save()];
|
|
|
|
if (group.type === 'party') {
|
|
user.party.lastMessageSeen = newChatMessage.id;
|
|
toSave.push(user.save());
|
|
}
|
|
|
|
await Promise.all(toSave);
|
|
|
|
const analyticsObject = {
|
|
uuid: user._id,
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
groupType: group.type,
|
|
privacy: group.privacy,
|
|
headers: req.headers,
|
|
};
|
|
|
|
if (mentions) {
|
|
analyticsObject.mentionsCount = mentions.length;
|
|
} else {
|
|
analyticsObject.mentionsCount = 0;
|
|
}
|
|
if (group.privacy === 'public') {
|
|
analyticsObject.groupName = group.name;
|
|
}
|
|
|
|
res.analytics.track('group chat', analyticsObject);
|
|
|
|
if (chatUpdated) {
|
|
res.respond(200, { chat: chatRes.chat });
|
|
} else {
|
|
res.respond(200, { message: newChatMessage });
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/groups/:groupId/chat/:chatId/like Like a group chat message
|
|
* @apiName LikeChat
|
|
* @apiGroup Chat
|
|
* @apiDescription Likes a chat message from a group
|
|
*
|
|
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted).
|
|
* @apiParam (Path) {UUID} chatId The chat message _id
|
|
*
|
|
* @apiSuccess {Object} data The liked <a href='https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js#L51' target='_blank'>chat message</a>
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse MessageNotFound
|
|
* @apiUse GroupIdRequired
|
|
* @apiUse ChatIdRequired
|
|
* @apiError (400) {NotFound} messageGroupChatLikeOwnMessage A user can't like their own message
|
|
*/
|
|
api.likeChat = {
|
|
method: 'POST',
|
|
url: '/groups/:groupId/chat/:chatId/like',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { groupId } = req.params;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const group = await Group.getGroup({ user, groupId });
|
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const message = await Chat.findOne({ _id: req.params.chatId }).exec();
|
|
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
|
|
// @TODO correct this error type
|
|
if (message.uuid === user._id) throw new NotFound(res.t('messageGroupChatLikeOwnMessage'));
|
|
|
|
if (!message.likes) message.likes = {};
|
|
message.likes[user._id] = !message.likes[user._id];
|
|
message.markModified('likes');
|
|
await message.save();
|
|
|
|
res.respond(200, message); // TODO what if the message is flagged and shouldn't be returned?
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/groups/:groupId/chat/:chatId/flag Flag a group chat message
|
|
* @apiDescription A message will be hidden from chat if two or more users flag a message.
|
|
* It will be hidden immediately if a moderator flags the message.
|
|
* An email is sent to the moderators about every flagged message.
|
|
* @apiName FlagChat
|
|
* @apiGroup Chat
|
|
*
|
|
* @apiParam (Path) {UUID} groupId The group id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted)
|
|
* @apiParam (Path) {UUID} chatId The chat message id
|
|
* @apiParam (Body) {String} [comment] explain why the message was flagged
|
|
*
|
|
* @apiSuccess {Object} data The flagged chat message
|
|
* @apiSuccess {UUID} data.id The id of the message
|
|
* @apiSuccess {String} data.text The text of the message
|
|
* @apiSuccess {Number} data.timestamp The timestamp of the message in milliseconds
|
|
* @apiSuccess {Object} data.likes The likes of the message
|
|
* @apiSuccess {Object} data.flags The flags of the message
|
|
* @apiSuccess {Number} data.flagCount The number of flags the message has
|
|
* @apiSuccess {UUID} data.uuid The User ID of the author of the message
|
|
* @apiSuccess {String} data.user The username of the author of the message
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse MessageNotFound
|
|
* @apiUse GroupIdRequired
|
|
* @apiUse ChatIdRequired
|
|
* @apiError (404) {NotFound} AlreadyFlagged Chat messages cannot be flagged
|
|
more than once by a user
|
|
* @apiError (404) {NotFound} messageGroupChatFlagAlreadyReported The message
|
|
has already been flagged.
|
|
*/
|
|
api.flagChat = {
|
|
method: 'POST',
|
|
url: '/groups/:groupId/chat/:chatId/flag',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const chatReporter = chatReporterFactory('Group', req, res);
|
|
const message = await chatReporter.flag();
|
|
res.respond(200, message);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/groups/:groupId/chat/:chatId/clearflags Clear flags
|
|
* @apiDescription Resets the flag count on a chat message.
|
|
* Retains the id of the user's that have flagged the message. (Only visible to moderators)
|
|
* @apiPermission Admin
|
|
* @apiName ClearFlags
|
|
* @apiGroup Chat
|
|
*
|
|
* @apiParam (Path) {UUID} groupId The group id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted)
|
|
* @apiParam (Path) {UUID} chatId The chat message id
|
|
*
|
|
* @apiSuccess {Object} data An empty object
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse MessageNotFound
|
|
* @apiUse GroupIdRequired
|
|
* @apiUse ChatIdRequired
|
|
* @apiError (404) {NotAuthorized} MustBeAdmin Must be a moderator to use this route
|
|
*/
|
|
api.clearChatFlags = {
|
|
method: 'Post',
|
|
url: '/groups/:groupId/chat/:chatId/clearflags',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { groupId } = req.params;
|
|
const { chatId } = req.params;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
if (!user.hasPermission('moderator')) {
|
|
throw new NotAuthorized(res.t('messageGroupChatAdminClearFlagCount'));
|
|
}
|
|
|
|
const group = await Group.getGroup({
|
|
user,
|
|
groupId,
|
|
optionalMembership: user.hasPermission('moderator'),
|
|
});
|
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const message = await Chat.findOne({ _id: chatId }).exec();
|
|
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
|
|
|
|
message.flagCount = 0;
|
|
await message.save();
|
|
|
|
const adminEmailContent = getUserInfo(user, ['email']).email;
|
|
const authorEmail = getAuthorEmailFromMessage(message);
|
|
const groupUrl = getGroupUrl(group);
|
|
|
|
res.respond(200, {});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/groups/:groupId/chat/seen Mark all messages as read for a group
|
|
* @apiName SeenChat
|
|
* @apiGroup Chat
|
|
*
|
|
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted)
|
|
*
|
|
* @apiSuccess {Object} data An empty object
|
|
* @apiUse GroupIdRequired
|
|
*/
|
|
api.seenChat = {
|
|
method: 'POST',
|
|
url: '/groups/:groupId/chat/seen',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { groupId } = req.params;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
// Do not validate group existence,
|
|
// it doesn't really matter and make it works if the group gets deleted
|
|
// let group = await Group.getGroup({user, groupId});
|
|
// if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const update = {
|
|
$unset: {},
|
|
$pull: {},
|
|
};
|
|
update.$unset[`newMessages.${groupId}`] = true;
|
|
|
|
update.$pull.notifications = {
|
|
type: 'NEW_CHAT_MESSAGE',
|
|
'data.group.id': groupId,
|
|
};
|
|
|
|
// Remove from response
|
|
user.notifications = user.notifications.filter(n => {
|
|
if (n && n.type === 'NEW_CHAT_MESSAGE' && n.data && n.data.group && n.data.group.id === groupId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Update the user version field manually,
|
|
// it cannot be updated in the pre update hook
|
|
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
|
user._v += 1;
|
|
|
|
await User.update({ _id: user._id }, update).exec();
|
|
res.respond(200, {});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {delete} /api/v3/groups/:groupId/chat/:chatId Delete chat message from a group
|
|
* @apiName DeleteChat
|
|
* @apiGroup Chat
|
|
* @apiDescription Delete's a chat message from a group
|
|
*
|
|
* @apiParam (Query) {UUID} previousMsg The last message's ID fetched by the
|
|
* client so that the whole chat will be returned only
|
|
* if new messages have been posted in the meantime.
|
|
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg'
|
|
* for tavern are accepted).
|
|
* @apiParam (Path) {UUID} chatId The chat message id
|
|
*
|
|
* @apiSuccess data The updated chat array or an empty object if no message was posted
|
|
* after previousMsg.
|
|
* @apiSuccess {Object} data An empty object when the previous message was deleted
|
|
*
|
|
* @apiUse GroupNotFound
|
|
* @apiUse MessageNotFound
|
|
* @apiUse GroupIdRequired
|
|
* @apiUse ChatIdRequired
|
|
* @apiError (400) onlyCreatorOrAdminCanDeleteChat Only the creator of the message and admins
|
|
can delete a chat message.
|
|
*/
|
|
api.deleteChat = {
|
|
method: 'DELETE',
|
|
url: '/groups/:groupId/chat/:chatId',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { groupId } = req.params;
|
|
const { chatId } = req.params;
|
|
|
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const group = await Group.getGroup({ user, groupId, fields: 'chat' });
|
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
|
|
|
const message = await Chat.findOne({ _id: chatId }).exec();
|
|
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
|
|
|
|
if (user._id !== message.uuid && !user.hasPermission('moderator')) {
|
|
throw new NotAuthorized(res.t('onlyCreatorOrAdminCanDeleteChat'));
|
|
}
|
|
|
|
const chatRes = await Group.toJSONCleanChat(group, user);
|
|
const lastClientMsg = req.query.previousMsg;
|
|
const chatUpdated = !!(
|
|
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
|
|
);
|
|
|
|
await Chat.remove({ _id: message._id }).exec();
|
|
|
|
if (chatUpdated) {
|
|
removeFromArray(chatRes.chat, { id: chatId });
|
|
res.respond(200, chatRes.chat);
|
|
} else {
|
|
res.respond(200, {});
|
|
}
|
|
},
|
|
};
|
|
|
|
export default api;
|