mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Real-time Chat (#7664)
* feat(realtime-chat): add Pusher library to the server * feat(realtime-chat): only for private groups * feat(realtime-chat): add authentication endpoint for Pusher * feat(realtime-chat): client proof of concept * fix typo in apidoc * feat(realtime-chat): redo authentication and write integration tests * remove firebase code * fix client side tests * fix line ending in bower.json * feat(realtime chat): use presence channels for parties, send events & disconnect clients if user leaves or is removed from party, automatically update UI * pusher: enable all events in the background * fix pusher integration tests
This commit is contained in:
@@ -41,7 +41,9 @@
|
|||||||
"sticky": "1.0.3",
|
"sticky": "1.0.3",
|
||||||
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
|
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
|
||||||
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
|
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
|
||||||
"habitica-markdown": "1.2.2"
|
"habitica-markdown": "1.2.2",
|
||||||
|
"pusher-js-auth": "^2.0.0",
|
||||||
|
"pusher-websocket-iso": "pusher#^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular-mocks": "1.3.9"
|
"angular-mocks": "1.3.9"
|
||||||
|
|||||||
@@ -67,9 +67,10 @@
|
|||||||
"GCM_SERVER_API_KEY": "",
|
"GCM_SERVER_API_KEY": "",
|
||||||
"APN_ENABLED": "true"
|
"APN_ENABLED": "true"
|
||||||
},
|
},
|
||||||
"FIREBASE": {
|
"PUSHER": {
|
||||||
"APP": "app-name",
|
"ENABLED": "false",
|
||||||
"SECRET": "secret",
|
"APP_ID": "appId",
|
||||||
"ENABLED": "false"
|
"KEY": "key",
|
||||||
|
"SECRET": "secret"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
"express": "~4.13.3",
|
"express": "~4.13.3",
|
||||||
"express-csv": "~0.6.0",
|
"express-csv": "~0.6.0",
|
||||||
"express-validator": "^2.18.0",
|
"express-validator": "^2.18.0",
|
||||||
"firebase": "^2.2.9",
|
|
||||||
"firebase-token-generator": "^2.0.0",
|
|
||||||
"glob": "^4.3.5",
|
"glob": "^4.3.5",
|
||||||
"got": "^6.1.1",
|
"got": "^6.1.1",
|
||||||
"grunt": "~0.4.1",
|
"grunt": "~0.4.1",
|
||||||
@@ -80,6 +78,7 @@
|
|||||||
"pretty-data": "^0.40.0",
|
"pretty-data": "^0.40.0",
|
||||||
"ps-tree": "^1.0.0",
|
"ps-tree": "^1.0.0",
|
||||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||||
|
"pusher": "^1.3.0",
|
||||||
"request": "~2.72.0",
|
"request": "~2.72.0",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"run-sequence": "^1.1.4",
|
"run-sequence": "^1.1.4",
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../../../../helpers/api-integration/v3';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
describe('POST /user/auth/firebase', () => {
|
|
||||||
let user;
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
user = await generateUser();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a Firebase token', async () => {
|
|
||||||
let {token, expires} = await user.post('/user/auth/firebase');
|
|
||||||
expect(moment(expires).isValid()).to.be.true;
|
|
||||||
expect(token).to.be.a('string');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
102
test/api/v3/integration/user/auth/POST-user_auth_pusher.test.js
Normal file
102
test/api/v3/integration/user/auth/POST-user_auth_pusher.test.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
requester,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../../helpers/api-integration/v3';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
describe('POST /user/auth/pusher', () => {
|
||||||
|
let user;
|
||||||
|
let endpoint = '/user/auth/pusher';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires authentication', async () => {
|
||||||
|
let api = requester();
|
||||||
|
|
||||||
|
await expect(api.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('missingAuthHeaders'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if req.body.socket_id is missing', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if req.body.channel_name is missing', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if req.body.channel_name is badly formatted', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: '123',
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid Pusher channel type.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if an invalid channel type is passed', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: 'invalid-group-123',
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid Pusher channel type.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if an invalid resource type is passed', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: 'presence-user-123',
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid Pusher resource type.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if an invalid resource id is passed', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: 'presence-group-123',
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid Pusher resource id, must be a UUID.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if the passed resource id doesn\'t match the user\'s party', async () => {
|
||||||
|
await expect(user.post(endpoint, {
|
||||||
|
channel_name: `presence-group-${generateUUID()}`,
|
||||||
|
socket_id: '123',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Resource id must be the user\'s party.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -29,13 +29,14 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
|
|||||||
$scope.postChat = function(group, message){
|
$scope.postChat = function(group, message){
|
||||||
if (_.isEmpty(message) || $scope._sending) return;
|
if (_.isEmpty(message) || $scope._sending) return;
|
||||||
$scope._sending = true;
|
$scope._sending = true;
|
||||||
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
|
// var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
|
||||||
Chat.postChat(group._id, message, previousMsg)
|
Chat.postChat(group._id, message) //, previousMsg) not sending the previousMsg as we have real time updates
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
var message = response.data.data.message;
|
var message = response.data.data.message;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
group.chat.unshift(message);
|
group.chat.unshift(message);
|
||||||
|
group.chat.splice(200);
|
||||||
} else {
|
} else {
|
||||||
group.chat = response.data.data.chat;
|
group.chat = response.data.data.chat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
|
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics', 'Pusher',
|
||||||
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
|
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics, Pusher) {
|
||||||
$scope.groups = {
|
$scope.groups = {
|
||||||
guilds: [],
|
guilds: [],
|
||||||
public: [],
|
public: [],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social',
|
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social', 'Pusher',
|
||||||
function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social) {
|
function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Pusher) {
|
||||||
|
|
||||||
var user = User.user;
|
var user = User.user;
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
|||||||
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrl, Payments, $sce, $window, Analytics, TAVERN_ID) {
|
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrl, Payments, $sce, $window, Analytics, TAVERN_ID) {
|
||||||
var user = User.user;
|
var user = User.user;
|
||||||
|
|
||||||
var initSticky = _.once(function(){
|
// Setup page once user is synced
|
||||||
$timeout(function () {
|
var clearAppLoadedListener = $rootScope.$watch('appLoaded', function (after) {
|
||||||
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
|
if (after === true) {
|
||||||
$('.header-wrap').sticky({topSpacing:0});
|
// Initialize sticky header
|
||||||
});
|
$timeout(function () {
|
||||||
});
|
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
|
||||||
|
$('.header-wrap').sticky({topSpacing:0});
|
||||||
|
});
|
||||||
|
|
||||||
$rootScope.$on('userUpdated',initSticky);
|
// Remove listener
|
||||||
|
clearAppLoadedListener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$rootScope.$on('$stateChangeSuccess',
|
$rootScope.$on('$stateChangeSuccess',
|
||||||
function(event, toState, toParams, fromState, fromParams){
|
function(event, toState, toParams, fromState, fromParams){
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('habitrpg')
|
angular.module('habitrpg')
|
||||||
.factory('Chat', ['$http', 'ApiUrl', 'User',
|
.factory('Chat', ['$http', 'ApiUrl', 'User', 'Pusher',
|
||||||
function($http, ApiUrl, User) {
|
function($http, ApiUrl, User, Pusher) {
|
||||||
var apiV3Prefix = '/api/v3';
|
var apiV3Prefix = '/api/v3';
|
||||||
|
|
||||||
function getChat (groupId) {
|
function getChat (groupId) {
|
||||||
@@ -24,6 +24,7 @@ angular.module('habitrpg')
|
|||||||
url: url,
|
url: url,
|
||||||
data: {
|
data: {
|
||||||
message: message,
|
message: message,
|
||||||
|
pusherSocketId: Pusher.socketId, // to make sure the send doesn't get notified of it's own message
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
94
website/client/js/services/pusherService.js
Normal file
94
website/client/js/services/pusherService.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('habitrpg')
|
||||||
|
.factory('Pusher', ['$rootScope', 'STORAGE_SETTINGS_ID', 'Groups',
|
||||||
|
function($rootScope, STORAGE_SETTINGS_ID, Groups) {
|
||||||
|
var settings = JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID));
|
||||||
|
var IS_PUSHER_ENABLED = window.env['PUSHER:ENABLED'] === 'true';
|
||||||
|
|
||||||
|
var api = {
|
||||||
|
pusher: undefined,
|
||||||
|
socketId: undefined, // when defined the user is connected
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup chat channels once app is ready, only for parties for now
|
||||||
|
var clearAppLoadedListener = $rootScope.$watch('appLoaded', function (after) {
|
||||||
|
if (!after) return;
|
||||||
|
clearAppLoadedListener(); // clean the event listerner
|
||||||
|
|
||||||
|
if (!IS_PUSHER_ENABLED) return;
|
||||||
|
|
||||||
|
var user = $rootScope.user;
|
||||||
|
|
||||||
|
// Connect the user to Pusher and to the party's chat channel
|
||||||
|
var partyId = user && $rootScope.user.party && $rootScope.user.party._id;
|
||||||
|
if (!partyId) return;
|
||||||
|
|
||||||
|
api.pusher = new Pusher(window.env['PUSHER:KEY'], {
|
||||||
|
encrypted: true,
|
||||||
|
authEndpoint: '/api/v3/user/auth/pusher',
|
||||||
|
auth: {
|
||||||
|
headers: {
|
||||||
|
'x-api-user': settings && settings.auth && settings.auth.apiId,
|
||||||
|
'x-api-key': settings && settings.auth && settings.auth.apiToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.pusher.connection.bind('error', function(err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO if( err.data.code === 4004 ) detected connection limit
|
||||||
|
});
|
||||||
|
|
||||||
|
api.pusher.connection.bind('connected', function () {
|
||||||
|
api.socketId = api.pusher.connection.socket_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
var partyChannelName = 'presence-group-' + partyId;
|
||||||
|
var partyChannel = api.pusher.subscribe(partyChannelName);
|
||||||
|
|
||||||
|
// When an error occurs while joining the channel
|
||||||
|
partyChannel.bind('pusher:subscription_error', function(status) {
|
||||||
|
console.error('Impossible to join the Pusher channel for your party, status: ', status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the user correctly enters the party channel
|
||||||
|
partyChannel.bind('pusher:subscription_succeeded', function(members) {
|
||||||
|
// TODO members = [{id, info}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a member enters the party channel
|
||||||
|
partyChannel.bind('pusher:member_added', function(member) {
|
||||||
|
// TODO member = {id, info}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a member leaves the party channel
|
||||||
|
partyChannel.bind('pusher:member_removed', function(member) {
|
||||||
|
// TODO member = {id, info}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the user is booted from the party, they get disconnected from Pusher
|
||||||
|
partyChannel.bind('user-removed', function (data) {
|
||||||
|
if (data.userId === user._id) {
|
||||||
|
api.pusher.unsubscribe(partyChannelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Same when the user leaves the party
|
||||||
|
partyChannel.bind('user-left', function (data) {
|
||||||
|
if (data.userId === user._id) {
|
||||||
|
api.pusher.unsubscribe(partyChannelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a new chat message is posted
|
||||||
|
partyChannel.bind('new-chat', function (data) {
|
||||||
|
Groups.party().then(function () {
|
||||||
|
// Groups.data.party.chat.unshift(data);
|
||||||
|
// Groups.data.party.chat.splice(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}]);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
|
"bower_components/pusher-websocket-iso/dist/web/pusher.js",
|
||||||
"bower_components/jquery/dist/jquery.min.js",
|
"bower_components/jquery/dist/jquery.min.js",
|
||||||
"bower_components/jquery.cookie/jquery.cookie.js",
|
"bower_components/jquery.cookie/jquery.cookie.js",
|
||||||
"bower_components/pnotify/jquery.pnotify.min.js",
|
"bower_components/pnotify/jquery.pnotify.min.js",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
"js/services/userNotificationsService.js",
|
"js/services/userNotificationsService.js",
|
||||||
"js/services/userServices.js",
|
"js/services/userServices.js",
|
||||||
"js/services/hallServices.js",
|
"js/services/hallServices.js",
|
||||||
|
"js/services/pusherService.js",
|
||||||
|
|
||||||
"js/filters/money.js",
|
"js/filters/money.js",
|
||||||
"js/filters/roundLargeNumbers.js",
|
"js/filters/roundLargeNumbers.js",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ var async = require('async');
|
|||||||
var utils = require('../../libs/api-v2/utils');
|
var utils = require('../../libs/api-v2/utils');
|
||||||
var nconf = require('nconf');
|
var nconf = require('nconf');
|
||||||
var request = require('request');
|
var request = require('request');
|
||||||
var FirebaseTokenGenerator = require('firebase-token-generator');
|
|
||||||
import {
|
import {
|
||||||
model as User,
|
model as User,
|
||||||
} from '../../models/user';
|
} from '../../models/user';
|
||||||
@@ -351,28 +350,6 @@ api.changePassword = function(req, res, next) {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// DISABLED FOR API v2
|
|
||||||
/*var firebaseTokenGeneratorInstance = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
|
|
||||||
api.getFirebaseToken = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
// Expires 24 hours after now (60*60*24*1000) (in milliseconds)
|
|
||||||
var expires = new Date();
|
|
||||||
expires.setTime(expires.getTime() + 86400000);
|
|
||||||
|
|
||||||
var token = firebaseTokenGeneratorInstance
|
|
||||||
.createToken({
|
|
||||||
uid: user._id,
|
|
||||||
isHabiticaUser: true
|
|
||||||
}, {
|
|
||||||
expires: expires
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
token: token,
|
|
||||||
expires: expires
|
|
||||||
});
|
|
||||||
};*/
|
|
||||||
|
|
||||||
// DISABLED FOR API v2
|
// DISABLED FOR API v2
|
||||||
/*api.setupPassport = function(router) {
|
/*api.setupPassport = function(router) {
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ var isProd = nconf.get('NODE_ENV') === 'production';
|
|||||||
var api = module.exports;
|
var api = module.exports;
|
||||||
var pushNotify = require('./pushNotifications');
|
var pushNotify = require('./pushNotifications');
|
||||||
var analytics = utils.analytics;
|
var analytics = utils.analytics;
|
||||||
var firebase = require('../../libs/api-v2/firebase');
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
@@ -233,8 +232,6 @@ api.create = function(req, res, next) {
|
|||||||
function(cb){user.save(cb)},
|
function(cb){user.save(cb)},
|
||||||
function(saved,ct,cb){group.save(cb)},
|
function(saved,ct,cb){group.save(cb)},
|
||||||
function(saved,ct,cb){
|
function(saved,ct,cb){
|
||||||
firebase.updateGroupData(saved);
|
|
||||||
firebase.addUserToGroup(saved._id, user._id);
|
|
||||||
saved.getTransformedData({
|
saved.getTransformedData({
|
||||||
populateMembers: nameFields,
|
populateMembers: nameFields,
|
||||||
cb,
|
cb,
|
||||||
@@ -278,7 +275,6 @@ api.update = function(req, res, next) {
|
|||||||
group.save(function(err, saved){
|
group.save(function(err, saved){
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
|
||||||
firebase.updateGroupData(saved);
|
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -548,7 +544,6 @@ api.join = function(req, res, next) {
|
|||||||
group.save(cb);
|
group.save(cb);
|
||||||
},
|
},
|
||||||
function(cb){
|
function(cb){
|
||||||
firebase.addUserToGroup(group._id, user._id);
|
|
||||||
group.getTransformedData({
|
group.getTransformedData({
|
||||||
cb,
|
cb,
|
||||||
populateMembers: group.type === 'party' ? partyFields : nameFields,
|
populateMembers: group.type === 'party' ? partyFields : nameFields,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import v3UserController from '../api-v3/user';
|
|||||||
let i18n = shared.i18n;
|
let i18n = shared.i18n;
|
||||||
|
|
||||||
var api = module.exports;
|
var api = module.exports;
|
||||||
var firebase = require('../../libs/api-v2/firebase');
|
|
||||||
var webhook = require('../../libs/api-v2/webhook');
|
var webhook = require('../../libs/api-v2/webhook');
|
||||||
|
|
||||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||||
@@ -468,7 +467,6 @@ api.delete = function(req, res, next) {
|
|||||||
return user.remove();
|
return user.remove();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
firebase.deleteUser(user._id);
|
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
})
|
})
|
||||||
.catch(next);
|
.catch(next);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { model as Group } from '../../models/group';
|
|||||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||||
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
||||||
import { decrypt } from '../../libs/api-v3/encryption';
|
import { decrypt } from '../../libs/api-v3/encryption';
|
||||||
import FirebaseTokenGenerator from 'firebase-token-generator';
|
|
||||||
import { send as sendEmail } from '../../libs/api-v3/email';
|
import { send as sendEmail } from '../../libs/api-v3/email';
|
||||||
|
import pusher from '../../libs/api-v3/pusher';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -282,6 +282,76 @@ api.loginSocial = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @apiIgnore Private route
|
||||||
|
* @api {post} /api/v3/user/auth/pusher Pusher.com authentication
|
||||||
|
* @apiDescription Authentication for Pusher.com private and presence channels
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName UserAuthPusher
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiParam {String} socket_id Body parameter
|
||||||
|
* @apiParam {String} channel_name Body parameter
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} auth The authentication token
|
||||||
|
*/
|
||||||
|
api.pusherAuth = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user/auth/pusher',
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
req.checkBody('socket_id').notEmpty();
|
||||||
|
req.checkBody('channel_name').notEmpty();
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
let socketId = req.body.socket_id;
|
||||||
|
let channelName = req.body.channel_name;
|
||||||
|
|
||||||
|
// Channel names are in the form of {presence|private}-{group|...}-{resourceId}
|
||||||
|
let [channelType, resourceType, ...resourceId] = channelName.split('-');
|
||||||
|
|
||||||
|
if (['presence'].indexOf(channelType) === -1) { // presence is used only for parties, private for guilds too
|
||||||
|
throw new BadRequest('Invalid Pusher channel type.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceType !== 'group') { // only groups are supported
|
||||||
|
throw new BadRequest('Invalid Pusher resource type.');
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceId = resourceId.join('-'); // the split at the beginning had split resourceId too
|
||||||
|
if (!validator.isUUID(resourceId)) {
|
||||||
|
throw new BadRequest('Invalid Pusher resource id, must be a UUID.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the user's party is supported for now
|
||||||
|
if (user.party._id !== resourceId) {
|
||||||
|
throw new NotFound('Resource id must be the user\'s party.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let authResult;
|
||||||
|
|
||||||
|
// Max 100 members for presence channel - parties only
|
||||||
|
if (channelType === 'presence') {
|
||||||
|
let presenceData = {
|
||||||
|
user_id: user._id, // eslint-disable-line camelcase
|
||||||
|
// Max 1KB
|
||||||
|
user_info: {}, // eslint-disable-line camelcase
|
||||||
|
};
|
||||||
|
|
||||||
|
authResult = pusher.authenticate(socketId, channelName, presenceData);
|
||||||
|
} else {
|
||||||
|
authResult = pusher.authenticate(socketId, channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not using res.respond because Pusher requires a different response format
|
||||||
|
res.status(200).json(authResult);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {put} /api/v3/user/auth/update-username Update username
|
* @api {put} /api/v3/user/auth/update-username Update username
|
||||||
* @apiDescription Update the username of a local user
|
* @apiDescription Update the username of a local user
|
||||||
@@ -472,28 +542,6 @@ api.updateEmail = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
|
|
||||||
|
|
||||||
// Internal route
|
|
||||||
api.getFirebaseToken = {
|
|
||||||
method: 'POST',
|
|
||||||
url: '/user/auth/firebase',
|
|
||||||
middlewares: [authWithHeaders()],
|
|
||||||
async handler (req, res) {
|
|
||||||
let user = res.locals.user;
|
|
||||||
// Expires 24 hours from now (60*60*24*1000) (in milliseconds)
|
|
||||||
let expires = new Date();
|
|
||||||
expires.setTime(expires.getTime() + 86400000);
|
|
||||||
|
|
||||||
let token = firebaseTokenGenerator.createToken({
|
|
||||||
uid: user._id,
|
|
||||||
isHabiticaUser: true,
|
|
||||||
}, { expires });
|
|
||||||
|
|
||||||
res.respond(200, {token, expires});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {delete} /api/v3/user/auth/social/:network Delete social authentication method
|
* @api {delete} /api/v3/user/auth/social/:network Delete social authentication method
|
||||||
* @apiDescription Remove a social authentication method (only facebook supported) from a user profile. The user must have local authentication enabled
|
* @apiDescription Remove a social authentication method (only facebook supported) from a user profile. The user must have local authentication enabled
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
||||||
import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/api-v3/email';
|
import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/api-v3/email';
|
||||||
|
import pusher from '../../libs/api-v3/pusher';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
|
|
||||||
@@ -53,8 +54,8 @@ api.getChat = {
|
|||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
* @apiParam {message} Body parameter - message The message to post
|
* @apiParam {string} message Body parameter - message The message to post
|
||||||
* @apiParam {previousMsg} previousMsg Query parameter - The previous chat message which will force a return of the full group chat
|
* @apiParam {UUID} previousMsg Query parameter - The previous chat message which will force a return of the full group chat
|
||||||
*
|
*
|
||||||
* @apiSuccess data An array of chat messages if a new message was posted after previousMsg, otherwise the posted message
|
* @apiSuccess data An array of chat messages if a new message was posted after previousMsg, otherwise the posted message
|
||||||
*/
|
*/
|
||||||
@@ -83,7 +84,7 @@ api.postChat = {
|
|||||||
let lastClientMsg = req.query.previousMsg;
|
let lastClientMsg = req.query.previousMsg;
|
||||||
chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
|
chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
|
||||||
|
|
||||||
group.sendChat(req.body.message, user);
|
let newChatMessage = group.sendChat(req.body.message, user);
|
||||||
|
|
||||||
let toSave = [group.save()];
|
let toSave = [group.save()];
|
||||||
|
|
||||||
@@ -93,6 +94,14 @@ api.postChat = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let [savedGroup] = await Bluebird.all(toSave);
|
let [savedGroup] = await Bluebird.all(toSave);
|
||||||
|
|
||||||
|
// real-time chat is only enabled for private groups (for now only for parties)
|
||||||
|
if (savedGroup.privacy === 'private' && savedGroup.type === 'party') {
|
||||||
|
// req.body.pusherSocketId is sent from official clients to identify the sender user's real time socket
|
||||||
|
// see https://pusher.com/docs/server_api_guide/server_excluding_recipients
|
||||||
|
pusher.trigger(`presence-group-${savedGroup._id}`, 'new-chat', newChatMessage, req.body.pusherSocketId);
|
||||||
|
}
|
||||||
|
|
||||||
if (chatUpdated) {
|
if (chatUpdated) {
|
||||||
res.respond(200, {chat: Group.toJSONCleanChat(savedGroup, user).chat});
|
res.respond(200, {chat: Group.toJSONCleanChat(savedGroup, user).chat});
|
||||||
} else {
|
} else {
|
||||||
@@ -107,8 +116,8 @@ api.postChat = {
|
|||||||
* @apiName LikeChat
|
* @apiName LikeChat
|
||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
* @apiParam {chatId} chatId The chat message _id
|
* @apiParam {UUID} chatId The chat message _id
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data The liked chat message
|
* @apiSuccess {Object} data The liked chat message
|
||||||
*/
|
*/
|
||||||
@@ -154,8 +163,8 @@ api.likeChat = {
|
|||||||
* @apiName FlagChat
|
* @apiName FlagChat
|
||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
* @apiParam {chatId} chatId The chat message id
|
* @apiParam {UUID} chatId The chat message id
|
||||||
*
|
*
|
||||||
* @apiSuccess {object} data The flagged chat message
|
* @apiSuccess {object} data The flagged chat message
|
||||||
*/
|
*/
|
||||||
@@ -243,8 +252,8 @@ api.flagChat = {
|
|||||||
* @apiName ClearFlags
|
* @apiName ClearFlags
|
||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
* @apiParam {chatId} chatId The chat message id
|
* @apiParam {UUID} chatId The chat message id
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data An empty object
|
* @apiSuccess {Object} data An empty object
|
||||||
*/
|
*/
|
||||||
@@ -318,7 +327,7 @@ api.clearChatFlags = {
|
|||||||
* @apiName SeenChat
|
* @apiName SeenChat
|
||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data An empty object
|
* @apiSuccess {Object} data An empty object
|
||||||
*/
|
*/
|
||||||
@@ -354,8 +363,8 @@ api.seenChat = {
|
|||||||
* @apiGroup Chat
|
* @apiGroup Chat
|
||||||
*
|
*
|
||||||
* @apiParam {string} previousMsg Query parameter - The last message fetched by the client so that the whole chat will be returned only if new messages have been posted in the meantime
|
* @apiParam {string} previousMsg Query parameter - The last message fetched by the client so that the whole chat will be returned only if new messages have been posted in the meantime
|
||||||
* @apiParam {string} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||||
* @apiParam {string} chatId The chat message id
|
* @apiParam {UUID} chatId The chat message id
|
||||||
*
|
*
|
||||||
* @apiSuccess data The updated chat array or an empty object if no message was posted after previousMsg
|
* @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
|
* @apiSuccess {Object} data An empty object when the previous message was deleted
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import {
|
|||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
||||||
import * as firebase from '../../libs/api-v3/firebase';
|
|
||||||
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
||||||
import { encrypt } from '../../libs/api-v3/encryption';
|
import { encrypt } from '../../libs/api-v3/encryption';
|
||||||
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
||||||
|
import pusher from '../../libs/api-v3/pusher';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,9 +67,6 @@ api.createGroup = {
|
|||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
};
|
};
|
||||||
res.respond(201, response); // do not remove chat flags data as we've just created the group
|
res.respond(201, response); // do not remove chat flags data as we've just created the group
|
||||||
|
|
||||||
firebase.updateGroupData(savedGroup);
|
|
||||||
firebase.addUserToGroup(savedGroup._id, user._id);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,8 +177,6 @@ api.updateGroup = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
res.respond(200, response);
|
res.respond(200, response);
|
||||||
|
|
||||||
firebase.updateGroupData(savedGroup);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,8 +274,6 @@ api.joinGroup = {
|
|||||||
response.leader = leader.toJSON({minimize: true});
|
response.leader = leader.toJSON({minimize: true});
|
||||||
}
|
}
|
||||||
res.respond(200, response);
|
res.respond(200, response);
|
||||||
|
|
||||||
firebase.addUserToGroup(group._id, user._id);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -449,7 +443,15 @@ api.removeGroupMember = {
|
|||||||
if (isInGroup === 'guild') {
|
if (isInGroup === 'guild') {
|
||||||
removeFromArray(member.guilds, group._id);
|
removeFromArray(member.guilds, group._id);
|
||||||
}
|
}
|
||||||
if (isInGroup === 'party') member.party._id = undefined; // TODO remove quest information too? Use group.leave()?
|
if (isInGroup === 'party') {
|
||||||
|
// Tell the realtime clients that a user is being removed
|
||||||
|
// If the user that is being removed is still connected, they'll get disconnected automatically
|
||||||
|
pusher.trigger(`presence-group-${group._id}`, 'user-removed', {
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
member.party._id = undefined; // TODO remove quest information too? Use group.leave()?
|
||||||
|
}
|
||||||
|
|
||||||
if (member.newMessages[group._id]) {
|
if (member.newMessages[group._id]) {
|
||||||
member.newMessages[group._id] = undefined;
|
member.newMessages[group._id] = undefined;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as firebase from '../../libs/api-v3/firebase';
|
|
||||||
import * as passwordUtils from '../../libs/api-v3/password';
|
import * as passwordUtils from '../../libs/api-v3/password';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
@@ -230,8 +229,6 @@ api.deleteUser = {
|
|||||||
await user.remove();
|
await user.remove();
|
||||||
|
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
|
|
||||||
firebase.deleteUser(user._id);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
var Firebase = require('firebase');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
|
||||||
var firebaseConfig = nconf.get('FIREBASE');
|
|
||||||
|
|
||||||
var firebaseRef;
|
|
||||||
var isFirebaseEnabled = (nconf.get('NODE_ENV') === 'production') && (firebaseConfig.ENABLED === 'true');
|
|
||||||
|
|
||||||
import { TAVERN_ID } from '../../models/group';
|
|
||||||
|
|
||||||
// Setup
|
|
||||||
if(isFirebaseEnabled){
|
|
||||||
firebaseRef = new Firebase('https://' + firebaseConfig.APP + '.firebaseio.com');
|
|
||||||
|
|
||||||
// TODO what happens if an op is sent before client is authenticated?
|
|
||||||
firebaseRef.authWithCustomToken(firebaseConfig.SECRET, function(err, authData){
|
|
||||||
// TODO it's ok to kill the server here? what if FB is offline?
|
|
||||||
if(err) throw new Error('Impossible to authenticate Firebase');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var api = module.exports = {};
|
|
||||||
|
|
||||||
api.updateGroupData = function(group){
|
|
||||||
if(!isFirebaseEnabled) return;
|
|
||||||
// TODO is throw ok? we don't have callbacks
|
|
||||||
if(!group) throw new Error('group is required.');
|
|
||||||
// Return in case of tavern (comparison working because we use string for _id)
|
|
||||||
if(group._id === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child('rooms/' + group._id)
|
|
||||||
.set({
|
|
||||||
name: group.name
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.addUserToGroup = function(groupId, userId){
|
|
||||||
if(!isFirebaseEnabled) return;
|
|
||||||
if(!userId || !groupId) throw new Error('groupId, userId are required.');
|
|
||||||
if(groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child('members/' + groupId + '/' + userId)
|
|
||||||
.set(true);
|
|
||||||
|
|
||||||
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
|
|
||||||
.set(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
api.removeUserFromGroup = function(groupId, userId){
|
|
||||||
if(!isFirebaseEnabled) return;
|
|
||||||
if(!userId || !groupId) throw new Error('groupId, userId are required.');
|
|
||||||
if(groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child('members/' + groupId + '/' + userId)
|
|
||||||
.remove();
|
|
||||||
|
|
||||||
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
|
|
||||||
.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
api.deleteGroup = function(groupId){
|
|
||||||
if(!isFirebaseEnabled) return;
|
|
||||||
if(!groupId) throw new Error('groupId is required.');
|
|
||||||
if(groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child('rooms/' + groupId)
|
|
||||||
.remove();
|
|
||||||
|
|
||||||
// TODO not really necessary as long as we only store room data,
|
|
||||||
// as empty objects are automatically deleted (/members/... in future...)
|
|
||||||
firebaseRef.child('members/' + groupId)
|
|
||||||
.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO not really necessary as long as we only store room data,
|
|
||||||
// as empty objects are automatically deleted
|
|
||||||
api.deleteUser = function(userId){
|
|
||||||
if(!isFirebaseEnabled) return;
|
|
||||||
if(!userId) throw new Error('userId is required.');
|
|
||||||
|
|
||||||
firebaseRef.child('users/' + userId)
|
|
||||||
.remove();
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import Firebase from 'firebase';
|
|
||||||
import nconf from 'nconf';
|
|
||||||
import { TAVERN_ID } from '../../models/group';
|
|
||||||
|
|
||||||
const FIREBASE_CONFIG = nconf.get('FIREBASE');
|
|
||||||
const FIREBASE_ENABLED = FIREBASE_CONFIG.ENABLED === 'true';
|
|
||||||
|
|
||||||
let firebaseRef;
|
|
||||||
|
|
||||||
if (FIREBASE_ENABLED) {
|
|
||||||
firebaseRef = new Firebase(`https://${FIREBASE_CONFIG.APP}.firebaseio.com`);
|
|
||||||
|
|
||||||
// TODO what happens if an op is sent before client is authenticated?
|
|
||||||
firebaseRef.authWithCustomToken(FIREBASE_CONFIG.SECRET, (err) => {
|
|
||||||
// TODO it's ok to kill the server here? what if FB is offline?
|
|
||||||
if (err) throw new Error('Impossible to authenticate Firebase');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateGroupData (group) {
|
|
||||||
if (!FIREBASE_ENABLED) return;
|
|
||||||
// TODO is throw ok? we don't have callbacks
|
|
||||||
if (!group) throw new Error('group obj is required.');
|
|
||||||
// Return in case of tavern (comparison working because we use string for _id)
|
|
||||||
if (group._id === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child(`rooms/${group._id}`)
|
|
||||||
.set({
|
|
||||||
name: group.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addUserToGroup (groupId, userId) {
|
|
||||||
if (!FIREBASE_ENABLED) return;
|
|
||||||
if (!userId || !groupId) throw new Error('groupId, userId are required.');
|
|
||||||
if (groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child(`members/${groupId}/${userId}`).set(true);
|
|
||||||
firebaseRef.child(`users/${userId}/rooms/${groupId}`).set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeUserFromGroup (groupId, userId) {
|
|
||||||
if (!FIREBASE_ENABLED) return;
|
|
||||||
if (!userId || !groupId) throw new Error('groupId, userId are required.');
|
|
||||||
if (groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child(`members/${groupId}/${userId}`).remove();
|
|
||||||
firebaseRef.child(`users/${userId}/rooms/${groupId}`).remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteGroup (groupId) {
|
|
||||||
if (!FIREBASE_ENABLED) return;
|
|
||||||
if (!groupId) throw new Error('groupId is required.');
|
|
||||||
if (groupId === TAVERN_ID) return;
|
|
||||||
|
|
||||||
firebaseRef.child(`members/${groupId}`).remove();
|
|
||||||
// TODO not really necessary as long as we only store room data,
|
|
||||||
// as empty objects are automatically deleted (/members/... in future...)
|
|
||||||
firebaseRef.child(`rooms/${groupId}`).remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO not really necessary as long as we only store room data,
|
|
||||||
// as empty objects are automatically deleted
|
|
||||||
export function deleteUser (userId) {
|
|
||||||
if (!FIREBASE_ENABLED) return;
|
|
||||||
if (!userId) throw new Error('userId is required.');
|
|
||||||
|
|
||||||
firebaseRef.child(`users/${userId}`).remove();
|
|
||||||
}
|
|
||||||
42
website/server/libs/api-v3/pusher.js
Normal file
42
website/server/libs/api-v3/pusher.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Pusher from 'pusher';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import { InternalServerError } from './errors';
|
||||||
|
|
||||||
|
const IS_PUSHER_ENABLED = nconf.get('PUSHER:ENABLED') === 'true';
|
||||||
|
|
||||||
|
let pusherInstance;
|
||||||
|
|
||||||
|
if (IS_PUSHER_ENABLED) {
|
||||||
|
pusherInstance = new Pusher({
|
||||||
|
appId: nconf.get('PUSHER:APP_ID'),
|
||||||
|
key: nconf.get('PUSHER:KEY'),
|
||||||
|
secret: nconf.get('PUSHER:SECRET'),
|
||||||
|
encrypted: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let api = {
|
||||||
|
// https://github.com/pusher/pusher-http-node#publishing-events
|
||||||
|
trigger (channel, event, data, socketId = null) {
|
||||||
|
if (!IS_PUSHER_ENABLED) return Promise.resolve(null);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pusherInstance.trigger(channel, event, data, socketId, (err, req, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve([req, res]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://github.com/pusher/pusher-http-node#authenticating-private-channels
|
||||||
|
authenticate (...args) {
|
||||||
|
if (!IS_PUSHER_ENABLED) throw new InternalServerError('Pusher is not enabled.');
|
||||||
|
|
||||||
|
return pusherInstance.authenticate(...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
@@ -15,7 +15,7 @@ import { mods } from '../../models/user';
|
|||||||
const CLIENT_VARS = ['language', 'isStaticPage', 'availableLanguages', 'translations',
|
const CLIENT_VARS = ['language', 'isStaticPage', 'availableLanguages', 'translations',
|
||||||
'FACEBOOK_KEY', 'NODE_ENV', 'BASE_URL', 'GA_ID',
|
'FACEBOOK_KEY', 'NODE_ENV', 'BASE_URL', 'GA_ID',
|
||||||
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
|
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
|
||||||
'worldDmg', 'mods', 'IS_MOBILE'];
|
'worldDmg', 'mods', 'IS_MOBILE', 'PUSHER:KEY', 'PUSHER:ENABLED'];
|
||||||
|
|
||||||
let env = {
|
let env = {
|
||||||
getManifestFiles,
|
getManifestFiles,
|
||||||
@@ -32,9 +32,11 @@ let env = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY AMPLITUDE_KEY'.split(' ').forEach(key => {
|
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED'
|
||||||
env[key] = nconf.get(key);
|
.split(' ')
|
||||||
});
|
.forEach(key => {
|
||||||
|
env[key] = nconf.get(key);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = function locals (req, res, next) {
|
module.exports = function locals (req, res, next) {
|
||||||
let language = _.find(i18n.availableLanguages, {code: req.language});
|
let language = _.find(i18n.availableLanguages, {code: req.language});
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
InternalServerError,
|
InternalServerError,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
} from '../libs/api-v3/errors';
|
} from '../libs/api-v3/errors';
|
||||||
import * as firebase from '../libs/api-v2/firebase';
|
|
||||||
import baseModel from '../libs/api-v3/baseModel';
|
import baseModel from '../libs/api-v3/baseModel';
|
||||||
import { sendTxn as sendTxnEmail } from '../libs/api-v3/email';
|
import { sendTxn as sendTxnEmail } from '../libs/api-v3/email';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
||||||
|
import pusher from '../libs/api-v3/pusher';
|
||||||
|
|
||||||
const questScrolls = shared.content.quests;
|
const questScrolls = shared.content.quests;
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
@@ -31,8 +31,6 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS
|
|||||||
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||||
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||||
|
|
||||||
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
|
|
||||||
// changes made directly to the db will cause Firebase to get out of sync
|
|
||||||
export let schema = new Schema({
|
export let schema = new Schema({
|
||||||
name: {type: String, required: true},
|
name: {type: String, required: true},
|
||||||
description: String,
|
description: String,
|
||||||
@@ -109,10 +107,6 @@ schema.pre('remove', true, async function preRemoveGroup (next, done) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
schema.post('remove', function postRemoveGroup (group) {
|
|
||||||
firebase.deleteGroup(group._id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// return a clean object for user.quest
|
// return a clean object for user.quest
|
||||||
function _cleanQuestProgress (merge) {
|
function _cleanQuestProgress (merge) {
|
||||||
let clean = {
|
let clean = {
|
||||||
@@ -239,12 +233,14 @@ schema.statics.getGroups = async function getGroups (options = {}) {
|
|||||||
// Not putting into toJSON because there we can't access user
|
// Not putting into toJSON because there we can't access user
|
||||||
schema.statics.toJSONCleanChat = function groupToJSONCleanChat (group, user) {
|
schema.statics.toJSONCleanChat = function groupToJSONCleanChat (group, user) {
|
||||||
let toJSON = group.toJSON();
|
let toJSON = group.toJSON();
|
||||||
|
|
||||||
if (!user.contributor.admin) {
|
if (!user.contributor.admin) {
|
||||||
_.remove(toJSON.chat, chatMsg => {
|
_.remove(toJSON.chat, chatMsg => {
|
||||||
chatMsg.flags = {};
|
chatMsg.flags = {};
|
||||||
return chatMsg.flagCount >= 2;
|
return chatMsg.flagCount >= 2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return toJSON;
|
return toJSON;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -308,7 +304,9 @@ export function chatDefaults (msg, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema.methods.sendChat = function sendChat (message, user) {
|
schema.methods.sendChat = function sendChat (message, user) {
|
||||||
this.chat.unshift(chatDefaults(message, user));
|
let newMessage = chatDefaults(message, user);
|
||||||
|
|
||||||
|
this.chat.unshift(newMessage);
|
||||||
this.chat.splice(200);
|
this.chat.splice(200);
|
||||||
|
|
||||||
// do not send notifications for guilds with more than 5000 users and for the tavern
|
// do not send notifications for guilds with more than 5000 users and for the tavern
|
||||||
@@ -331,6 +329,8 @@ schema.methods.sendChat = function sendChat (message, user) {
|
|||||||
query._id = { $ne: user ? user._id : ''};
|
query._id = { $ne: user ? user._id : ''};
|
||||||
|
|
||||||
User.update(query, lastSeenUpdate, {multi: true}).exec();
|
User.update(query, lastSeenUpdate, {multi: true}).exec();
|
||||||
|
|
||||||
|
return newMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.startQuest = async function startQuest (user) {
|
schema.methods.startQuest = async function startQuest (user) {
|
||||||
@@ -719,6 +719,11 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
|||||||
promises.push(User.update({_id: user._id}, {$pull: {guilds: group._id}}).exec());
|
promises.push(User.update({_id: user._id}, {$pull: {guilds: group._id}}).exec());
|
||||||
} else {
|
} else {
|
||||||
promises.push(User.update({_id: user._id}, {$set: {party: {}}}).exec());
|
promises.push(User.update({_id: user._id}, {$set: {party: {}}}).exec());
|
||||||
|
// Tell the realtime clients that a user has left
|
||||||
|
// If the user that left is still connected, they'll get disconnected
|
||||||
|
pusher.trigger(`presence-group-${group._id}`, 'user-left', {
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user is the last one in group and group is private, delete it
|
// If user is the last one in group and group is private, delete it
|
||||||
@@ -740,8 +745,6 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
|||||||
promises.push(group.update(update).exec());
|
promises.push(group.update(update).exec());
|
||||||
}
|
}
|
||||||
|
|
||||||
firebase.removeUserFromGroup(group._id, user._id);
|
|
||||||
|
|
||||||
return await Bluebird.all(promises);
|
return await Bluebird.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,5 @@ router.post('/user/reset-password', getUserLanguage, auth.resetPassword);
|
|||||||
router.post('/user/change-password', getUserLanguage, auth.auth, auth.changePassword);
|
router.post('/user/change-password', getUserLanguage, auth.auth, auth.changePassword);
|
||||||
router.post('/user/change-username', getUserLanguage, auth.auth, auth.changeUsername);
|
router.post('/user/change-username', getUserLanguage, auth.auth, auth.changeUsername);
|
||||||
router.post('/user/change-email', getUserLanguage, auth.auth, auth.changeEmail);
|
router.post('/user/change-email', getUserLanguage, auth.auth, auth.changeEmail);
|
||||||
// router.post('/user/auth/firebase', i18n.getUserLanguage, auth.auth, auth.getFirebaseToken);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import './libs/api-v3/i18n';
|
|||||||
|
|
||||||
// Load config files
|
// Load config files
|
||||||
import './libs/api-v3/setupMongoose';
|
import './libs/api-v3/setupMongoose';
|
||||||
import './libs/api-v3/firebase';
|
|
||||||
import './libs/api-v3/setupPassport';
|
import './libs/api-v3/setupPassport';
|
||||||
|
|
||||||
// Load some schemas & models
|
// Load some schemas & models
|
||||||
|
|||||||
Reference in New Issue
Block a user