mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
* begin moving to new fcm library * Add error handling * Add opening notification to correct screen * Fix tests and make async * lint fix * Rename pushNotificationstest..js to pushNotifications.test.js * fix(potions): remove Fungi Potion time banner * 5.24.3 * update(content): add 2024-06 content prebuild (#15231) * update sprites * add 2024-06 content * add 2024-06 enchanted armoire items * update sprites * update sprites * fix errors found in testing * Fix liveliness probes being rate limited (#15236) * Do not rate limit any liveliness probes * update example config * Translated using Weblate (German) Currently translated at 96.2% (181 of 188 strings) Translated using Weblate (Japanese) Currently translated at 99.4% (769 of 773 strings) Translated using Weblate (German) Currently translated at 93.6% (176 of 188 strings) Translated using Weblate (Japanese) Currently translated at 96.2% (2972 of 3089 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (232 of 232 strings) Translated using Weblate (Japanese) Currently translated at 96.8% (841 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (German) Currently translated at 86.7% (163 of 188 strings) Translated using Weblate (German) Currently translated at 85.1% (160 of 188 strings) Translated using Weblate (German) Currently translated at 84.0% (158 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (German) Currently translated at 83.5% (157 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (German) Currently translated at 82.9% (156 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (German) Currently translated at 81.9% (154 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 79.2% (149 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (189 of 189 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (German) Currently translated at 90.6% (2799 of 3089 strings) Translated using Weblate (German) Currently translated at 77.6% (146 of 188 strings) Translated using Weblate (German) Currently translated at 90.5% (2797 of 3089 strings) Translated using Weblate (German) Currently translated at 90.4% (2794 of 3089 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (German) Currently translated at 90.1% (2786 of 3089 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 77.1% (145 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.7% (763 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (German) Currently translated at 90.0% (2782 of 3089 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (773 of 773 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (378 of 378 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (239 of 239 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (French) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 75.0% (141 of 188 strings) Translated using Weblate (Spanish) Currently translated at 99.0% (766 of 773 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (189 of 189 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Japanese) Currently translated at 98.8% (764 of 773 strings) Translated using Weblate (Japanese) Currently translated at 99.6% (258 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (378 of 378 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (140 of 140 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 62.5% (1931 of 3089 strings) Translated using Weblate (German) Currently translated at 89.8% (2777 of 3089 strings) Translated using Weblate (French) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (French) Currently translated at 82.9% (156 of 188 strings) Translated using Weblate (German) Currently translated at 93.0% (241 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (427 of 427 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Japanese) Currently translated at 99.2% (257 of 259 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (German) Currently translated at 92.2% (239 of 259 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (239 of 239 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 91.8% (238 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (German) Currently translated at 90.3% (234 of 259 strings) Co-authored-by: Finrod <963505255@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Kem Kembo <medamamef@gmail.com> Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com> Co-authored-by: TOMA Mitsuru <toma0001@gmail.com> Co-authored-by: Tetiana <merekka13@gmail.com> Co-authored-by: Toro Mor <thomas.bizer@gmx.de> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/ Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/ Translation: Habitica/Achievements Translation: Habitica/Backgrounds Translation: Habitica/Character Translation: Habitica/Content Translation: Habitica/Faq Translation: Habitica/Gear Translation: Habitica/Generic Translation: Habitica/Groups Translation: Habitica/Inventory Translation: Habitica/Limited Translation: Habitica/Npc Translation: Habitica/Overview Translation: Habitica/Pets Translation: Habitica/Quests Translation: Habitica/Questscontent Translation: Habitica/Settings Translation: Habitica/Subscriber Translation: Habitica/Tasks * 5.25.0 * Fix dockerfile (#15241) * Fix issue with l4p not resetting properly (#15240) * actually clear out seeking field on user. Even when creating a party * Add tests to ensure party.seeking is cleared * fix(lint): don't assign unused const --------- Co-authored-by: Sabe Jones <sabe@habitica.com> --------- Co-authored-by: Sabe Jones <sabe@habitica.com> Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Finrod <963505255@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Kem Kembo <medamamef@gmail.com> Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com> Co-authored-by: TOMA Mitsuru <toma0001@gmail.com> Co-authored-by: Tetiana <merekka13@gmail.com> Co-authored-by: Toro Mor <thomas.bizer@gmx.de> Co-authored-by: Rafał Jagielski <jagielski.rafal.uwm@gmail.com>
269 lines
8.4 KiB
JavaScript
269 lines
8.4 KiB
JavaScript
import _ from 'lodash';
|
|
|
|
import { encrypt } from '../encryption';
|
|
import { sendNotification as sendPushNotification } from '../pushNotifications';
|
|
import {
|
|
NotFound,
|
|
BadRequest,
|
|
NotAuthorized,
|
|
} from '../errors';
|
|
import { sendTxn as sendTxnEmail } from '../email';
|
|
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
|
import {
|
|
model as User,
|
|
} from '../../models/user';
|
|
import {
|
|
model as Group,
|
|
} from '../../models/group';
|
|
|
|
async function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) {
|
|
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return;
|
|
|
|
const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
|
|
|
await sendPushNotification(
|
|
userToInvite,
|
|
{
|
|
title: group.name,
|
|
message: res.t(identifier, userToInvite.preferences.language),
|
|
identifier,
|
|
payload: { groupID: group._id, publicGuild },
|
|
},
|
|
);
|
|
}
|
|
|
|
function sendInviteEmail (userToInvite, groupLabel, group, inviter) {
|
|
if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] === false) return;
|
|
const groupTemplate = group.type === 'guild' ? 'guild' : 'party';
|
|
|
|
const emailVars = [
|
|
{ name: 'INVITER', content: inviter.profile.name },
|
|
];
|
|
|
|
if (group.type === 'guild') {
|
|
emailVars.push(
|
|
{ name: 'GUILD_NAME', content: group.name },
|
|
{ name: 'GUILD_URL', content: '/groups/discovery' },
|
|
);
|
|
} else {
|
|
emailVars.push(
|
|
{ name: 'PARTY_NAME', content: group.name },
|
|
{ name: 'PARTY_URL', content: '/party' },
|
|
);
|
|
}
|
|
|
|
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
|
|
}
|
|
|
|
function inviteUserToGuild (userToInvite, group, inviter, publicGuild, res) {
|
|
const uuid = userToInvite._id;
|
|
|
|
if (_.includes(userToInvite.guilds, group._id)) {
|
|
throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name }));
|
|
}
|
|
|
|
if (_.find(userToInvite.invitations.guilds, { id: group._id })) {
|
|
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name }));
|
|
}
|
|
|
|
const guildInvite = {
|
|
id: group._id,
|
|
name: group.name,
|
|
inviter: inviter._id,
|
|
publicGuild,
|
|
};
|
|
|
|
if (group.hasActiveGroupPlan() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
|
|
|
|
userToInvite.invitations.guilds.push(guildInvite);
|
|
}
|
|
|
|
async function inviteUserToParty (userToInvite, group, inviter, res) {
|
|
const uuid = userToInvite._id;
|
|
|
|
// Do not add to invitations.parties array if the user is already invited to that party
|
|
if (_.find(userToInvite.invitations.parties, { id: group._id })) {
|
|
throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name }));
|
|
}
|
|
|
|
if (userToInvite.party._id) {
|
|
const userParty = await Group.getGroup({ user: userToInvite, groupId: 'party', fields: '_id' });
|
|
|
|
if (userParty) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name }));
|
|
}
|
|
|
|
const partyInvite = { id: group._id, name: group.name, inviter: inviter._id };
|
|
if (group.hasActiveGroupPlan() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
|
|
|
|
userToInvite.invitations.parties.push(partyInvite);
|
|
userToInvite.invitations.party = partyInvite;
|
|
}
|
|
|
|
async function addInvitationToUser (userToInvite, group, inviter, res) {
|
|
const publicGuild = group.type === 'guild' && group.privacy === 'public';
|
|
|
|
if (group.type === 'guild') {
|
|
inviteUserToGuild(userToInvite, group, inviter, publicGuild, res);
|
|
} else if (group.type === 'party') {
|
|
await inviteUserToParty(userToInvite, group, inviter, res);
|
|
}
|
|
|
|
const groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
|
sendInviteEmail(userToInvite, groupLabel, group, inviter);
|
|
await sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res);
|
|
|
|
const userInvited = await userToInvite.save();
|
|
if (group.type === 'guild') {
|
|
return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1];
|
|
}
|
|
|
|
if (group.type === 'party') {
|
|
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
|
|
}
|
|
|
|
throw new Error('Invalid group type');
|
|
}
|
|
|
|
async function inviteByUUID (uuid, group, inviter, req, res) {
|
|
const userToInvite = await User.findById(uuid).exec();
|
|
|
|
if (!userToInvite) {
|
|
throw new NotFound(res.t('userWithIDNotFound', { userId: uuid }));
|
|
} else if (inviter._id === userToInvite._id) {
|
|
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
|
}
|
|
|
|
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
|
|
if (objections.length > 0) {
|
|
throw new NotAuthorized(res.t(
|
|
objections[0],
|
|
{ userId: uuid, username: userToInvite.profile.name },
|
|
));
|
|
}
|
|
|
|
const analyticsObject = {
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
uuid: inviter._id,
|
|
invitee: uuid,
|
|
groupId: group._id,
|
|
groupType: group.type,
|
|
headers: req.headers,
|
|
};
|
|
|
|
if (group.type === 'party') {
|
|
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
|
|
}
|
|
|
|
res.analytics.track('group invite', analyticsObject);
|
|
|
|
return addInvitationToUser(userToInvite, group, inviter, res);
|
|
}
|
|
|
|
async function inviteByEmail (invite, group, inviter, req, res) {
|
|
let userReturnInfo;
|
|
|
|
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
|
|
|
|
const userToContact = await User.findOne({
|
|
$or: [
|
|
{ 'auth.local.email': invite.email },
|
|
{ 'auth.facebook.emails.value': invite.email },
|
|
{ 'auth.google.emails.value': invite.email },
|
|
{ 'auth.apple.emails.value': invite.email },
|
|
],
|
|
})
|
|
.select({ _id: true, 'preferences.emailNotifications': true })
|
|
.exec();
|
|
|
|
if (userToContact) {
|
|
userReturnInfo = await inviteByUUID(userToContact._id, group, inviter, req, res);
|
|
} else {
|
|
userReturnInfo = invite.email;
|
|
|
|
const cancelledPlan = group.hasActiveGroupPlan() && !group.hasNotCancelled();
|
|
const groupQueryString = JSON.stringify({
|
|
id: group._id,
|
|
inviter: inviter._id,
|
|
publicGuild: group.type === 'guild' && group.privacy === 'public',
|
|
sentAt: Date.now(), // so we can let it expire
|
|
cancelledPlan,
|
|
});
|
|
const link = `/static/home?groupInvite=${encrypt(groupQueryString)}`;
|
|
|
|
const variables = [
|
|
{ name: 'LINK', content: link },
|
|
{ name: 'INVITER', content: req.body.inviter || inviter.profile.name },
|
|
];
|
|
|
|
if (group.type === 'guild') {
|
|
variables.push({ name: 'GUILD_NAME', content: group.name });
|
|
}
|
|
|
|
// Check for the email address not to be unsubscribed
|
|
const userIsUnsubscribed = await EmailUnsubscription.findOne({ email: invite.email }).exec();
|
|
const groupLabel = group.type === 'guild' ? '-guild' : '';
|
|
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
|
|
|
|
const analyticsObject = {
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
uuid: inviter._id,
|
|
invitee: 'email',
|
|
groupId: group._id,
|
|
groupType: group.type,
|
|
headers: req.headers,
|
|
};
|
|
|
|
res.analytics.track('group invite', analyticsObject);
|
|
}
|
|
|
|
return userReturnInfo;
|
|
}
|
|
|
|
async function inviteByUserName (username, group, inviter, req, res) {
|
|
if (username.indexOf('@') === 0) username = username.slice(1, username.length); // eslint-disable-line no-param-reassign
|
|
username = username.toLowerCase(); // eslint-disable-line no-param-reassign
|
|
const userToInvite = await User.findOne({ 'auth.local.lowerCaseUsername': username }).exec();
|
|
|
|
if (!userToInvite) {
|
|
throw new NotFound(res.t('userWithUsernameNotFound', { username }));
|
|
}
|
|
|
|
if (inviter._id === userToInvite._id) {
|
|
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
|
}
|
|
|
|
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
|
|
if (objections.length > 0) {
|
|
throw new NotAuthorized(res.t(
|
|
objections[0],
|
|
{ userId: userToInvite._id, username: userToInvite.profile.name },
|
|
));
|
|
}
|
|
|
|
const analyticsObject = {
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
uuid: inviter._id,
|
|
invitee: userToInvite._id,
|
|
groupId: group._id,
|
|
groupType: group.type,
|
|
headers: req.headers,
|
|
};
|
|
|
|
if (group.type === 'party') {
|
|
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
|
|
}
|
|
|
|
res.analytics.track('group invite', analyticsObject);
|
|
|
|
return addInvitationToUser(userToInvite, group, inviter, res);
|
|
}
|
|
|
|
export {
|
|
inviteByUUID,
|
|
inviteByEmail,
|
|
inviteByUserName,
|
|
};
|