Update to new method for fcm (#15238)

* 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>
This commit is contained in:
Phillip Thelen
2024-06-11 20:19:03 +02:00
committed by GitHub
parent 5719e5e996
commit 98f9d2a8f4
19 changed files with 1608 additions and 360 deletions

1123
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
"express": "^4.19.2", "express": "^4.19.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
"glob": "^8.1.0", "glob": "^8.1.0",
"got": "^11.8.6", "got": "^11.8.6",
"gulp": "^4.0.0", "gulp": "^4.0.0",

View File

@@ -1,184 +0,0 @@
import apn from '@parse/node-apn/mock';
import _ from 'lodash';
import nconf from 'nconf';
import gcmLib from 'node-gcm'; // works with FCM notifications too
import { model as User } from '../../../../website/server/models/user';
import {
sendNotification as sendPushNotification,
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.spy();
apnSendSpy = sinon.spy();
sandbox.stub(nconf, 'get').returns('true-key');
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(apn.Provider.prototype, 'send').returns({
on: () => null,
send: apnSendSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
// TODO disabled because APN relies on a Promise
xit('uses APN for iOS devices', () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
const expectedNotification = new apn.Notification({
alert: message,
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
});

View File

@@ -0,0 +1,354 @@
import apn from '@parse/node-apn';
import _ from 'lodash';
import nconf from 'nconf';
import admin from 'firebase-admin';
import { model as User } from '../../../../website/server/models/user';
import {
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
let sendPushNotification;
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
let updateStub;
let classStubbedInstance;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.stub().returns(Promise.resolve('success'));
apnSendSpy = sinon.stub().returns(Promise.resolve());
nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true');
classStubbedInstance = sandbox.createStubInstance(apn.Provider, {
send: apnSendSpy,
});
sandbox.stub(apn, 'Provider').returns(classStubbedInstance);
delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')];
// eslint-disable-next-line global-require
sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification;
updateStub = sandbox.stub(User, 'updateOne').resolves();
sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy }));
});
afterEach(() => {
sandbox.restore();
});
describe('validates supplied data', () => {
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
describe('sends notifications', () => {
let details;
beforeEach(() => {
details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
});
it('uses APN for iOS devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const expectedNotification = new apn.Notification({
alert: {
title,
body: message,
},
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
await sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
it('uses FCM for Android devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const expectedMessage = {
notification: {
title,
body: message,
},
data: {
identifier,
notificationIdentifier: identifier,
},
token: '123',
};
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledOnce;
expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage);
expect(apnSendSpy).to.not.have.been.called;
});
it('handles multiple devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
user.pushDevices.push({
type: 'ios',
regId: '456',
});
user.pushDevices.push({
type: 'android',
regId: '789',
});
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledTwice;
expect(apnSendSpy).to.have.been.calledOnce;
});
});
describe('handles sending errors', () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('removes unregistered fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
await clock.tick(10);
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
expect(updateStub).to.have.been.calledOnce;
});
it('removes unregistered apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'Unregistered' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'BadDeviceToken' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
});
});

View File

@@ -1362,8 +1362,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'updateMany'); sandbox.spy(User, 'updateMany');
}); });
it('formats message', () => { it('formats message', async () => {
const chatMessage = party.sendChat({ const chatMessage = await party.sendChat({
message: 'a _new_ message with *markdown*', message: 'a _new_ message with *markdown*',
user: { user: {
_id: 'user-id', _id: 'user-id',
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
expect(chat.user).to.eql('user name'); expect(chat.user).to.eql('user name');
}); });
it('formats message as system if no user is passed in', () => { it('formats message as system if no user is passed in', async () => {
const chat = party.sendChat({ message: 'a system message' }); const chat = await party.sendChat({ message: 'a system message' });
expect(chat.text).to.eql('a system message'); expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true); expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist; expect(chat.user).to.not.exist;
}); });
it('updates users about new messages in party', () => { it('updates users about new messages in party', async () => {
party.sendChat({ message: 'message' }); await party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.updateMany).to.be.calledWithMatch({
@@ -1421,12 +1421,12 @@ describe('Group Model', () => {
}); });
}); });
it('updates users about new messages in group', () => { it('updates users about new messages in group', async () => {
const group = new Group({ const group = new Group({
type: 'guild', type: 'guild',
}); });
group.sendChat({ message: 'message' }); await group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.updateMany).to.be.calledWithMatch({
@@ -1435,8 +1435,8 @@ describe('Group Model', () => {
}); });
}); });
it('does not send update to user that sent the message', () => { it('does not send update to user that sent the message', async () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } }); await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.updateMany).to.be.calledWithMatch({
@@ -1445,18 +1445,18 @@ describe('Group Model', () => {
}); });
}); });
it('skips sending new message notification for guilds with > 5000 members', () => { it('skips sending new message notification for guilds with > 5000 members', async () => {
party.memberCount = 5001; party.memberCount = 5001;
party.sendChat({ message: 'message' }); await party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.updateMany).to.not.be.called;
}); });
it('skips sending messages to the tavern', () => { it('skips sending messages to the tavern', async () => {
party._id = TAVERN_ID; party._id = TAVERN_ID;
party.sendChat({ message: 'message' }); await party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.updateMany).to.not.be.called;
}); });
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
await guild.save(); await guild.save();
const groupMessage = guild.sendChat({ message: 'Test message.' }); const groupMessage = await guild.sendChat({ message: 'Test message.' });
await groupMessage.save(); await groupMessage.save();
await sleep(); await sleep();

View File

@@ -214,7 +214,7 @@ api.postChat = {
}); });
} }
const newChatMessage = group.sendChat({ const newChatMessage = await group.sendChat({
message, message,
user, user,
flagCount, flagCount,

View File

@@ -756,7 +756,7 @@ api.transferGems = {
]); ]);
} }
if (receiver.preferences.pushNotifications.giftedGems !== false) { if (receiver.preferences.pushNotifications.giftedGems !== false) {
sendPushNotification( await sendPushNotification(
receiver, receiver,
{ {
title: res.t('giftedGems', receiverLang), title: res.t('giftedGems', receiverLang),

View File

@@ -120,10 +120,10 @@ api.inviteToQuest = {
// send out invites // send out invites
const inviterVars = getUserInfo(user, ['name', 'email']); const inviterVars = getUserInfo(user, ['name', 'email']);
const membersToEmail = members.filter(member => { const membersToEmail = members.filter(async member => {
// send push notifications while filtering members before sending emails // send push notifications while filtering members before sending emails
if (member.preferences.pushNotifications.invitedQuest !== false) { if (member.preferences.pushNotifications.invitedQuest !== false) {
sendPushNotification( await sendPushNotification(
member, member,
{ {
title: quest.text(member.preferences.language), title: quest.text(member.preferences.language),
@@ -394,7 +394,7 @@ api.cancelQuest = {
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest')); if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
const questName = questScrolls[group.quest.key].text('en'); const questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat({ const newChatMessage = await group.sendChat({
message: `\`${user.profile.name} cancelled the party quest ${questName}.\``, message: `\`${user.profile.name} cancelled the party quest ${questName}.\``,
info: { info: {
type: 'quest_cancel', type: 'quest_cancel',
@@ -456,7 +456,7 @@ api.abortQuest = {
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest')); if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
const questName = questScrolls[group.quest.key].text('en'); const questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat({ const newChatMessage = await group.sendChat({
message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``, message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``,
info: { info: {
type: 'quest_abort', type: 'quest_abort',

View File

@@ -25,7 +25,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username') .select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
.exec(); .exec();
members.forEach(member => { members.forEach(async member => {
if (member.preferences.pushNotifications.partyActivity !== false) { if (member.preferences.pushNotifications.partyActivity !== false) {
if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) { if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) {
return; return;
@@ -33,7 +33,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
if (!message.unformattedText) return; if (!message.unformattedText) return;
sendPushNotification( await sendPushNotification(
member, member,
{ {
title: translate('groupActivityNotificationTitle', { user: message.user, group: group.name }, member.preferences.language), title: translate('groupActivityNotificationTitle', { user: message.user, group: group.name }, member.preferences.language),

View File

@@ -13,7 +13,7 @@ export async function sentMessage (sender, receiver, message, translate) {
} }
if (receiver.preferences.pushNotifications.newPM !== false && messageSent.unformattedText) { if (receiver.preferences.pushNotifications.newPM !== false && messageSent.unformattedText) {
sendPushNotification( await sendPushNotification(
receiver, receiver,
{ {
title: translate( title: translate(

View File

@@ -16,12 +16,12 @@ import {
model as Group, model as Group,
} from '../../models/group'; } from '../../models/group';
function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) { async function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) {
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return; if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return;
const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty'; const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
sendPushNotification( await sendPushNotification(
userToInvite, userToInvite,
{ {
title: group.name, title: group.name,
@@ -110,7 +110,7 @@ async function addInvitationToUser (userToInvite, group, inviter, res) {
const groupLabel = group.type === 'guild' ? 'Guild' : 'Party'; const groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
sendInviteEmail(userToInvite, groupLabel, group, inviter); sendInviteEmail(userToInvite, groupLabel, group, inviter);
sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res); await sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res);
const userInvited = await userToInvite.save(); const userInvited = await userToInvite.save();
if (group.type === 'guild') { if (group.type === 'guild') {

View File

@@ -50,7 +50,7 @@ async function buyGemGift (data) {
data.gift.member._id !== data.user._id data.gift.member._id !== data.user._id
&& data.gift.member.preferences.pushNotifications.giftedGems !== false && data.gift.member.preferences.pushNotifications.giftedGems !== false
) { ) {
sendPushNotification( await sendPushNotification(
data.gift.member, data.gift.member,
{ {
title: shared.i18n.t('giftedGems', languages[1]), title: shared.i18n.t('giftedGems', languages[1]),

View File

@@ -367,7 +367,7 @@ async function createSubscription (data) {
} }
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) { if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
sendPushNotification( await sendPushNotification(
data.gift.member, data.gift.member,
{ {
title: shared.i18n.t('giftedSubscription', languages[1]), title: shared.i18n.t('giftedSubscription', languages[1]),

View File

@@ -1,15 +1,12 @@
import _ from 'lodash'; import _ from 'lodash';
import nconf from 'nconf'; import nconf from 'nconf';
import apn from '@parse/node-apn'; import apn from '@parse/node-apn';
import gcmLib from 'node-gcm'; // works with FCM notifications too import admin from 'firebase-admin';
import logger from './logger'; import logger from './logger';
import { // eslint-disable-line import/no-cycle import { // eslint-disable-line import/no-cycle
model as User, model as User,
} from '../models/user'; } from '../models/user';
const FCM_API_KEY = nconf.get('PUSH_CONFIGS_FCM_SERVER_API_KEY');
const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined;
const APN_ENABLED = nconf.get('PUSH_CONFIGS_APN_ENABLED') === 'true'; const APN_ENABLED = nconf.get('PUSH_CONFIGS_APN_ENABLED') === 'true';
const apnProvider = APN_ENABLED ? new apn.Provider({ const apnProvider = APN_ENABLED ? new apn.Provider({
token: { token: {
@@ -30,7 +27,91 @@ function removePushDevice (user, pushDevice) {
export const MAX_MESSAGE_LENGTH = 300; export const MAX_MESSAGE_LENGTH = 300;
export function sendNotification (user, details = {}) { async function sendFCMNotification (user, pushDevice, payload) {
const messaging = admin.messaging();
if (messaging === undefined) {
return;
}
const message = {
notification: {
title: payload.title,
body: payload.body,
},
data: {
identifier: payload.identifier,
notificationIdentifier: payload.identifier,
},
token: pushDevice.regId,
};
try {
await messaging.send(message);
} catch (error) {
if (error.code === 'messaging/registration-token-not-registered') {
removePushDevice(user, pushDevice);
logger.error(new Error('FCM error, unregistered pushDevice'), {
regId: pushDevice.regId, userId: user._id,
});
} else if (error.code === 'messaging/invalid-registration-token') {
removePushDevice(user, pushDevice);
logger.error(new Error('FCM error, invalid pushDevice'), {
regId: pushDevice.regId, userId: user._id,
});
} else {
logger.error(error, 'Unhandled FCM error.');
}
}
}
async function sendAPNNotification (user, pushDevice, details, payload) {
if (apnProvider) {
const notification = new apn.Notification({
alert: {
title: details.title,
body: details.message,
},
sound: 'default',
category: details.category,
topic: 'com.habitrpg.ios.Habitica',
payload,
});
try {
const response = await apnProvider.send(notification, pushDevice.regId);
// Handle failed push notifications deliveries
response.failed.forEach(failure => {
if (failure.error) { // generic error
logger.error(new Error('Unhandled APN error'), {
response, regId: pushDevice.regId, userId: user._id,
});
} else { // rejected
// see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17
// for a list of rejection reasons
const { reason } = failure.response;
if (reason === 'Unregistered') {
removePushDevice(user, pushDevice);
logger.error(new Error('APN error, unregistered pushDevice'), {
regId: pushDevice.regId, userId: user._id,
});
} else {
if (reason === 'BadDeviceToken') {
// An invalid token was registered by mistake
// Remove it but log the error differently so that it can be distinguished
// from when reason === Unregistered
removePushDevice(user, pushDevice);
}
logger.error(new Error('APN error'), {
response, regId: pushDevice.regId, userId: user._id,
});
}
}
});
} catch (err) {
logger.error(err, 'Unhandled APN error.');
}
}
}
export async function sendNotification (user, details = {}) {
if (!user) throw new Error('User is required.'); if (!user) throw new Error('User is required.');
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return; if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
const pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices; const pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
@@ -51,110 +132,16 @@ export function sendNotification (user, details = {}) {
payload.message = _.truncate(payload.message, { length: MAX_MESSAGE_LENGTH }); payload.message = _.truncate(payload.message, { length: MAX_MESSAGE_LENGTH });
} }
_.each(pushDevices, pushDevice => { await _.each(pushDevices, async pushDevice => {
switch (pushDevice.type) { // eslint-disable-line default-case switch (pushDevice.type) { // eslint-disable-line default-case
case 'android': case 'android':
// Required for fcm to be received in background // Required for fcm to be received in background
payload.title = details.title; payload.title = details.title;
payload.body = details.message; payload.body = details.message;
await sendFCMNotification(user, pushDevice, payload);
if (fcmSender) {
const message = new gcmLib.Message({
data: payload,
});
fcmSender.send(message, {
registrationTokens: [pushDevice.regId],
}, 5, (err, response) => {
if (err) {
logger.error(err, 'Unhandled FCM error.');
return;
}
// Handle failed push notifications deliveries
// Note that we're always sending to one device, not multiple
const failed = response
&& response.results && response.results[0] && response.results[0].error;
if (failed) {
// See https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9
// for the list of errors
// The regId is not valid anymore, remove it
if (failed === 'NotRegistered') {
removePushDevice(user, pushDevice);
logger.error(new Error('FCM error, unregistered pushDevice'), {
regId: pushDevice.regId, userId: user._id,
});
} else {
// An invalid token was registered by mistake
// Remove it but log the error differently so that it can be distinguished
// from when failed === NotRegistered
// Blacklisted can happen in some rare cases,
// see https://stackoverflow.com/questions/42136122/why-does-firebase-push-token-return-blacklisted
// MismatchSenderId could be due to old tokens,
// see https://stackoverflow.com/questions/11313342/why-do-i-get-mismatchsenderid-from-gcm-server-side
if (
failed === 'InvalidRegistration'
|| failed === 'MismatchSenderId'
|| pushDevice.regId === 'BLACKLISTED'
) {
removePushDevice(user, pushDevice);
}
logger.error(new Error('FCM error'), {
response, regId: pushDevice.regId, userId: user._id,
});
}
}
});
}
break; break;
case 'ios': case 'ios':
if (apnProvider) { sendAPNNotification(user, pushDevice, details, payload);
const notification = new apn.Notification({
alert: {
title: details.title,
body: details.message,
},
sound: 'default',
category: details.category,
topic: 'com.habitrpg.ios.Habitica',
payload,
});
apnProvider.send(notification, pushDevice.regId)
.then(response => {
// Handle failed push notifications deliveries
response.failed.forEach(failure => {
if (failure.error) { // generic error
logger.error(new Error('Unhandled APN error'), {
response, regId: pushDevice.regId, userId: user._id,
});
} else { // rejected
// see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17
// for a list of rejection reasons
const { reason } = failure.response;
if (reason === 'Unregistered') {
removePushDevice(user, pushDevice);
logger.error(new Error('APN error, unregistered pushDevice'), {
regId: pushDevice.regId, userId: user._id,
});
} else {
if (reason === 'BadDeviceToken') {
// An invalid token was registered by mistake
// Remove it but log the error differently so that it can be distinguished
// from when reason === Unregistered
removePushDevice(user, pushDevice);
}
logger.error(new Error('APN error'), {
response, regId: pushDevice.regId, userId: user._id,
});
}
}
});
})
.catch(err => logger.error(err, 'Unhandled APN error.'));
}
break; break;
} }
}); });

View File

@@ -0,0 +1,14 @@
import admin from 'firebase-admin';
import nconf from 'nconf';
if (nconf.get('FIREBASE_PROJECT_ID') !== undefined && nconf.get('FIREBASE_PROJECT_ID') !== '') {
if (!global.firebaseApp) {
global.firebaseApp = admin.initializeApp({
credential: admin.credential.cert({
projectId: nconf.get('FIREBASE_PROJECT_ID'),
clientEmail: nconf.get('FIREBASE_CLIENT_EMAIL'),
privateKey: nconf.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'),
}),
});
}
}

View File

@@ -254,7 +254,7 @@ async function castSpell (req, res, { isV3 = false }) {
if (lastMessage && lastMessage.info.spell === spellId if (lastMessage && lastMessage.info.spell === spellId
&& lastMessage.info.user === user.profile.name && lastMessage.info.user === user.profile.name
&& lastMessage.info.target === partyMembers.profile.name) { && lastMessage.info.target === partyMembers.profile.name) {
const newChatMessage = party.sendChat({ const newChatMessage = await party.sendChat({
message: `\`${common.i18n.t('chatCastSpellUserTimes', { message: `\`${common.i18n.t('chatCastSpellUserTimes', {
username: user.profile.name, username: user.profile.name,
spell: spell.text(), spell: spell.text(),
@@ -273,7 +273,7 @@ async function castSpell (req, res, { isV3 = false }) {
await newChatMessage.save(); await newChatMessage.save();
await lastMessage.deleteOne(); await lastMessage.deleteOne();
} else { // Single target spell, not repeated } else { // Single target spell, not repeated
const newChatMessage = party.sendChat({ const newChatMessage = await party.sendChat({
message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``, message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``,
info: { info: {
type: 'spell_cast_user', type: 'spell_cast_user',
@@ -288,7 +288,7 @@ async function castSpell (req, res, { isV3 = false }) {
} }
} else if (lastMessage && lastMessage.info.spell === spellId // Party spell, check for repeat } else if (lastMessage && lastMessage.info.spell === spellId // Party spell, check for repeat
&& lastMessage.info.user === user.profile.name) { && lastMessage.info.user === user.profile.name) {
const newChatMessage = party.sendChat({ const newChatMessage = await party.sendChat({
message: `\`${common.i18n.t('chatCastSpellPartyTimes', { message: `\`${common.i18n.t('chatCastSpellPartyTimes', {
username: user.profile.name, username: user.profile.name,
spell: spell.text(), spell: spell.text(),
@@ -305,7 +305,7 @@ async function castSpell (req, res, { isV3 = false }) {
await newChatMessage.save(); await newChatMessage.save();
await lastMessage.deleteOne(); await lastMessage.deleteOne();
} else { } else {
const newChatMessage = party.sendChat({ // Non-repetitive partywide spell const newChatMessage = await party.sendChat({ // Non-repetitive partywide spell
message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``, message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``,
info: { info: {
type: 'spell_cast_party', type: 'spell_cast_party',

View File

@@ -403,7 +403,7 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
]); ]);
} }
if (savedWinner.preferences.pushNotifications.wonChallenge !== false) { if (savedWinner.preferences.pushNotifications.wonChallenge !== false) {
sendPushNotification( await sendPushNotification(
savedWinner, savedWinner,
{ {
title: challenge.name, title: challenge.name,

View File

@@ -526,7 +526,7 @@ schema.methods.getMemberCount = async function getMemberCount () {
return User.countDocuments(query).exec(); return User.countDocuments(query).exec();
}; };
schema.methods.sendChat = function sendChat (options = {}) { schema.methods.sendChat = async function sendChat (options = {}) {
const { const {
message, user, metaData, message, user, metaData,
client, flagCount = 0, info = {}, client, flagCount = 0, info = {},
@@ -596,7 +596,7 @@ schema.methods.sendChat = function sendChat (options = {}) {
sendChatPushNotifications(user, this, newChatMessage, mentions, translate); sendChatPushNotifications(user, this, newChatMessage, mentions, translate);
} }
if (mentionedMembers) { if (mentionedMembers) {
mentionedMembers.forEach(member => { await mentionedMembers.forEach(async member => {
if (member._id === user._id) return; if (member._id === user._id) return;
const pushNotifPrefs = member.preferences.pushNotifications; const pushNotifPrefs = member.preferences.pushNotifications;
if (this.type === 'party') { if (this.type === 'party') {
@@ -617,7 +617,7 @@ schema.methods.sendChat = function sendChat (options = {}) {
} }
if (newChatMessage.unformattedText) { if (newChatMessage.unformattedText) {
sendPushNotification(member, { await sendPushNotification(member, {
identifier: 'chatMention', identifier: 'chatMention',
title: `${user.profile.name} mentioned you in ${this.name}`, title: `${user.profile.name} mentioned you in ${this.name}`,
message: newChatMessage.unformattedText, message: newChatMessage.unformattedText,
@@ -751,7 +751,7 @@ schema.methods.startQuest = async function startQuest (user) {
_id: { $in: nonMembers }, _id: { $in: nonMembers },
}, _cleanQuestParty()).exec(); }, _cleanQuestParty()).exec();
const newMessage = this.sendChat({ const newMessage = await this.sendChat({
message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``, message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``,
metaData: { metaData: {
participatingMembers: this.getParticipatingQuestMembers().join(', '), participatingMembers: this.getParticipatingQuestMembers().join(', '),
@@ -766,7 +766,7 @@ schema.methods.startQuest = async function startQuest (user) {
const membersToEmail = []; const membersToEmail = [];
// send notifications and webhooks in the background without blocking // send notifications and webhooks in the background without blocking
members.forEach(member => { await members.forEach(async member => {
if (member._id !== user._id) { if (member._id !== user._id) {
// send push notifications and filter users that disabled emails // send push notifications and filter users that disabled emails
if (member.preferences.emailNotifications.questStarted !== false) { if (member.preferences.emailNotifications.questStarted !== false) {
@@ -776,7 +776,7 @@ schema.methods.startQuest = async function startQuest (user) {
// send push notifications and filter users that disabled emails // send push notifications and filter users that disabled emails
if (member.preferences.pushNotifications.questStarted !== false) { if (member.preferences.pushNotifications.questStarted !== false) {
const memberLang = member.preferences.language; const memberLang = member.preferences.language;
sendPushNotification(member, { await sendPushNotification(member, {
title: quest.text(memberLang), title: quest.text(memberLang),
message: shared.i18n.t('questStarted', memberLang), message: shared.i18n.t('questStarted', memberLang),
identifier: 'questStarted', identifier: 'questStarted',
@@ -1021,7 +1021,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
group.quest.progress.hp -= progress.up; group.quest.progress.hp -= progress.up;
if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) { if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) {
const groupMessage = group.sendChat({ const groupMessage = await group.sendChat({
message: `\`${shared.i18n.t('chatBossDontAttack', { bossName: quest.boss.name('en') }, 'en')}\``, message: `\`${shared.i18n.t('chatBossDontAttack', { bossName: quest.boss.name('en') }, 'en')}\``,
info: { info: {
type: 'boss_dont_attack', type: 'boss_dont_attack',
@@ -1032,7 +1032,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
}); });
promises.push(groupMessage.save()); promises.push(groupMessage.save());
} else { } else {
const groupMessage = group.sendChat({ const groupMessage = await group.sendChat({
message: `\`${shared.i18n.t('chatBossDamage', { message: `\`${shared.i18n.t('chatBossDamage', {
username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1), username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1),
}, user.preferences.language)}\``, }, user.preferences.language)}\``,
@@ -1051,7 +1051,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
if (quest.boss.rage) { if (quest.boss.rage) {
group.quest.progress.rage += Math.abs(down); group.quest.progress.rage += Math.abs(down);
if (group.quest.progress.rage >= quest.boss.rage.value) { if (group.quest.progress.rage >= quest.boss.rage.value) {
const rageMessage = group.sendChat({ const rageMessage = await group.sendChat({
message: quest.boss.rage.effect('en'), message: quest.boss.rage.effect('en'),
info: { info: {
type: 'boss_rage', type: 'boss_rage',
@@ -1094,7 +1094,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
// Boss slain, finish quest // Boss slain, finish quest
if (group.quest.progress.hp <= 0) { if (group.quest.progress.hp <= 0) {
const questFinishChat = group.sendChat({ const questFinishChat = await group.sendChat({
message: `\`${shared.i18n.t('chatBossDefeated', { bossName: quest.boss.name('en') }, 'en')}\``, message: `\`${shared.i18n.t('chatBossDefeated', { bossName: quest.boss.name('en') }, 'en')}\``,
info: { info: {
type: 'boss_defeated', type: 'boss_defeated',
@@ -1148,7 +1148,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
}, []); }, []);
foundText = foundText.join(', '); foundText = foundText.join(', ');
const foundChat = group.sendChat({ const foundChat = await group.sendChat({
message: `\`${shared.i18n.t('chatFindItems', { username: user.profile.name, items: foundText }, 'en')}\``, message: `\`${shared.i18n.t('chatFindItems', { username: user.profile.name, items: foundText }, 'en')}\``,
info: { info: {
type: 'user_found_items', type: 'user_found_items',
@@ -1164,7 +1164,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
const questFinished = collectedItems.length === remainingItems.length; const questFinished = collectedItems.length === remainingItems.length;
if (questFinished) { if (questFinished) {
await group.finishQuest(quest); await group.finishQuest(quest);
const allItemsFoundChat = group.sendChat({ const allItemsFoundChat = await group.sendChat({
message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``, message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``,
info: { info: {
type: 'all_items_found', type: 'all_items_found',
@@ -1236,7 +1236,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
const chatPromises = []; const chatPromises = [];
if (tavern.quest.progress.hp <= 0) { if (tavern.quest.progress.hp <= 0) {
const completeChat = tavern.sendChat({ const completeChat = await tavern.sendChat({
message: quest.completionChat('en'), message: quest.completionChat('en'),
info: { info: {
type: 'tavern_quest_completed', type: 'tavern_quest_completed',
@@ -1273,7 +1273,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
} }
if (!scene) { if (!scene) {
const tiredChat = tavern.sendChat({ const tiredChat = await tavern.sendChat({
message: `\`${shared.i18n.t('tavernBossTired', { rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en') }, 'en')}\``, message: `\`${shared.i18n.t('tavernBossTired', { rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en') }, 'en')}\``,
info: { info: {
type: 'tavern_boss_rage_tired', type: 'tavern_boss_rage_tired',
@@ -1283,7 +1283,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
chatPromises.push(tiredChat.save()); chatPromises.push(tiredChat.save());
tavern.quest.progress.rage = 0; // quest.boss.rage.value; tavern.quest.progress.rage = 0; // quest.boss.rage.value;
} else { } else {
const rageChat = tavern.sendChat({ const rageChat = await tavern.sendChat({
message: quest.boss.rage[scene]('en'), message: quest.boss.rage[scene]('en'),
info: { info: {
type: 'tavern_boss_rage', type: 'tavern_boss_rage',
@@ -1306,7 +1306,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
&& tavern.quest.progress.hp < quest.boss.desperation.threshold && tavern.quest.progress.hp < quest.boss.desperation.threshold
&& !tavern.quest.extra.desperate && !tavern.quest.extra.desperate
) { ) {
const progressChat = tavern.sendChat({ const progressChat = await tavern.sendChat({
message: quest.boss.desperation.text('en'), message: quest.boss.desperation.text('en'),
info: { info: {
type: 'tavern_boss_desperation', type: 'tavern_boss_desperation',

View File

@@ -12,6 +12,7 @@ import attachMiddlewares from './middlewares/index';
// Load config files // Load config files
import './libs/setupMongoose'; import './libs/setupMongoose';
import './libs/setupPassport'; import './libs/setupPassport';
import './libs/setupFirebase';
// Load some schemas & models // Load some schemas & models
import './models/challenge'; import './models/challenge';