Skip to content

Commit

Permalink
Merge pull request #8 from kb0304/contacts-discovery
Browse files Browse the repository at this point in the history
Contacts Discovery
  • Loading branch information
ear-dev committed Jan 3, 2019
2 parents 1b6074b + cf1b023 commit 7395058
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 2 deletions.
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ rocketchat:lazy-load
tap:i18n
underscore@1.0.10
rocketchat:bigbluebutton
rocketchat:contacts
rocketchat:mailmessages
juliancwirko:postcss
littledata:synced-cron
1 change: 1 addition & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ rocketchat:cas@1.0.0
rocketchat:channel-settings@0.0.1
rocketchat:channel-settings-mail-messages@0.0.1
rocketchat:colors@0.0.1
rocketchat:contacts@0.0.1
rocketchat:cors@0.0.1
rocketchat:crowd@1.0.0
rocketchat:custom-oauth@1.0.0
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "Rocket.Chat",
"description": "The Ultimate Open Source WebChat Platform",
"version": "0.73.0-develop",

"author": {
"name": "Rocket.Chat",
"url": "https://rocket.chat/"
Expand Down Expand Up @@ -151,6 +150,7 @@
"fibers": "^3.1.1",
"file-type": "^10.6.0",
"filesize": "^3.6.1",
"google-libphonenumber": "3.2.1",
"grapheme-splitter": "^1.0.4",
"gridfs-stream": "^1.1.1",
"he": "^1.2.0",
Expand Down
19 changes: 19 additions & 0 deletions packages/rocketchat-api/server/v1/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { TAPi18n } from 'meteor/tap:i18n';
import { RocketChat } from 'meteor/rocketchat:lib';
import { PhoneNumberUtil } from 'google-libphonenumber';

const phoneUtil = PhoneNumberUtil.getInstance();

RocketChat.API.v1.addRoute('info', { authRequired: false }, {
get() {
Expand Down Expand Up @@ -202,6 +205,10 @@ RocketChat.API.v1.addRoute('invite.sms', { authRequired: true }, {
throw new Meteor.Error('error-phone-param-not-provided', 'The required "phone" param is required.');
}
const phone = this.bodyParams.phone.replace(/-|\s/g, '');
if (!phoneUtil.isValidNumber(phoneUtil.parse(phone))) {
return RocketChat.API.v1.failure('Invalid number');
}

const result = Meteor.runAsUser(this.userId, () => Meteor.call('sendInvitationSMS', [phone]));
if (result.indexOf(phone) >= 0) {
return RocketChat.API.v1.success();
Expand All @@ -210,3 +217,15 @@ RocketChat.API.v1.addRoute('invite.sms', { authRequired: true }, {
}
},
});

RocketChat.API.v1.addRoute('query.contacts', { authRequired: true }, {
post() {
const hashes = this.bodyParams.weakHashes;
if (!hashes) {
return RocketChat.API.v1.failure('weakHashes param not present.');
}
const result = Meteor.runAsUser(this.userId, () => Meteor.call('queryContacts', hashes));
return RocketChat.API.v1.success({ strongHashes:result });

},
});
14 changes: 14 additions & 0 deletions packages/rocketchat-contacts/package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Package.describe({
name: 'rocketchat:contacts',
version: '0.0.1',
git: '',
});

Package.onUse(function(api) {
api.use('rocketchat:lib');
api.use('ecmascript');

api.addFiles('server/index.js', 'server');
api.addFiles('server/service.js', 'server');
api.addFiles('server/startup.js', 'server');
});
114 changes: 114 additions & 0 deletions packages/rocketchat-contacts/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* globals SyncedCron */

import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
const service = require('./service.js');
const provider = new service.Provider();

function refreshContactsHashMap() {
let phoneFieldName = '';
RocketChat.settings.get('Contacts_Phone_Custom_Field_Name', function(name, fieldName) {
phoneFieldName = fieldName;
});

let emailFieldName = '';
RocketChat.settings.get('Contacts_Email_Custom_Field_Name', function(name, fieldName) {
emailFieldName = fieldName;
});

let useDefaultEmails = false;
RocketChat.settings.get('Contacts_Use_Default_Emails', function(name, fieldName) {
useDefaultEmails = fieldName;
});

const contacts = [];
const cursor = Meteor.users.find({ active:true });

let phoneFieldArray = [];
if (phoneFieldName) {
phoneFieldArray = phoneFieldName.split(',');
}

let emailFieldArray = [];
if (emailFieldName) {
emailFieldArray = emailFieldName.split(',');
}

let dict;

const phonePattern = /^\+?[1-9]\d{1,14}$/;
const rfcMailPattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
cursor.forEach((user) => {
const discoverable = RocketChat.getUserPreference(user, 'isPublicAccount');
if (discoverable !== false) {
if (phoneFieldArray.length > 0) {
dict = user;
for (let i = 0;i < phoneFieldArray.length - 1;i++) {
if (phoneFieldArray[i] in dict) {
dict = dict[phoneFieldArray[i]];
}
}
let phone = dict[phoneFieldArray[phoneFieldArray.length - 1]];
if (phone && _.isString(phone)) {
phone = phone.replace(/[^0-9+]|_/g, '');
if (phonePattern.test(phone)) {
contacts.push({ d:phone, u:user.username });
}
}

}

if (emailFieldArray.length > 0) {
dict = user;
for (let i = 0;i < emailFieldArray.length - 1;i++) {
if (emailFieldArray[i] in dict) {
dict = dict[emailFieldArray[i]];
}
}
const email = dict[emailFieldArray[emailFieldArray.length - 1]];
if (email && _.isString(email)) {
if (rfcMailPattern.test(email)) {
contacts.push({ d:email, u:user.username });
}
}
}

if (useDefaultEmails && 'emails' in user) {
user.emails.forEach((email) => {
if (email.verified) {
contacts.push({ d:email.address, u:user.username });
}
});
}
}
});
provider.setHashedMap(provider.generateHashedMap(contacts));
}

Meteor.methods({
queryContacts(weakHashes) {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'queryContactse',
});
}
return provider.queryContacts(weakHashes);
},
});

const jobName = 'Refresh_Contacts_Hashes';

Meteor.startup(() => {
Meteor.defer(() => {
refreshContactsHashMap();

RocketChat.settings.get('Contacts_Background_Sync_Interval', function(name, processingFrequency) {
SyncedCron.remove(jobName);
SyncedCron.add({
name: jobName,
schedule: (parser) => parser.cron(`*/${ processingFrequency } * * * *`),
job: refreshContactsHashMap,
});
});
});
});
81 changes: 81 additions & 0 deletions packages/rocketchat-contacts/server/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const crypto = require('crypto');

class ContactsProvider {

constructor() {
this.contactsWeakHashMap = {};
}

addContact(contact, username) {
const weakHash = this.getWeakHash(contact);
const strongHash = this.getStrongHash(contact);

if (weakHash in this.contactsWeakHashMap) {
if (this.contactsWeakHashMap[weakHash].indexOf(strongHash) === -1) {
this.contactsWeakHashMap[weakHash].push({
h:strongHash,
u:username,
});
}
} else {
this.contactsWeakHashMap[weakHash] = [{ h:strongHash, u:username }];
}
}

generateHashedMap(contacts) {
const contactsWeakHashMap = {};
contacts.forEach((contact) => {
const weakHash = this.getWeakHash(contact.d);
const strongHash = this.getStrongHash(contact.d);
if (weakHash in contactsWeakHashMap) {
if (contactsWeakHashMap[weakHash].indexOf(strongHash) === -1) {
contactsWeakHashMap[weakHash].push({ h:strongHash, u:contact.u });
}
} else {
contactsWeakHashMap[weakHash] = [{ h:strongHash, u:contact.u }];
}
});
return contactsWeakHashMap;
}

setHashedMap(contactsWeakHashMap) {
this.contactsWeakHashMap = contactsWeakHashMap;
}

getStrongHash(contact) {
return crypto.createHash('sha1').update(contact).digest('hex');
}

getWeakHash(contact) {
return crypto.createHash('sha1').update(contact).digest('hex').substr(3, 6);
}

queryContacts(contactWeakHashList) {
let result = [];
contactWeakHashList.forEach((weakHash) => {
if (weakHash in this.contactsWeakHashMap) {
result = result.concat(this.contactsWeakHashMap[weakHash]);
}
});
return result;
}

removeContact(contact, username) {
const weakHash = this.getWeakHash(contact);
const strongHash = this.getStrongHash(contact);

if (weakHash in this.contactsWeakHashMap && this.contactsWeakHashMap[weakHash].indexOf(strongHash) >= 0) {
this.contactsWeakHashMap[weakHash].splice(this.contactsWeakHashMap[weakHash].indexOf({ h:strongHash, u:username }), 1);

if (!this.contactsWeakHashMap[weakHash].length) { delete this.contactsWeakHashMap[weakHash]; }
}
}

reset() {
this.contactsWeakHashMap = {};
}
}

module.exports = {
Provider: ContactsProvider,
};
25 changes: 25 additions & 0 deletions packages/rocketchat-contacts/server/startup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
RocketChat.settings.addGroup('Contacts', function() {
this.add('Contacts_Phone_Custom_Field_Name', '', {
type: 'string',
public: true,
i18nDescription: 'Contacts_Phone_Custom_Field_Name_Description',
});

this.add('Contacts_Use_Default_Emails', true, {
type: 'boolean',
public: true,
i18nDescription: 'Contacts_Use_Default_Emails_Description',
});

this.add('Contacts_Email_Custom_Field_Name', '', {
type: 'string',
public: true,
i18nDescription: 'Contacts_Email_Custom_Field_Name_Description',
});

this.add('Contacts_Background_Sync_Interval', 10, {
type: 'int',
public: true,
i18nDescription: 'Contacts_Background_Sync_Interval_Description',
});
});
4 changes: 4 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@
"Consumer_Goods": "Consumer Goods",
"Contains_Security_Fixes": "Contains Security Fixes",
"Contact": "Contact",
"Contacts_Background_Sync_Interval_Description":"Interval in minutes after which the contacts are synced form db by the discovery service",
"Contacts_Email_Custom_Field_Name_Description":"Comma seperated keys in hierarcy order to access email in the User object. Eg. services,ssotest,email . Can be used in addition to default emails.",
"Contacts_Phone_Custom_Field_Name_Description":"Comma seperated keys in hierarcy order to access phone number in User object. Eg. services,ssotest,telephoneNumber",
"Contacts_Use_Default_Emails_Description":"Use verified emails from the user accounts",
"Content": "Content",
"Continue": "Continue",
"Continuous_sound_notifications_for_new_livechat_room": "Continuous sound notifications for new livechat room",
Expand Down
5 changes: 5 additions & 0 deletions packages/rocketchat-lib/server/startup/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,11 @@ RocketChat.settings.addGroup('Accounts', function() {
public: true,
i18nLabel: 'Notifications_Sound_Volume',
});
this.add('Accounts_Default_User_Preferences_isPublicAccount', true, {
type: 'boolean',
public: true,
i18nLabel: 'Is_Public_Account',
});
});

this.section('Avatar', function() {
Expand Down
7 changes: 7 additions & 0 deletions packages/rocketchat-ui-account/client/accountPreferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ <h1>{{_ "Localization"}}</h1>
<div class="section">
<h1>{{_ "Global"}}</h1>
<div class="section-content border-component-color">
<div class="input-line double-col" id="isPublicAccount">
<label class="setting-label">{{_ "Public_Account"}}</label>
<div class="setting-field">
<label><input type="radio" name="isPublicAccount" value="true" checked="{{ checked 'isPublicAccount' true }}"/> {{_ "True"}}</label>
<label><input type="radio" name="isPublicAccount" value="false" checked="{{ checked 'isPublicAccount' false }}"/> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label for="dont-ask" class="setting-label">{{_ "Dont_ask_me_again_list"}}</label>
<div class="rc-select setting-field">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ Template.accountPreferences.onCreated(function() {
return s.trim(e);
}));
data.dontAskAgainList = Array.from(document.getElementById('dont-ask').options).map((option) => ({ action: option.value, label: option.text }));
data.isPublicAccount = JSON.parse($('#isPublicAccount').find('input:checked').val());

let reload = false;

Expand Down
2 changes: 1 addition & 1 deletion tests/end-to-end/api/00-miscellaneous.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('miscellaneous', function() {
'saveMobileBandwidth', 'collapseMediaByDefault', 'hideUsernames', 'hideRoles', 'hideFlexTab', 'hideAvatars',
'sidebarViewMode', 'sidebarHideAvatar', 'sidebarShowUnread', 'sidebarShowFavorites', 'sidebarGroupByType',
'sendOnEnter', 'messageViewMode', 'emailNotificationMode', 'roomCounterSidebar', 'newRoomNotification', 'newMessageNotification',
'muteFocusedConversations', 'notificationsSoundVolume'];
'muteFocusedConversations', 'notificationsSoundVolume', 'isPublicAccount'];
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', credentials['X-User-Id']);
expect(res.body).to.have.property('username', login.user);
Expand Down

0 comments on commit 7395058

Please sign in to comment.