├── .gitignore ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .versions ├── HISTORY.md ├── LICENSE ├── README.md ├── client └── client.js ├── common └── common.js ├── package.js ├── server ├── monitor.js └── server.js ├── user-presence-tests.js └── utils └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | .meteor/meteorite 3 | .idea -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "dependencies": { 4 | "colors": { 5 | "version": "1.3.2", 6 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz", 7 | "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | babel-compiler@7.5.2 3 | babel-runtime@1.5.0 4 | base64@1.0.12 5 | binary-heap@1.0.11 6 | boilerplate-generator@1.6.0 7 | callback-hook@1.3.0 8 | check@1.3.1 9 | ddp@1.4.0 10 | ddp-client@2.3.3 11 | ddp-common@1.4.0 12 | ddp-server@2.3.0 13 | diff-sequence@1.1.1 14 | dynamic-import@0.5.1 15 | ecmascript@0.14.2 16 | ecmascript-runtime@0.7.0 17 | ecmascript-runtime-client@0.10.0 18 | ecmascript-runtime-server@0.9.0 19 | ejson@1.1.1 20 | fetch@0.1.1 21 | geojson-utils@1.0.10 22 | id-map@1.1.0 23 | inter-process-messaging@0.1.0 24 | konecty:user-presence@2.6.3 25 | local-test:konecty:user-presence@2.6.3 26 | logging@1.1.20 27 | meteor@1.9.3 28 | minimongo@1.4.5 29 | modern-browsers@0.1.4 30 | modules@0.15.0 31 | modules-runtime@0.12.0 32 | mongo@1.8.0 33 | mongo-decimal@0.1.1 34 | mongo-dev-server@1.1.0 35 | mongo-id@1.0.7 36 | npm-mongo@3.3.0 37 | ordered-dict@1.1.0 38 | promise@0.11.2 39 | random@1.1.0 40 | reload@1.3.0 41 | retry@1.1.0 42 | routepolicy@1.1.0 43 | socket-stream-client@0.2.2 44 | tinytest@1.1.0 45 | tracker@1.2.0 46 | underscore@1.0.10 47 | webapp@1.8.0 48 | webapp-hashing@1.0.9 49 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.6.0 (2019-08-14) 2 | * Do not allow users change another users status (#42) 3 | 4 | # 2.5.0 (2019-07-11) 5 | * Remove user's connection on logout 6 | 7 | # 2.4.0 (2018-12-13) 8 | * Allow presence monitoring to be completely disabled 9 | * Do not emit a status change if it has not changed 10 | * Do not call `connect` anymore 11 | * Add a `debounce` to `setAway` 12 | 13 | # 2.3.0 (2018-11-16) 14 | * Export `UsersSessions` collection 15 | * Remove `underscore` package dependency 16 | * Add `check` for all methods' params 17 | * Replace `nooitaf:colors` Meteor package for its npm package 18 | 19 | # 2.2.0 (2018-08-08) 20 | * Prevent already closed connections from being stored on db 21 | 22 | # 2.1.0 (2018-06-09) 23 | * Do not start timer if `awayTime` is `null` 24 | * Start user presence only once 25 | 26 | # 2.0.1 (2017-12-13) 27 | * Do not tracks a connection without `id` 28 | 29 | # 2.0.0 (2017-12-04) 30 | * [BREAK] Remove `visitor` related code, use the new `metadata` field instead 31 | * Add event emitter for tracking any connection 32 | * Stop away verification on disconnect and set user back online on reconnect 33 | * Set correct user on `UserPresence:online` stub method 34 | * Don't call methods if client is disconnected 35 | * Allow set a value for `userId` on client side (instead of regular `Meteor.userId()`) 36 | 37 | # 1.2.9 (2016-09-09) 38 | * Fix #16; Prevent error when proccess exit 39 | * Fix ESLint errors 40 | 41 | # 1.2.8 (2016-05-25) 42 | * Add _.throttle to set online status 43 | 44 | # 1.2.7 (2016-05-25) 45 | * Remove observeChanges on the users collection 46 | * Accept multiple callbacks for status change 47 | 48 | # 1.2.6 (2015-09-12) 49 | * Add option to passa a callback to setUserStatus on UserPresenceMonitor 50 | 51 | # 1.2.5 (2015-08-11) 52 | * Set user online on touch events too 53 | 54 | # 1.2.4 (2015-08-03) 55 | * Add callback *onSetUserStatus* to watch status changes 56 | 57 | # 1.2.3 (2015-07-25) 58 | * Added this.ready to publication 59 | 60 | # 1.2.2 (2015-02-11) 61 | * Use Accounts if package 'accounts-base' exists 62 | 63 | # 1.2.1 (2015-02-04) 64 | * Create index for 'connections.id' to improve performance 65 | 66 | # 1.2.0 (2015-02-04) 67 | * Move api common.js file to top of list 68 | * Create index for 'connections.instanceId' to improve performance 69 | * Do not process removal of users 70 | * Only process user changes that affects the field 'statusDefault' 71 | * Pass action names to processUserSession 72 | * Do not process removed sessions with no connections 73 | * Remove sessions with no connections 74 | 75 | # 1.1.0 (2015-02-02) 76 | * Allow visitor status tracking 77 | * Prevent error when no user was not found in setUserStatus 78 | 79 | # 1.0.15 (2015-01-21) 80 | * Allow pass status to createConnection 81 | * Update field '_updatedAt' of connection when update connection status 82 | * Change setConnection to use update instead upsert and recreate connection if no connetion exists 83 | 84 | # 1.0.14 (2015-01-21) 85 | * Improve latency compensation 86 | 87 | # 1.0.13 (2015-01-21) 88 | * Set user into connection on login to only remove connections with user 89 | 90 | # 1.0.12 (2015-01-21) 91 | * Add this.unblock() to all methods 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Konecty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USER PRESENCE 2 | 3 | * Use with multiple instances: https://github.com/Konecty/meteor-multiple-instances-status 4 | * Example code: https://github.com/Konecty/meteor-user-presence-example-chat 5 | 6 | This package monitors the user to track user's state and save 3 fields in user's record: 7 | * statusDefault - Status setted by user 8 | * statusConnection - Connection status (offline, online and away) 9 | * status 10 | * Offline if **statusConnection** or **statusDefault** are offline 11 | * Same as **statusConnection** if **statusDefault** is online 12 | * Same as **statusDefault** 13 | 14 | ## How to use 15 | 16 | #### Add package 17 | ```shell 18 | meteor add konecty:user-presence 19 | ``` 20 | 21 | #### Configure client 22 | ```javascript 23 | //CLIENT 24 | Meteor.startup(function() { 25 | // Time of inactivity to set user as away automaticly. Default 60000 26 | UserPresence.awayTime = 300000; 27 | // Set user as away when window loses focus. Defaults false 28 | UserPresence.awayOnWindowBlur = true; 29 | // Start monitor for user activity 30 | UserPresence.start(); 31 | }); 32 | ``` 33 | 34 | #### Start server 35 | ```javascript 36 | //SERVER 37 | // Listen for new connections, login, logoff and application exit to manage user status and register methods to be used by client to set user status and default status 38 | UserPresence.start(); 39 | // Active logs for every changes 40 | // Listen for changes in UserSessions and Meteor.users to set user status based on active connections 41 | UserPresenceMonitor.start(); 42 | ``` 43 | 44 | #### Logs 45 | ```javascript 46 | //SERVER 47 | UserPresence.activeLogs(); 48 | ``` 49 | 50 | ### Server Methods 51 | ```javascript 52 | // Create a new connection, this package do this automaticly 53 | Meteor.call('UserPresence:connect'); 54 | ``` 55 | 56 | ```javascript 57 | // Set connection as away, can be usefull call this method if you are using cordova to ser user as away when application goes to background for example. 58 | Meteor.call('UserPresence:away'); 59 | ``` 60 | 61 | ```javascript 62 | // Set connection as online 63 | Meteor.call('UserPresence:online'); 64 | ``` 65 | 66 | ```javascript 67 | // Changes the default status of user 68 | Meteor.call('UserPresence:setDefaultStatus', 'busy'); 69 | ``` 70 | -------------------------------------------------------------------------------- /client/client.js: -------------------------------------------------------------------------------- 1 | /* globals UserPresence */ 2 | import { debounce } from '../utils'; 3 | let timer, status; 4 | const setUserPresence = debounce((newStatus) => { 5 | if (!UserPresence.connected || newStatus === status) { 6 | UserPresence.startTimer(); 7 | return; 8 | } 9 | switch (newStatus) { 10 | case 'online': 11 | Meteor.call('UserPresence:online', UserPresence.userId); 12 | break; 13 | case 'away': 14 | Meteor.call('UserPresence:away', UserPresence.userId); 15 | UserPresence.stopTimer(); 16 | break; 17 | default: 18 | return; 19 | } 20 | status = newStatus; 21 | }, 1000); 22 | 23 | UserPresence = { 24 | awayTime: 60000, // 1 minute 25 | awayOnWindowBlur: false, 26 | callbacks: [], 27 | connected: true, 28 | started: false, 29 | userId: null, 30 | 31 | /** 32 | * The callback will receive the following parameters: user, status 33 | */ 34 | onSetUserStatus: function(callback) { 35 | this.callbacks.push(callback); 36 | }, 37 | 38 | runCallbacks: function(user, status) { 39 | this.callbacks.forEach(function(callback) { 40 | callback.call(null, user, status); 41 | }); 42 | }, 43 | 44 | startTimer: function() { 45 | UserPresence.stopTimer(); 46 | if (!UserPresence.awayTime) { 47 | return; 48 | } 49 | timer = setTimeout(UserPresence.setAway, UserPresence.awayTime); 50 | }, 51 | stopTimer: function() { 52 | clearTimeout(timer); 53 | }, 54 | restartTimer: function() { 55 | UserPresence.startTimer(); 56 | }, 57 | setAway: () => setUserPresence('away'), 58 | setOnline: () => setUserPresence('online'), 59 | start: function(userId) { 60 | // after first call overwrite start function to only call startTimer 61 | this.start = () => { this.startTimer(); }; 62 | this.userId = userId; 63 | 64 | // register a tracker on connection status so we can setup the away timer again (on reconnect) 65 | Tracker.autorun(() => { 66 | const { connected } = Meteor.status(); 67 | this.connected = connected; 68 | if (connected) { 69 | this.startTimer(); 70 | status = 'online'; 71 | return; 72 | } 73 | this.stopTimer(); 74 | status = 'offline'; 75 | }); 76 | 77 | ['mousemove', 'mousedown', 'touchend', 'keydown'] 78 | .forEach(key => document.addEventListener(key, this.setOnline)); 79 | 80 | window.addEventListener('focus', this.setOnline); 81 | 82 | if (this.awayOnWindowBlur === true) { 83 | window.addEventListener('blur', this.setAway); 84 | } 85 | } 86 | }; 87 | 88 | Meteor.methods({ 89 | 'UserPresence:setDefaultStatus': function(status) { 90 | check(status, String); 91 | Meteor.users.update({_id: Meteor.userId()}, {$set: { status, statusDefault: status }}); 92 | }, 93 | 'UserPresence:online': function() { 94 | const user = Meteor.user(); 95 | if (user && user.status !== 'online' && user.statusDefault === 'online') { 96 | Meteor.users.update({_id: Meteor.userId()}, {$set: {status: 'online'}}); 97 | } 98 | UserPresence.runCallbacks(user, 'online'); 99 | }, 100 | 'UserPresence:away': function() { 101 | var user = Meteor.user(); 102 | UserPresence.runCallbacks(user, 'away'); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /common/common.js: -------------------------------------------------------------------------------- 1 | /* globals UsersSessions */ 2 | /* exported UsersSessions */ 3 | 4 | UsersSessions = new Meteor.Collection('usersSessions'); 5 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'konecty:user-presence', 3 | summary: 'Track user status', 4 | version: '2.6.3', 5 | git: 'https://github.com/Konecty/meteor-user-presence' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.0.2.1'); 10 | 11 | api.use('tracker'); 12 | api.use('check'); 13 | api.use('ecmascript@0.12.2'); 14 | 15 | api.addFiles('common/common.js'); 16 | api.addFiles('server/server.js', ['server']); 17 | api.addFiles('server/monitor.js', ['server']); 18 | api.addFiles('client/client.js', ['client']); 19 | 20 | api.export(['UserPresence', 'UsersSessions'], ['server', 'client']); 21 | api.export(['UserPresenceMonitor', 'UserPresenceEvents'], ['server']); 22 | }); 23 | 24 | Package.onTest(function(api) { 25 | api.use('tinytest'); 26 | api.use('konecty:user-presence'); 27 | api.addFiles('user-presence-tests.js'); 28 | }); 29 | 30 | Npm.depends({ 31 | 'colors': '1.3.2' 32 | }); 33 | -------------------------------------------------------------------------------- /server/monitor.js: -------------------------------------------------------------------------------- 1 | /* globals UserPresenceMonitor, UsersSessions, InstanceStatus */ 2 | var EventEmitter = Npm.require('events'); 3 | 4 | UserPresenceEvents = new EventEmitter(); 5 | 6 | function monitorUsersSessions() { 7 | UsersSessions.find({}).observe({ 8 | added: function(record) { 9 | UserPresenceMonitor.processUserSession(record, 'added'); 10 | }, 11 | changed: function(record) { 12 | UserPresenceMonitor.processUserSession(record, 'changed'); 13 | }, 14 | removed: function(record) { 15 | UserPresenceMonitor.processUserSession(record, 'removed'); 16 | } 17 | }); 18 | } 19 | 20 | function monitorDeletedServers() { 21 | InstanceStatus.getCollection().find({}, {fields: {_id: 1}}).observeChanges({ 22 | removed: function(id) { 23 | UserPresence.removeConnectionsByInstanceId(id); 24 | } 25 | }); 26 | } 27 | 28 | function removeLostConnections() { 29 | if (!Package['konecty:multiple-instances-status']) { 30 | return UsersSessions.remove({}); 31 | } 32 | 33 | var ids = InstanceStatus.getCollection().find({}, {fields: {_id: 1}}).fetch().map(function(id) { 34 | return id._id; 35 | }); 36 | 37 | var update = { 38 | $pull: { 39 | connections: { 40 | instanceId: { 41 | $nin: ids 42 | } 43 | } 44 | } 45 | }; 46 | UsersSessions.update({}, update, {multi: true}); 47 | } 48 | 49 | UserPresenceMonitor = { 50 | /** 51 | * The callback will receive the following parameters: user, status, statusConnection 52 | */ 53 | onSetUserStatus: function(callback) { 54 | UserPresenceEvents.on('setUserStatus', callback); 55 | }, 56 | 57 | // following actions/observers will run only when presence monitor turned on 58 | start: function() { 59 | monitorUsersSessions(); 60 | removeLostConnections(); 61 | 62 | if (Package['konecty:multiple-instances-status']) { 63 | monitorDeletedServers(); 64 | } 65 | }, 66 | 67 | processUserSession: function(record, action) { 68 | if (action === 'removed' && (record.connections == null || record.connections.length === 0)) { 69 | return; 70 | } 71 | 72 | if (record.connections == null || record.connections.length === 0 || action === 'removed') { 73 | UserPresenceMonitor.setStatus(record._id, 'offline', record.metadata); 74 | 75 | if (action !== 'removed') { 76 | UsersSessions.remove({_id: record._id, 'connections.0': {$exists: false} }); 77 | } 78 | return; 79 | } 80 | 81 | var connectionStatus = 'offline'; 82 | record.connections.forEach(function(connection) { 83 | if (connection.status === 'online') { 84 | connectionStatus = 'online'; 85 | } else if (connection.status === 'away' && connectionStatus === 'offline') { 86 | connectionStatus = 'away'; 87 | } 88 | }); 89 | 90 | UserPresenceMonitor.setStatus(record._id, connectionStatus, record.metadata); 91 | }, 92 | 93 | processUser: function(id, fields) { 94 | if (fields.statusDefault == null) { 95 | return; 96 | } 97 | 98 | var userSession = UsersSessions.findOne({_id: id}); 99 | 100 | if (userSession) { 101 | UserPresenceMonitor.processUserSession(userSession, 'changed'); 102 | } 103 | }, 104 | 105 | setStatus: function(id, status, metadata) { 106 | UserPresenceEvents.emit('setStatus', id, status, metadata); 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* globals InstanceStatus, UsersSessions, UserPresenceMonitor, UserPresence */ 2 | import 'colors'; 3 | 4 | UsersSessions._ensureIndex({'connections.instanceId': 1}, {sparse: 1, name: 'connections.instanceId'}); 5 | UsersSessions._ensureIndex({'connections.id': 1}, {sparse: 1, name: 'connections.id'}); 6 | 7 | var allowedStatus = ['online', 'away', 'busy', 'offline']; 8 | 9 | var logEnable = process.env.ENABLE_PRESENCE_LOGS === 'true'; 10 | 11 | var log = function(msg, color) { 12 | if (logEnable) { 13 | if (color) { 14 | console.log(msg[color]); 15 | } else { 16 | console.log(msg); 17 | } 18 | } 19 | }; 20 | 21 | var logRed = function() { 22 | log(Array.prototype.slice.call(arguments).join(' '), 'red'); 23 | }; 24 | var logGrey = function() { 25 | log(Array.prototype.slice.call(arguments).join(' '), 'grey'); 26 | }; 27 | var logGreen = function() { 28 | log(Array.prototype.slice.call(arguments).join(' '), 'green'); 29 | }; 30 | var logYellow = function() { 31 | log(Array.prototype.slice.call(arguments).join(' '), 'yellow'); 32 | }; 33 | 34 | var checkUser = function(id, userId) { 35 | if (!id || !userId || id === userId) { 36 | return true; 37 | } 38 | var user = Meteor.users.findOne(id, { fields: { _id: 1 } }); 39 | if (user) { 40 | throw new Meteor.Error('cannot-change-other-users-status'); 41 | } 42 | 43 | return true; 44 | } 45 | 46 | UserPresence = { 47 | activeLogs: function() { 48 | logEnable = true; 49 | }, 50 | 51 | removeConnectionsByInstanceId: function(instanceId) { 52 | logRed('[user-presence] removeConnectionsByInstanceId', instanceId); 53 | var update = { 54 | $pull: { 55 | connections: { 56 | instanceId: instanceId 57 | } 58 | } 59 | }; 60 | 61 | UsersSessions.update({}, update, {multi: true}); 62 | }, 63 | 64 | removeAllConnections: function() { 65 | logRed('[user-presence] removeAllConnections'); 66 | UsersSessions.remove({}); 67 | }, 68 | 69 | getConnectionHandle(connectionId) { 70 | const internalConnection = Meteor.server.sessions.get(connectionId); 71 | 72 | if (!internalConnection) { 73 | return; 74 | } 75 | 76 | return internalConnection.connectionHandle; 77 | }, 78 | 79 | createConnection: function(userId, connection, status, metadata) { 80 | // if connections is invalid, does not have an userId or is already closed, don't save it on db 81 | if (!userId || !connection.id) { 82 | return; 83 | } 84 | 85 | const connectionHandle = UserPresence.getConnectionHandle(connection.id); 86 | 87 | if (!connectionHandle || connectionHandle.closed) { 88 | return; 89 | } 90 | 91 | connectionHandle.UserPresenceUserId = userId; 92 | 93 | status = status || 'online'; 94 | 95 | logGreen('[user-presence] createConnection', userId, connection.id, status, metadata); 96 | 97 | var query = { 98 | _id: userId 99 | }; 100 | 101 | var now = new Date(); 102 | 103 | var instanceId = undefined; 104 | if (Package['konecty:multiple-instances-status']) { 105 | instanceId = InstanceStatus.id(); 106 | } 107 | 108 | var update = { 109 | $push: { 110 | connections: { 111 | id: connection.id, 112 | instanceId: instanceId, 113 | status: status, 114 | _createdAt: now, 115 | _updatedAt: now 116 | } 117 | } 118 | }; 119 | 120 | if (metadata) { 121 | update.$set = { 122 | metadata: metadata 123 | }; 124 | connection.metadata = metadata; 125 | } 126 | 127 | // make sure closed connections are being created 128 | if (!connectionHandle.closed) { 129 | UsersSessions.upsert(query, update); 130 | } 131 | }, 132 | 133 | setConnection: function(userId, connection, status) { 134 | if (!userId) { 135 | return; 136 | } 137 | 138 | logGrey('[user-presence] setConnection', userId, connection.id, status); 139 | 140 | var query = { 141 | _id: userId, 142 | 'connections.id': connection.id 143 | }; 144 | 145 | var now = new Date(); 146 | 147 | var update = { 148 | $set: { 149 | 'connections.$.status': status, 150 | 'connections.$._updatedAt': now 151 | } 152 | }; 153 | 154 | if (connection.metadata) { 155 | update.$set.metadata = connection.metadata; 156 | } 157 | 158 | var count = UsersSessions.update(query, update); 159 | 160 | if (count === 0) { 161 | return UserPresence.createConnection(userId, connection, status, connection.metadata); 162 | } 163 | 164 | if (status === 'online') { 165 | Meteor.users.update({_id: userId, statusDefault: 'online', status: {$ne: 'online'}}, {$set: {status: 'online'}}); 166 | } else if (status === 'away') { 167 | Meteor.users.update({_id: userId, statusDefault: 'online', status: {$ne: 'away'}}, {$set: {status: 'away'}}); 168 | } 169 | }, 170 | 171 | setDefaultStatus: function(userId, status) { 172 | if (!userId) { 173 | return; 174 | } 175 | 176 | if (allowedStatus.indexOf(status) === -1) { 177 | return; 178 | } 179 | 180 | logYellow('[user-presence] setDefaultStatus', userId, status); 181 | 182 | var update = Meteor.users.update({_id: userId, statusDefault: {$ne: status}}, {$set: {statusDefault: status}}); 183 | 184 | if (update > 0) { 185 | UserPresenceMonitor.processUser(userId, { statusDefault: status }); 186 | } 187 | }, 188 | 189 | removeConnection: function(connectionId) { 190 | logRed('[user-presence] removeConnection', connectionId); 191 | 192 | var query = { 193 | 'connections.id': connectionId 194 | }; 195 | 196 | var update = { 197 | $pull: { 198 | connections: { 199 | id: connectionId 200 | } 201 | } 202 | }; 203 | 204 | return UsersSessions.update(query, update); 205 | }, 206 | 207 | start: function() { 208 | Meteor.onConnection(function(connection) { 209 | const session = Meteor.server.sessions.get(connection.id); 210 | 211 | connection.onClose(function() { 212 | if (!session) { 213 | return; 214 | } 215 | 216 | const connectionHandle = session.connectionHandle; 217 | 218 | // mark connection as closed so if it drops in the middle of the process it doesn't even is created 219 | if (!connectionHandle) { 220 | return; 221 | } 222 | connectionHandle.closed = true; 223 | 224 | if (connectionHandle.UserPresenceUserId != null) { 225 | UserPresence.removeConnection(connection.id); 226 | } 227 | }); 228 | }); 229 | 230 | process.on('exit', Meteor.bindEnvironment(function() { 231 | if (Package['konecty:multiple-instances-status']) { 232 | UserPresence.removeConnectionsByInstanceId(InstanceStatus.id()); 233 | } else { 234 | UserPresence.removeAllConnections(); 235 | } 236 | })); 237 | 238 | if (Package['accounts-base']) { 239 | Accounts.onLogin(function(login) { 240 | UserPresence.createConnection(login.user._id, login.connection); 241 | }); 242 | 243 | Accounts.onLogout(function(login) { 244 | UserPresence.removeConnection(login.connection.id); 245 | }); 246 | } 247 | 248 | Meteor.publish(null, function() { 249 | if (this.userId == null && this.connection && this.connection.id) { 250 | const connectionHandle = UserPresence.getConnectionHandle(this.connection.id); 251 | if (connectionHandle && connectionHandle.UserPresenceUserId != null) { 252 | UserPresence.removeConnection(this.connection.id); 253 | } 254 | } 255 | 256 | this.ready(); 257 | }); 258 | 259 | UserPresenceEvents.on('setStatus', function(userId, status) { 260 | var user = Meteor.users.findOne(userId); 261 | var statusConnection = status; 262 | 263 | if (!user) { 264 | return; 265 | } 266 | 267 | if (user.statusDefault != null && status !== 'offline' && user.statusDefault !== 'online') { 268 | status = user.statusDefault; 269 | } 270 | 271 | var query = { 272 | _id: userId, 273 | $or: [ 274 | {status: {$ne: status}}, 275 | {statusConnection: {$ne: statusConnection}} 276 | ] 277 | }; 278 | 279 | var update = { 280 | $set: { 281 | status: status, 282 | statusConnection: statusConnection 283 | } 284 | }; 285 | 286 | const result = Meteor.users.update(query, update); 287 | 288 | // if nothing updated, do not emit anything 289 | if (result) { 290 | UserPresenceEvents.emit('setUserStatus', user, status, statusConnection); 291 | } 292 | }); 293 | 294 | Meteor.methods({ 295 | 'UserPresence:connect': function(id, metadata) { 296 | check(id, Match.Maybe(String)); 297 | check(metadata, Match.Maybe(Object)); 298 | this.unblock(); 299 | checkUser(id, this.userId); 300 | UserPresence.createConnection(id || this.userId, this.connection, 'online', metadata); 301 | }, 302 | 303 | 'UserPresence:away': function(id) { 304 | check(id, Match.Maybe(String)); 305 | this.unblock(); 306 | checkUser(id, this.userId); 307 | UserPresence.setConnection(id || this.userId, this.connection, 'away'); 308 | }, 309 | 310 | 'UserPresence:online': function(id) { 311 | check(id, Match.Maybe(String)); 312 | this.unblock(); 313 | checkUser(id, this.userId); 314 | UserPresence.setConnection(id || this.userId, this.connection, 'online'); 315 | }, 316 | 317 | 'UserPresence:setDefaultStatus': function(id, status) { 318 | check(id, Match.Maybe(String)); 319 | check(status, Match.Maybe(String)); 320 | this.unblock(); 321 | 322 | // backward compatible (receives status as first argument) 323 | if (arguments.length === 1) { 324 | UserPresence.setDefaultStatus(this.userId, id); 325 | return; 326 | } 327 | checkUser(id, this.userId); 328 | UserPresence.setDefaultStatus(id || this.userId, status); 329 | } 330 | }); 331 | } 332 | }; 333 | -------------------------------------------------------------------------------- /user-presence-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | Tinytest.add('example', function (test) { 4 | test.equal(true, true); 5 | }); 6 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait) { 2 | let timeout; 3 | 4 | return (...args) => { 5 | if (timeout) clearTimeout(timeout); 6 | timeout = setTimeout(() => func(...args), wait); 7 | }; 8 | }; 9 | --------------------------------------------------------------------------------