- {{> icon block="mail-messages__instructions-icon" icon="modal-success"}}
+ {{> icon block="mail-messages__instructions-icon rc-icon--default-size" icon="checkmark-circled"}}
{{selectedMessages.length}} Messages selected
Click here to clear the selection
@@ -13,7 +13,7 @@
{{else}}
- {{> icon block="mail-messages__instructions-icon" icon="hand-pointer"}}
+ {{> icon block="mail-messages__instructions-icon rc-icon--default-size" icon="hand-pointer"}}
{{_ "Click_the_messages_you_would_like_to_send_by_email"}}
@@ -25,7 +25,7 @@
{{_ "To_users"}}
+
+
{{_ "Agents"}}
diff --git a/packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js
similarity index 86%
rename from packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.js
rename to app/livechat/client/views/app/livechatDepartmentForm.js
index 1ecfeddd6966..5a807d3c10d1 100644
--- a/packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.js
+++ b/app/livechat/client/views/app/livechatDepartmentForm.js
@@ -2,13 +2,13 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { t } from 'meteor/rocketchat:utils';
-import { handleError } from 'meteor/rocketchat:lib';
+import { t, handleError } from '../../../../utils';
import { AgentUsers } from '../../collections/AgentUsers';
import { LivechatDepartment } from '../../collections/LivechatDepartment';
import { LivechatDepartmentAgents } from '../../collections/LivechatDepartmentAgents';
import _ from 'underscore';
import toastr from 'toastr';
+import './livechatDepartmentForm.html';
Template.livechatDepartmentForm.helpers({
department() {
@@ -28,6 +28,10 @@ Template.livechatDepartmentForm.helpers({
const department = Template.instance().department.get();
return department.showOnRegistration === value || (department.showOnRegistration === undefined && value === true);
},
+ showOnOfflineForm(value) {
+ const department = Template.instance().department.get();
+ return department.showOnOfflineForm === value || (department.showOnOfflineForm === undefined && value === true);
+ },
});
Template.livechatDepartmentForm.events({
@@ -40,6 +44,8 @@ Template.livechatDepartmentForm.events({
const name = instance.$('input[name=name]').val();
const description = instance.$('textarea[name=description]').val();
const showOnRegistration = instance.$('input[name=showOnRegistration]:checked').val();
+ const email = instance.$('input[name=email]').val();
+ const showOnOfflineForm = instance.$('input[name=showOnOfflineForm]:checked').val();
if (enabled !== '1' && enabled !== '0') {
return toastr.error(t('Please_select_enabled_yes_or_no'));
@@ -49,6 +55,10 @@ Template.livechatDepartmentForm.events({
return toastr.error(t('Please_fill_a_name'));
}
+ if (email.trim() === '' && showOnOfflineForm === '1') {
+ return toastr.error(t('Please_fill_an_email'));
+ }
+
const oldBtnValue = $btn.html();
$btn.html(t('Saving'));
@@ -57,6 +67,8 @@ Template.livechatDepartmentForm.events({
name: name.trim(),
description: description.trim(),
showOnRegistration: showOnRegistration === '1',
+ showOnOfflineForm: showOnOfflineForm === '1',
+ email: email.trim(),
};
const departmentAgents = [];
diff --git a/packages/rocketchat-livechat/client/views/app/livechatDepartments.html b/app/livechat/client/views/app/livechatDepartments.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatDepartments.html
rename to app/livechat/client/views/app/livechatDepartments.html
diff --git a/app/livechat/client/views/app/livechatDepartments.js b/app/livechat/client/views/app/livechatDepartments.js
new file mode 100644
index 000000000000..49400dd8bd34
--- /dev/null
+++ b/app/livechat/client/views/app/livechatDepartments.js
@@ -0,0 +1,53 @@
+import { Meteor } from 'meteor/meteor';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Template } from 'meteor/templating';
+import { modal } from '../../../../ui-utils';
+import { t, handleError } from '../../../../utils';
+import { LivechatDepartment } from '../../collections/LivechatDepartment';
+import './livechatDepartments.html';
+
+Template.livechatDepartments.helpers({
+ departments() {
+ return LivechatDepartment.find();
+ },
+});
+
+Template.livechatDepartments.events({
+ 'click .remove-department'(e/* , instance*/) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ modal.open({
+ title: t('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: t('Yes'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: false,
+ html: false,
+ }, () => {
+ Meteor.call('livechat:removeDepartment', this._id, function(error/* , result*/) {
+ if (error) {
+ return handleError(error);
+ }
+ modal.open({
+ title: t('Removed'),
+ text: t('Department_removed'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
+ });
+ });
+ },
+
+ 'click .department-info'(e/* , instance*/) {
+ e.preventDefault();
+ FlowRouter.go('livechat-department-edit', { _id: this._id });
+ },
+});
+
+Template.livechatDepartments.onCreated(function() {
+ this.subscribe('livechat:departments');
+});
diff --git a/app/livechat/client/views/app/livechatInstallation.html b/app/livechat/client/views/app/livechatInstallation.html
new file mode 100644
index 000000000000..3aced66ab596
--- /dev/null
+++ b/app/livechat/client/views/app/livechatInstallation.html
@@ -0,0 +1,18 @@
+
+ {{#requiresPermission 'view-livechat-manager'}}
+ {{{_ "To_install_the_new_version_of_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site"}}}
+
+
+ {{script}}
+ {{_ "Copy_to_clipboard"}}
+
+
+ {{{_ "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site"}}}
+
+
+ {{oldScript}}
+ {{_ "Copy_to_clipboard"}}
+
+
+ {{/requiresPermission}}
+
diff --git a/app/livechat/client/views/app/livechatInstallation.js b/app/livechat/client/views/app/livechatInstallation.js
new file mode 100644
index 000000000000..2e468c95f1d8
--- /dev/null
+++ b/app/livechat/client/views/app/livechatInstallation.js
@@ -0,0 +1,36 @@
+import { Template } from 'meteor/templating';
+import { settings } from '../../../../settings';
+import s from 'underscore.string';
+import './livechatInstallation.html';
+
+const latestVersion = '1.0.0';
+
+Template.livechatInstallation.helpers({
+ oldScript() {
+ const siteUrl = s.rtrim(settings.get('Site_Url'), '/');
+ return `
+
+`;
+ },
+
+ script() {
+ const siteUrl = s.rtrim(settings.get('Site_Url'), '/');
+ return `
+
+`;
+ },
+});
diff --git a/packages/rocketchat-livechat/client/views/app/livechatNotSubscribed.html b/app/livechat/client/views/app/livechatNotSubscribed.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatNotSubscribed.html
rename to app/livechat/client/views/app/livechatNotSubscribed.html
diff --git a/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html b/app/livechat/client/views/app/livechatOfficeHours.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html
rename to app/livechat/client/views/app/livechatOfficeHours.html
diff --git a/app/livechat/client/views/app/livechatOfficeHours.js b/app/livechat/client/views/app/livechatOfficeHours.js
new file mode 100644
index 000000000000..04844d35e897
--- /dev/null
+++ b/app/livechat/client/views/app/livechatOfficeHours.js
@@ -0,0 +1,165 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { t, handleError } from '../../../../utils';
+import { settings } from '../../../../settings';
+import { LivechatOfficeHour } from '../../collections/livechatOfficeHour';
+import toastr from 'toastr';
+import moment from 'moment';
+import './livechatOfficeHours.html';
+
+Template.livechatOfficeHours.helpers({
+ days() {
+ return LivechatOfficeHour.find({}, { sort: { code: 1 } });
+ },
+ startName(day) {
+ return `${ day.day }_start`;
+ },
+ finishName(day) {
+ return `${ day.day }_finish`;
+ },
+ openName(day) {
+ return `${ day.day }_open`;
+ },
+ start(day) {
+ return Template.instance().dayVars[day.day].start.get();
+ },
+ finish(day) {
+ return Template.instance().dayVars[day.day].finish.get();
+
+ },
+ name(day) {
+ return TAPi18n.__(day.day);
+ },
+ open(day) {
+ return Template.instance().dayVars[day.day].open.get();
+ },
+ enableOfficeHoursTrueChecked() {
+ if (Template.instance().enableOfficeHours.get()) {
+ return 'checked';
+ }
+ },
+ enableOfficeHoursFalseChecked() {
+ if (!Template.instance().enableOfficeHours.get()) {
+ return 'checked';
+ }
+ },
+});
+
+Template.livechatOfficeHours.events({
+ 'change .preview-settings, keydown .preview-settings'(e, instance) {
+ const temp = e.currentTarget.name.split('_');
+
+ const newTime = moment(e.currentTarget.value, 'HH:mm');
+
+ // check if start and stop do not cross
+ if (temp[1] === 'start') {
+ if (newTime.isSameOrBefore(moment(instance.dayVars[temp[0]].finish.get(), 'HH:mm'))) {
+ instance.dayVars[temp[0]].start.set(e.currentTarget.value);
+ } else {
+ e.currentTarget.value = instance.dayVars[temp[0]].start.get();
+ }
+ } else if (temp[1] === 'finish') {
+ if (newTime.isSameOrAfter(moment(instance.dayVars[temp[0]].start.get(), 'HH:mm'))) {
+ instance.dayVars[temp[0]].finish.set(e.currentTarget.value);
+ } else {
+ e.currentTarget.value = instance.dayVars[temp[0]].finish.get();
+ }
+ }
+ },
+ 'change .dayOpenCheck input'(e, instance) {
+ const temp = e.currentTarget.name.split('_');
+ instance.dayVars[temp[0]][temp[1]].set(e.target.checked);
+ },
+ 'change .preview-settings, keyup .preview-settings'(e, instance) {
+ let { value } = e.currentTarget;
+ if (e.currentTarget.type === 'radio') {
+ value = value === 'true';
+ instance[e.currentTarget.name].set(value);
+ }
+ },
+ 'submit .rocket-form'(e, instance) {
+ e.preventDefault();
+
+ // convert all times to utc then update them in db
+ for (const d in instance.dayVars) {
+ if (instance.dayVars.hasOwnProperty(d)) {
+ const day = instance.dayVars[d];
+ const start_utc = moment(day.start.get(), 'HH:mm').utc().format('HH:mm');
+ const finish_utc = moment(day.finish.get(), 'HH:mm').utc().format('HH:mm');
+
+ Meteor.call('livechat:saveOfficeHours', d, start_utc, finish_utc, day.open.get(), function(err /* ,result*/) {
+ if (err) {
+ return handleError(err);
+ }
+ });
+ }
+ }
+
+ settings.set('Livechat_enable_office_hours', instance.enableOfficeHours.get(), (err/* , success*/) => {
+ if (err) {
+ return handleError(err);
+ }
+ toastr.success(t('Office_hours_updated'));
+ });
+ },
+});
+
+Template.livechatOfficeHours.onCreated(function() {
+ this.dayVars = {
+ Monday: {
+ start: new ReactiveVar('08:00'),
+ finish: new ReactiveVar('20:00'),
+ open: new ReactiveVar(true),
+ },
+ Tuesday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(true),
+ },
+ Wednesday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(true),
+ },
+ Thursday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(true),
+ },
+ Friday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(true),
+ },
+ Saturday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(false),
+ },
+ Sunday: {
+ start: new ReactiveVar('00:00'),
+ finish: new ReactiveVar('00:00'),
+ open: new ReactiveVar(false),
+ },
+ };
+
+ this.autorun(() => {
+ this.subscribe('livechat:officeHour');
+
+ if (this.subscriptionsReady()) {
+ LivechatOfficeHour.find().forEach(function(d) {
+ Template.instance().dayVars[d.day].start.set(moment.utc(d.start, 'HH:mm').local().format('HH:mm'));
+ Template.instance().dayVars[d.day].finish.set(moment.utc(d.finish, 'HH:mm').local().format('HH:mm'));
+ Template.instance().dayVars[d.day].open.set(d.open);
+ });
+ }
+ });
+
+ this.enableOfficeHours = new ReactiveVar(null);
+
+ this.autorun(() => {
+ this.enableOfficeHours.set(settings.get('Livechat_enable_office_hours'));
+ });
+});
diff --git a/packages/rocketchat-livechat/client/views/app/livechatQueue.html b/app/livechat/client/views/app/livechatQueue.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatQueue.html
rename to app/livechat/client/views/app/livechatQueue.html
diff --git a/app/livechat/client/views/app/livechatQueue.js b/app/livechat/client/views/app/livechatQueue.js
new file mode 100644
index 000000000000..7050de8e0b1e
--- /dev/null
+++ b/app/livechat/client/views/app/livechatQueue.js
@@ -0,0 +1,71 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { settings } from '../../../../settings';
+import { hasRole } from '../../../../authorization';
+import { Users } from '../../../../models';
+import { LivechatDepartment } from '../../collections/LivechatDepartment';
+import { LivechatQueueUser } from '../../collections/LivechatQueueUser';
+import { AgentUsers } from '../../collections/AgentUsers';
+import './livechatQueue.html';
+
+Template.livechatQueue.helpers({
+ departments() {
+ return LivechatDepartment.find({
+ enabled: true,
+ }, {
+ sort: {
+ name: 1,
+ },
+ });
+ },
+
+ users() {
+ const users = [];
+
+ const showOffline = Template.instance().showOffline.get();
+
+ LivechatQueueUser.find({
+ departmentId: this._id,
+ }, {
+ sort: {
+ count: 1,
+ order: 1,
+ username: 1,
+ },
+ }).forEach((user) => {
+ const options = { fields: { _id: 1 } };
+ const userFilter = { _id: user.agentId, status: { $ne: 'offline' } };
+ const agentFilter = { _id: user.agentId, statusLivechat: 'available' };
+
+ if (showOffline[this._id] || (Meteor.users.findOne(userFilter, options) && AgentUsers.findOne(agentFilter, options))) {
+ users.push(user);
+ }
+ });
+
+ return users;
+ },
+
+ hasPermission() {
+ const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } });
+ return hasRole(Meteor.userId(), 'livechat-manager') || (user.statusLivechat === 'available' && settings.get('Livechat_show_queue_list_link'));
+ },
+});
+
+Template.livechatQueue.events({
+ 'click .show-offline'(event, instance) {
+ const showOffline = instance.showOffline.get();
+
+ showOffline[this._id] = event.currentTarget.checked;
+
+ instance.showOffline.set(showOffline);
+ },
+});
+
+Template.livechatQueue.onCreated(function() {
+ this.showOffline = new ReactiveVar({});
+
+ this.subscribe('livechat:queue');
+ this.subscribe('livechat:agents');
+ this.subscribe('livechat:departments');
+});
diff --git a/app/livechat/client/views/app/livechatReadOnly.html b/app/livechat/client/views/app/livechatReadOnly.html
new file mode 100644
index 000000000000..9b2f6d484a2b
--- /dev/null
+++ b/app/livechat/client/views/app/livechatReadOnly.html
@@ -0,0 +1,16 @@
+
+ {{#if roomOpen}}
+ {{#if guestPool}}
+ {{#if inquiryOpen}}
+
+ {{{_ "you_are_in_preview_mode_of_incoming_livechat"}}}
+ {{_ "Take_it"}}
+
+ {{else}}
+ {{_ "room_is_read_only"}}
+ {{/if}}
+ {{/if}}
+ {{else}}
+ {{_ "This_conversation_is_already_closed"}}
+ {{/if}}
+
diff --git a/app/livechat/client/views/app/livechatReadOnly.js b/app/livechat/client/views/app/livechatReadOnly.js
new file mode 100644
index 000000000000..7f1cf2403fd9
--- /dev/null
+++ b/app/livechat/client/views/app/livechatReadOnly.js
@@ -0,0 +1,56 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { ChatRoom } from '../../../../models';
+import { LivechatInquiry } from '../../../lib/LivechatInquiry';
+import { call } from '../../../../ui-utils/client';
+import { settings } from '../../../../settings';
+import './livechatReadOnly.html';
+
+Template.livechatReadOnly.helpers({
+ inquiryOpen() {
+ const inquiry = Template.instance().inquiry.get();
+ return inquiry || FlowRouter.go('/home');
+ },
+
+ roomOpen() {
+ const room = Template.instance().room.get();
+ return room && room.open === true;
+ },
+
+ guestPool() {
+ return settings.get('Livechat_Routing_Method') === 'Guest_Pool';
+ },
+});
+
+Template.livechatReadOnly.events({
+ async 'click .js-take-it'(event, instance) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const inquiry = instance.inquiry.get();
+ const { _id } = inquiry;
+ await call('livechat:takeInquiry', _id);
+ },
+});
+
+Template.livechatReadOnly.onCreated(function() {
+ this.rid = Template.currentData().rid;
+ this.room = new ReactiveVar();
+ this.inquiry = new ReactiveVar();
+
+ this.autorun(() => {
+ const inquiry = LivechatInquiry.findOne({ agents: Meteor.userId(), status: 'open', rid: this.rid });
+ this.inquiry.set(inquiry);
+
+ if (inquiry) {
+ this.subscribe('livechat:inquiry', inquiry._id);
+ }
+ });
+
+ this.autorun(() => {
+ this.room.set(ChatRoom.findOne({ _id: Template.currentData().rid }, { fields: { open: 1 } }));
+ });
+
+});
diff --git a/packages/rocketchat-livechat/client/views/app/livechatTriggers.html b/app/livechat/client/views/app/livechatTriggers.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatTriggers.html
rename to app/livechat/client/views/app/livechatTriggers.html
diff --git a/app/livechat/client/views/app/livechatTriggers.js b/app/livechat/client/views/app/livechatTriggers.js
new file mode 100644
index 000000000000..0b690001e558
--- /dev/null
+++ b/app/livechat/client/views/app/livechatTriggers.js
@@ -0,0 +1,82 @@
+import { Meteor } from 'meteor/meteor';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Template } from 'meteor/templating';
+import { modal } from '../../../../ui-utils';
+import { t, handleError } from '../../../../utils';
+import { LivechatTrigger } from '../../collections/LivechatTrigger';
+import './livechatTriggers.html';
+
+Template.livechatTriggers.helpers({
+ triggers() {
+ return LivechatTrigger.find();
+ },
+});
+
+Template.livechatTriggers.events({
+ 'click .remove-trigger'(e/* , instance*/) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ modal.open({
+ title: t('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: t('Yes'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: false,
+ html: false,
+ }, () => {
+ Meteor.call('livechat:removeTrigger', this._id, function(error/* , result*/) {
+ if (error) {
+ return handleError(error);
+ }
+ modal.open({
+ title: t('Removed'),
+ text: t('Trigger_removed'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
+ });
+ });
+ },
+
+ 'click .trigger-info'(e/* , instance*/) {
+ e.preventDefault();
+ FlowRouter.go('livechat-trigger-edit', { _id: this._id });
+ },
+
+ 'click .delete-trigger'(e/* , instance*/) {
+ e.preventDefault();
+
+ modal.open({
+ title: t('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: t('Yes'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: false,
+ html: false,
+ }, () => {
+ Meteor.call('livechat:removeTrigger', this._id, function(error/* , result*/) {
+ if (error) {
+ return handleError(error);
+ }
+
+ modal.open({
+ title: t('Removed'),
+ text: t('Trigger_removed'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
+ });
+ });
+ },
+});
+
+Template.livechatTriggers.onCreated(function() {
+ this.subscribe('livechat:triggers');
+});
diff --git a/packages/rocketchat-livechat/client/views/app/livechatTriggersForm.html b/app/livechat/client/views/app/livechatTriggersForm.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatTriggersForm.html
rename to app/livechat/client/views/app/livechatTriggersForm.html
diff --git a/packages/rocketchat-livechat/client/views/app/livechatTriggersForm.js b/app/livechat/client/views/app/livechatTriggersForm.js
similarity index 96%
rename from packages/rocketchat-livechat/client/views/app/livechatTriggersForm.js
rename to app/livechat/client/views/app/livechatTriggersForm.js
index 5955bd3d8a9a..fbd4a5ebeb1f 100644
--- a/packages/rocketchat-livechat/client/views/app/livechatTriggersForm.js
+++ b/app/livechat/client/views/app/livechatTriggersForm.js
@@ -1,10 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { t } from 'meteor/rocketchat:utils';
-import { handleError } from 'meteor/rocketchat:lib';
+import { t, handleError } from '../../../../utils';
import { LivechatTrigger } from '../../collections/LivechatTrigger';
import toastr from 'toastr';
+import './livechatTriggersForm.html';
Template.livechatTriggersForm.helpers({
name() {
diff --git a/packages/rocketchat-livechat/client/views/app/livechatUsers.html b/app/livechat/client/views/app/livechatUsers.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/livechatUsers.html
rename to app/livechat/client/views/app/livechatUsers.html
diff --git a/packages/rocketchat-livechat/client/views/app/livechatUsers.js b/app/livechat/client/views/app/livechatUsers.js
similarity index 96%
rename from packages/rocketchat-livechat/client/views/app/livechatUsers.js
rename to app/livechat/client/views/app/livechatUsers.js
index 8e58dd89d114..df35acd776c2 100644
--- a/packages/rocketchat-livechat/client/views/app/livechatUsers.js
+++ b/app/livechat/client/views/app/livechatUsers.js
@@ -1,12 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
-import { modal } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
-import { handleError } from 'meteor/rocketchat:lib';
+import { modal } from '../../../../ui-utils';
+import { t, handleError } from '../../../../utils';
import { AgentUsers } from '../../collections/AgentUsers';
import _ from 'underscore';
import toastr from 'toastr';
+import './livechatUsers.html';
+
let ManagerUsers;
Meteor.startup(function() {
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.html b/app/livechat/client/views/app/tabbar/externalSearch.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.html
rename to app/livechat/client/views/app/tabbar/externalSearch.html
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.js b/app/livechat/client/views/app/tabbar/externalSearch.js
similarity index 75%
rename from packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.js
rename to app/livechat/client/views/app/tabbar/externalSearch.js
index e5146f59e9eb..75b844e6ade5 100644
--- a/packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.js
+++ b/app/livechat/client/views/app/tabbar/externalSearch.js
@@ -1,9 +1,10 @@
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { LivechatExternalMessage } from '../../../../lib/LivechatExternalMessage';
+import './externalSearch.html';
Template.externalSearch.helpers({
messages() {
- return RocketChat.models.LivechatExternalMessage.findByRoomId(this.rid, { ts: 1 });
+ return LivechatExternalMessage.findByRoomId(this.rid, { ts: 1 });
},
});
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorEdit.html b/app/livechat/client/views/app/tabbar/visitorEdit.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorEdit.html
rename to app/livechat/client/views/app/tabbar/visitorEdit.html
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorEdit.js b/app/livechat/client/views/app/tabbar/visitorEdit.js
similarity index 94%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorEdit.js
rename to app/livechat/client/views/app/tabbar/visitorEdit.js
index 139c1b549c91..55acc81f09cb 100644
--- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorEdit.js
+++ b/app/livechat/client/views/app/tabbar/visitorEdit.js
@@ -1,10 +1,11 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
-import { ChatRoom } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { ChatRoom } from '../../../../../models';
+import { t } from '../../../../../utils';
import { LivechatVisitor } from '../../../collections/LivechatVisitor';
import toastr from 'toastr';
+import './visitorEdit.html';
Template.visitorEdit.helpers({
visitor() {
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.html b/app/livechat/client/views/app/tabbar/visitorForward.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.html
rename to app/livechat/client/views/app/tabbar/visitorForward.html
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js
similarity index 95%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.js
rename to app/livechat/client/views/app/tabbar/visitorForward.js
index 6466ecd3354d..3b8dda292e00 100644
--- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.js
+++ b/app/livechat/client/views/app/tabbar/visitorForward.js
@@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { ChatRoom } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { ChatRoom } from '../../../../../models';
+import { t } from '../../../../../utils';
import { LivechatDepartment } from '../../../collections/LivechatDepartment';
import { AgentUsers } from '../../../collections/AgentUsers';
import toastr from 'toastr';
+import './visitorForward.html';
Template.visitorForward.helpers({
visitor() {
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorHistory.html b/app/livechat/client/views/app/tabbar/visitorHistory.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorHistory.html
rename to app/livechat/client/views/app/tabbar/visitorHistory.html
diff --git a/app/livechat/client/views/app/tabbar/visitorHistory.js b/app/livechat/client/views/app/tabbar/visitorHistory.js
new file mode 100644
index 000000000000..cb7760a6a872
--- /dev/null
+++ b/app/livechat/client/views/app/tabbar/visitorHistory.js
@@ -0,0 +1,49 @@
+import { ChatRoom } from '../../../../../models';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { Mongo } from 'meteor/mongo';
+import moment from 'moment';
+import './visitorHistory.html';
+
+const visitorHistory = new Mongo.Collection('visitor_history');
+
+Template.visitorHistory.helpers({
+ historyLoaded() {
+ return !Template.instance().loadHistory.ready();
+ },
+
+ previousChats() {
+ return visitorHistory.find({
+ _id: { $ne: this.rid },
+ 'v._id': Template.instance().visitorId.get(),
+ }, {
+ sort: {
+ ts: -1,
+ },
+ });
+ },
+
+ title() {
+ let title = moment(this.ts).format('L LTS');
+
+ if (this.label) {
+ title += ` - ${ this.label }`;
+ }
+
+ return title;
+ },
+});
+
+Template.visitorHistory.onCreated(function() {
+ const currentData = Template.currentData();
+ this.visitorId = new ReactiveVar();
+
+ this.autorun(() => {
+ const room = ChatRoom.findOne({ _id: Template.currentData().rid });
+ this.visitorId.set(room.v._id);
+ });
+
+ if (currentData && currentData.rid) {
+ this.loadHistory = this.subscribe('livechat:visitorHistory', { rid: currentData.rid });
+ }
+});
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html
similarity index 89%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorInfo.html
rename to app/livechat/client/views/app/tabbar/visitorInfo.html
index 9f5297ffd4cf..b6649092fb82 100644
--- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorInfo.html
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.html
@@ -38,6 +38,14 @@
{{username}}
{{/with}}
+
+ {{#with department}}
+
+
+ {{#if name}} {{_ "Department"}}: {{name}} {{/if}}
+
+
+ {{/with}}
{{#if canSeeButtons}}
@@ -46,10 +54,9 @@
{{username}}
{{#if roomOpen}}
{{_ "Close"}}
{{_ "Forward"}}
- {{/if}}
-
- {{#if guestPool}}
-
{{_ "Return"}}
+ {{#if guestPool}}
+
{{_ "Return"}}
+ {{/if}}
{{/if}}
{{/if}}
diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js
new file mode 100644
index 000000000000..c3edee3358ba
--- /dev/null
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.js
@@ -0,0 +1,275 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Session } from 'meteor/session';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { modal } from '../../../../../ui-utils';
+import { ChatRoom, Rooms, Subscriptions } from '../../../../../models';
+import { settings } from '../../../../../settings';
+import { t, handleError, roomTypes } from '../../../../../utils';
+import { hasRole } from '../../../../../authorization';
+import { LivechatVisitor } from '../../../collections/LivechatVisitor';
+import { LivechatDepartment } from '../../../collections/LivechatDepartment';
+import _ from 'underscore';
+import s from 'underscore.string';
+import moment from 'moment';
+import UAParser from 'ua-parser-js';
+import './visitorInfo.html';
+
+Template.visitorInfo.helpers({
+ user() {
+ const user = Template.instance().user.get();
+ if (user && user.userAgent) {
+ const ua = new UAParser();
+ ua.setUA(user.userAgent);
+
+ user.os = `${ ua.getOS().name } ${ ua.getOS().version }`;
+ if (['Mac OS', 'iOS'].indexOf(ua.getOS().name) !== -1) {
+ user.osIcon = 'icon-apple';
+ } else {
+ user.osIcon = `icon-${ ua.getOS().name.toLowerCase() }`;
+ }
+ user.browser = `${ ua.getBrowser().name } ${ ua.getBrowser().version }`;
+ user.browserIcon = `icon-${ ua.getBrowser().name.toLowerCase() }`;
+
+ user.status = roomTypes.getUserStatus('l', this.rid) || 'offline';
+ }
+ return user;
+ },
+
+ room() {
+ return ChatRoom.findOne({ _id: this.rid });
+ },
+
+ department() {
+ return LivechatDepartment.findOne({ _id: Template.instance().departmentId.get() });
+ },
+
+ joinTags() {
+ return this.tags && this.tags.join(', ');
+ },
+
+ customFields() {
+ const fields = [];
+ let livechatData = {};
+ const user = Template.instance().user.get();
+ if (user) {
+ livechatData = _.extend(livechatData, user.livechatData);
+ }
+
+ const data = Template.currentData();
+ if (data && data.rid) {
+ const room = Rooms.findOne(data.rid);
+ if (room) {
+ livechatData = _.extend(livechatData, room.livechatData);
+ }
+ }
+
+ if (!_.isEmpty(livechatData)) {
+ for (const _id in livechatData) {
+ if (livechatData.hasOwnProperty(_id)) {
+ const customFields = Template.instance().customFields.get();
+ if (customFields) {
+ const field = _.findWhere(customFields, { _id });
+ if (field && field.visibility !== 'hidden') {
+ fields.push({ label: field.label, value: livechatData[_id] });
+ }
+ }
+ }
+ }
+ return fields;
+ }
+ },
+
+ createdAt() {
+ if (!this.createdAt) {
+ return '';
+ }
+ return moment(this.createdAt).format('L LTS');
+ },
+
+ lastLogin() {
+ if (!this.lastLogin) {
+ return '';
+ }
+ return moment(this.lastLogin).format('L LTS');
+ },
+
+ editing() {
+ return Template.instance().action.get() === 'edit';
+ },
+
+ forwarding() {
+ return Template.instance().action.get() === 'forward';
+ },
+
+ editDetails() {
+ const instance = Template.instance();
+ const user = instance.user.get();
+ return {
+ visitorId: user ? user._id : null,
+ roomId: this.rid,
+ save() {
+ instance.action.set();
+ },
+ cancel() {
+ instance.action.set();
+ },
+ };
+ },
+
+ forwardDetails() {
+ const instance = Template.instance();
+ const user = instance.user.get();
+ return {
+ visitorId: user ? user._id : null,
+ roomId: this.rid,
+ save() {
+ instance.action.set();
+ },
+ cancel() {
+ instance.action.set();
+ },
+ };
+ },
+
+ roomOpen() {
+ const room = ChatRoom.findOne({ _id: this.rid });
+
+ return room.open;
+ },
+
+ guestPool() {
+ return settings.get('Livechat_Routing_Method') === 'Guest_Pool';
+ },
+
+ showDetail() {
+ if (Template.instance().action.get()) {
+ return 'hidden';
+ }
+ },
+
+ canSeeButtons() {
+ if (hasRole(Meteor.userId(), 'livechat-manager')) {
+ return true;
+ }
+
+ const data = Template.currentData();
+ if (data && data.rid) {
+ const subscription = Subscriptions.findOne({ rid: data.rid });
+ return subscription !== undefined;
+ }
+ return false;
+ },
+});
+
+Template.visitorInfo.events({
+ 'click .edit-livechat'(event, instance) {
+ event.preventDefault();
+
+ instance.action.set('edit');
+ },
+ 'click .close-livechat'(event) {
+ event.preventDefault();
+
+ const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, function(error/* , result*/) {
+ if (error) {
+ return handleError(error);
+ }
+ modal.open({
+ title: t('Chat_closed'),
+ text: t('Chat_closed_successfully'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
+ });
+
+ if (!settings.get('Livechat_request_comment_when_closing_conversation')) {
+ const comment = TAPi18n.__('Chat_closed_by_agent');
+ return closeRoom(comment);
+ }
+
+ // Setting for Ask_for_conversation_finished_message is set to true
+ modal.open({
+ title: t('Closing_chat'),
+ type: 'input',
+ inputPlaceholder: t('Please_add_a_comment'),
+ showCancelButton: true,
+ closeOnConfirm: false,
+ }, (inputValue) => {
+ if (!inputValue) {
+ modal.showInputError(t('Please_add_a_comment_to_close_the_room'));
+ return false;
+ }
+
+ if (s.trim(inputValue) === '') {
+ modal.showInputError(t('Please_add_a_comment_to_close_the_room'));
+ return false;
+ }
+
+ return closeRoom(inputValue);
+
+ });
+ },
+
+ 'click .return-inquiry'(event) {
+ event.preventDefault();
+
+ modal.open({
+ title: t('Would_you_like_to_return_the_inquiry'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: t('Yes'),
+ }, () => {
+ Meteor.call('livechat:returnAsInquiry', this.rid, function(error/* , result*/) {
+ if (error) {
+ console.log(error);
+ } else {
+ Session.set('openedRoom');
+ FlowRouter.go('/home');
+ }
+ });
+ });
+ },
+
+ 'click .forward-livechat'(event, instance) {
+ event.preventDefault();
+
+ instance.action.set('forward');
+ },
+});
+
+Template.visitorInfo.onCreated(function() {
+ this.visitorId = new ReactiveVar(null);
+ this.customFields = new ReactiveVar([]);
+ this.action = new ReactiveVar();
+ this.user = new ReactiveVar();
+ this.departmentId = new ReactiveVar(null);
+
+ Meteor.call('livechat:getCustomFields', (err, customFields) => {
+ if (customFields) {
+ this.customFields.set(customFields);
+ }
+ });
+
+ const currentData = Template.currentData();
+
+ if (currentData && currentData.rid) {
+ this.autorun(() => {
+ const room = Rooms.findOne({ _id: currentData.rid });
+ this.visitorId.set(room && room.v && room.v._id);
+ this.departmentId.set(room && room.departmentId);
+ });
+
+ this.subscribe('livechat:visitorInfo', { rid: currentData.rid });
+ this.subscribe('livechat:departments', this.departmentId.get());
+ }
+
+ this.autorun(() => {
+ this.user.set(LivechatVisitor.findOne({ _id: this.visitorId.get() }));
+ });
+});
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html b/app/livechat/client/views/app/tabbar/visitorNavigation.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html
rename to app/livechat/client/views/app/tabbar/visitorNavigation.html
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js b/app/livechat/client/views/app/tabbar/visitorNavigation.js
similarity index 87%
rename from packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js
rename to app/livechat/client/views/app/tabbar/visitorNavigation.js
index c5007719fec5..c0c8a59d134c 100644
--- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js
+++ b/app/livechat/client/views/app/tabbar/visitorNavigation.js
@@ -1,8 +1,9 @@
import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
-import { ChatRoom } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { ChatRoom } from '../../../../../models';
+import { t } from '../../../../../utils';
import moment from 'moment';
+import './visitorNavigation.html';
const visitorNavigationHistory = new Mongo.Collection('visitor_navigation_history');
diff --git a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerAction.html b/app/livechat/client/views/app/triggers/livechatTriggerAction.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerAction.html
rename to app/livechat/client/views/app/triggers/livechatTriggerAction.html
diff --git a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerAction.js b/app/livechat/client/views/app/triggers/livechatTriggerAction.js
similarity index 87%
rename from packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerAction.js
rename to app/livechat/client/views/app/triggers/livechatTriggerAction.js
index 8667a60312e6..527a2ed9011b 100644
--- a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerAction.js
+++ b/app/livechat/client/views/app/triggers/livechatTriggerAction.js
@@ -1,6 +1,7 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../../../settings';
+import './livechatTriggerAction.html';
Template.livechatTriggerAction.helpers({
hiddenValue(current) {
@@ -18,7 +19,7 @@ Template.livechatTriggerAction.helpers({
return !!(this.params && this.params.sender === current);
},
disableIfGuestPool() {
- return RocketChat.settings.get('Livechat_Routing_Method') === 'Guest_Pool';
+ return settings.get('Livechat_Routing_Method') === 'Guest_Pool';
},
});
diff --git a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerCondition.html b/app/livechat/client/views/app/triggers/livechatTriggerCondition.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerCondition.html
rename to app/livechat/client/views/app/triggers/livechatTriggerCondition.html
diff --git a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerCondition.js b/app/livechat/client/views/app/triggers/livechatTriggerCondition.js
similarity index 95%
rename from packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerCondition.js
rename to app/livechat/client/views/app/triggers/livechatTriggerCondition.js
index 5e0aa9b068c7..6a6490853ce7 100644
--- a/packages/rocketchat-livechat/client/views/app/triggers/livechatTriggerCondition.js
+++ b/app/livechat/client/views/app/triggers/livechatTriggerCondition.js
@@ -1,4 +1,5 @@
import { Template } from 'meteor/templating';
+import './livechatTriggerCondition.html';
Template.livechatTriggerCondition.helpers({
hiddenValue(current) {
diff --git a/packages/rocketchat-livechat/client/views/sideNav/livechat.html b/app/livechat/client/views/sideNav/livechat.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/sideNav/livechat.html
rename to app/livechat/client/views/sideNav/livechat.html
diff --git a/app/livechat/client/views/sideNav/livechat.js b/app/livechat/client/views/sideNav/livechat.js
new file mode 100644
index 000000000000..a49ed44d5952
--- /dev/null
+++ b/app/livechat/client/views/sideNav/livechat.js
@@ -0,0 +1,127 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Session } from 'meteor/session';
+import { Template } from 'meteor/templating';
+import { ChatSubscription, Users } from '../../../../models';
+import { KonchatNotification } from '../../../../ui';
+import { settings } from '../../../../settings';
+import { hasRole } from '../../../../authorization';
+import { t, handleError, getUserPreference } from '../../../../utils';
+import { LivechatInquiry } from '../../../lib/LivechatInquiry';
+import './livechat.html';
+
+Template.livechat.helpers({
+ isActive() {
+ const query = {
+ t: 'l',
+ f: { $ne: true },
+ open: true,
+ rid: Session.get('openedRoom'),
+ };
+
+ const options = { fields: { _id: 1 } };
+
+ if (ChatSubscription.findOne(query, options)) {
+ return 'active';
+ }
+ },
+
+ rooms() {
+ const query = {
+ t: 'l',
+ open: true,
+ };
+
+ const user = Users.findOne(Meteor.userId(), {
+ fields: { 'settings.preferences.sidebarShowUnread': 1 },
+ });
+
+ if (getUserPreference(user, 'sidebarShowUnread')) {
+ query.alert = { $ne: true };
+ }
+
+ return ChatSubscription.find(query, {
+ sort: {
+ t: 1,
+ fname: 1,
+ },
+ });
+ },
+
+ inquiries() {
+ // get all inquiries of the department
+ const inqs = LivechatInquiry.find({
+ agents: Meteor.userId(),
+ status: 'open',
+ }, {
+ sort: {
+ ts: 1,
+ },
+ });
+
+ // for notification sound
+ inqs.forEach((inq) => {
+ KonchatNotification.newRoom(inq.rid);
+ });
+
+ return inqs;
+ },
+
+ guestPool() {
+ return settings.get('Livechat_Routing_Method') === 'Guest_Pool';
+ },
+
+ available() {
+ const statusLivechat = Template.instance().statusLivechat.get();
+
+ return {
+ status: statusLivechat === 'available' ? 'status-online' : '',
+ icon: statusLivechat === 'available' ? 'icon-toggle-on' : 'icon-toggle-off',
+ hint: statusLivechat === 'available' ? t('Available') : t('Not_Available'),
+ };
+ },
+
+ isLivechatAvailable() {
+ return Template.instance().statusLivechat.get() === 'available';
+ },
+
+ showQueueLink() {
+ if (settings.get('Livechat_Routing_Method') !== 'Least_Amount') {
+ return false;
+ }
+ return hasRole(Meteor.userId(), 'livechat-manager') || (Template.instance().statusLivechat.get() === 'available' && settings.get('Livechat_show_queue_list_link'));
+ },
+
+ activeLivechatQueue() {
+ FlowRouter.watchPathChange();
+ if (FlowRouter.current().route.name === 'livechat-queue') {
+ return 'active';
+ }
+ },
+});
+
+Template.livechat.events({
+ 'click .livechat-status'() {
+ Meteor.call('livechat:changeLivechatStatus', (err /* , results*/) => {
+ if (err) {
+ return handleError(err);
+ }
+ });
+ },
+});
+
+Template.livechat.onCreated(function() {
+ this.statusLivechat = new ReactiveVar();
+
+ this.autorun(() => {
+ if (Meteor.userId()) {
+ const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } });
+ this.statusLivechat.set(user.statusLivechat);
+ } else {
+ this.statusLivechat.set();
+ }
+ });
+
+ this.subscribe('livechat:inquiry');
+});
diff --git a/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html b/app/livechat/client/views/sideNav/livechatFlex.html
similarity index 100%
rename from packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html
rename to app/livechat/client/views/sideNav/livechatFlex.html
diff --git a/app/livechat/client/views/sideNav/livechatFlex.js b/app/livechat/client/views/sideNav/livechatFlex.js
new file mode 100644
index 000000000000..6848f07f9316
--- /dev/null
+++ b/app/livechat/client/views/sideNav/livechatFlex.js
@@ -0,0 +1,24 @@
+import { Template } from 'meteor/templating';
+import { SideNav, Layout } from '../../../../ui-utils';
+import { t } from '../../../../utils';
+import './livechatFlex.html';
+
+Template.livechatFlex.helpers({
+ menuItem(name, icon, section) {
+ return {
+ name: t(name),
+ icon,
+ pathSection: section,
+ darken: true,
+ };
+ },
+ embeddedVersion() {
+ return Layout.isEmbedded();
+ },
+});
+
+Template.livechatFlex.events({
+ 'click [data-action="close"]'() {
+ SideNav.closeFlex();
+ },
+});
diff --git a/app/livechat/imports/server/rest/departments.js b/app/livechat/imports/server/rest/departments.js
new file mode 100644
index 000000000000..337c4d523ed3
--- /dev/null
+++ b/app/livechat/imports/server/rest/departments.js
@@ -0,0 +1,109 @@
+import { check } from 'meteor/check';
+import { API } from '../../../../api';
+import { hasPermission } from '../../../../authorization';
+import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models';
+import { Livechat } from '../../../server/lib/Livechat';
+
+API.v1.addRoute('livechat/department', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ return API.v1.success({
+ departments: LivechatDepartment.find().fetch(),
+ });
+ },
+ post() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.bodyParams, {
+ department: Object,
+ agents: Array,
+ });
+
+ const department = Livechat.saveDepartment(null, this.bodyParams.department, this.bodyParams.agents);
+
+ if (department) {
+ return API.v1.success({
+ department,
+ agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(),
+ });
+ }
+
+ API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/department/:_id', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ return API.v1.success({
+ department: LivechatDepartment.findOneById(this.urlParams._id),
+ agents: LivechatDepartmentAgents.find({ departmentId: this.urlParams._id }).fetch(),
+ });
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ put() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ check(this.bodyParams, {
+ department: Object,
+ agents: Array,
+ });
+
+ if (Livechat.saveDepartment(this.urlParams._id, this.bodyParams.department, this.bodyParams.agents)) {
+ return API.v1.success({
+ department: LivechatDepartment.findOneById(this.urlParams._id),
+ agents: LivechatDepartmentAgents.find({ departmentId: this.urlParams._id }).fetch(),
+ });
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ delete() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ if (Livechat.removeDepartment(this.urlParams._id)) {
+ return API.v1.success();
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
diff --git a/app/livechat/imports/server/rest/facebook.js b/app/livechat/imports/server/rest/facebook.js
new file mode 100644
index 000000000000..9637f3869522
--- /dev/null
+++ b/app/livechat/imports/server/rest/facebook.js
@@ -0,0 +1,94 @@
+import crypto from 'crypto';
+import { Random } from 'meteor/random';
+import { API } from '../../../../api';
+import { Rooms, Users, LivechatVisitors } from '../../../../models';
+import { settings } from '../../../../settings';
+import { Livechat } from '../../../server/lib/Livechat';
+
+/**
+ * @api {post} /livechat/facebook Send Facebook message
+ * @apiName Facebook
+ * @apiGroup Livechat
+ *
+ * @apiParam {String} mid Facebook message id
+ * @apiParam {String} page Facebook pages id
+ * @apiParam {String} token Facebook user's token
+ * @apiParam {String} first_name Facebook user's first name
+ * @apiParam {String} last_name Facebook user's last name
+ * @apiParam {String} [text] Facebook message text
+ * @apiParam {String} [attachments] Facebook message attachments
+ */
+API.v1.addRoute('livechat/facebook', {
+ post() {
+ if (!this.bodyParams.text && !this.bodyParams.attachments) {
+ return {
+ success: false,
+ };
+ }
+
+ if (!this.request.headers['x-hub-signature']) {
+ return {
+ success: false,
+ };
+ }
+
+ if (!settings.get('Livechat_Facebook_Enabled')) {
+ return {
+ success: false,
+ error: 'Integration disabled',
+ };
+ }
+
+ // validate if request come from omni
+ const signature = crypto.createHmac('sha1', settings.get('Livechat_Facebook_API_Secret')).update(JSON.stringify(this.request.body)).digest('hex');
+ if (this.request.headers['x-hub-signature'] !== `sha1=${ signature }`) {
+ return {
+ success: false,
+ error: 'Invalid signature',
+ };
+ }
+
+ const sendMessage = {
+ message: {
+ _id: this.bodyParams.mid,
+ },
+ roomInfo: {
+ facebook: {
+ page: this.bodyParams.page,
+ },
+ },
+ };
+ let visitor = LivechatVisitors.getVisitorByToken(this.bodyParams.token);
+ if (visitor) {
+ const rooms = Rooms.findOpenByVisitorToken(visitor.token).fetch();
+ if (rooms && rooms.length > 0) {
+ sendMessage.message.rid = rooms[0]._id;
+ } else {
+ sendMessage.message.rid = Random.id();
+ }
+ sendMessage.message.token = visitor.token;
+ } else {
+ sendMessage.message.rid = Random.id();
+ sendMessage.message.token = this.bodyParams.token;
+
+ const userId = Livechat.registerGuest({
+ token: sendMessage.message.token,
+ name: `${ this.bodyParams.first_name } ${ this.bodyParams.last_name }`,
+ });
+
+ visitor = Users.findOneById(userId);
+ }
+
+ sendMessage.message.msg = this.bodyParams.text;
+ sendMessage.guest = visitor;
+
+ try {
+ return {
+ sucess: true,
+ message: Livechat.sendMessage(sendMessage),
+ };
+ } catch (e) {
+ console.error('Error using Facebook ->', e);
+ }
+ },
+});
diff --git a/packages/rocketchat-livechat/imports/server/rest/sms.js b/app/livechat/imports/server/rest/sms.js
similarity index 79%
rename from packages/rocketchat-livechat/imports/server/rest/sms.js
rename to app/livechat/imports/server/rest/sms.js
index 4924d9b5cdfb..5eec0dd39b5e 100644
--- a/packages/rocketchat-livechat/imports/server/rest/sms.js
+++ b/app/livechat/imports/server/rest/sms.js
@@ -1,11 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import LivechatVisitors from '../../../server/models/LivechatVisitors';
+import { Rooms, LivechatVisitors } from '../../../../models';
+import { API } from '../../../../api';
+import { SMS } from '../../../../sms';
+import { Livechat } from '../../../server/lib/Livechat';
-RocketChat.API.v1.addRoute('livechat/sms-incoming/:service', {
+API.v1.addRoute('livechat/sms-incoming/:service', {
post() {
- const SMSService = RocketChat.SMS.getService(this.urlParams.service);
+ const SMSService = SMS.getService(this.urlParams.service);
const sms = SMSService.parse(this.bodyParams);
@@ -23,7 +25,7 @@ RocketChat.API.v1.addRoute('livechat/sms-incoming/:service', {
};
if (visitor) {
- const rooms = RocketChat.models.Rooms.findOpenByVisitorToken(visitor.token).fetch();
+ const rooms = Rooms.findOpenByVisitorToken(visitor.token).fetch();
if (rooms && rooms.length > 0) {
sendMessage.message.rid = rooms[0]._id;
@@ -35,7 +37,7 @@ RocketChat.API.v1.addRoute('livechat/sms-incoming/:service', {
sendMessage.message.rid = Random.id();
sendMessage.message.token = Random.id();
- const visitorId = RocketChat.Livechat.registerGuest({
+ const visitorId = Livechat.registerGuest({
username: sms.from.replace(/[^0-9]/g, ''),
token: sendMessage.message.token,
phone: {
@@ -71,7 +73,7 @@ RocketChat.API.v1.addRoute('livechat/sms-incoming/:service', {
});
try {
- const message = SMSService.response.call(this, RocketChat.Livechat.sendMessage(sendMessage));
+ const message = SMSService.response.call(this, Livechat.sendMessage(sendMessage));
Meteor.defer(() => {
if (sms.extra) {
diff --git a/app/livechat/imports/server/rest/upload.js b/app/livechat/imports/server/rest/upload.js
new file mode 100644
index 000000000000..6ec96c1d494b
--- /dev/null
+++ b/app/livechat/imports/server/rest/upload.js
@@ -0,0 +1,103 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../../../settings';
+import { Settings, Rooms, LivechatVisitors } from '../../../../models';
+import { fileUploadIsValidContentType } from '../../../../utils';
+import { FileUpload } from '../../../../file-upload';
+import { API } from '../../../../api';
+import Busboy from 'busboy';
+import filesize from 'filesize';
+let maxFileSize;
+
+settings.get('FileUpload_MaxFileSize', function(key, value) {
+ try {
+ maxFileSize = parseInt(value);
+ } catch (e) {
+ maxFileSize = Settings.findOneById('FileUpload_MaxFileSize').packageValue;
+ }
+});
+
+API.v1.addRoute('livechat/upload/:rid', {
+ post() {
+ if (!this.request.headers['x-visitor-token']) {
+ return API.v1.unauthorized();
+ }
+
+ const visitorToken = this.request.headers['x-visitor-token'];
+ const visitor = LivechatVisitors.getVisitorByToken(visitorToken);
+
+ if (!visitor) {
+ return API.v1.unauthorized();
+ }
+
+ const room = Rooms.findOneOpenByRoomIdAndVisitorToken(this.urlParams.rid, visitorToken);
+ if (!room) {
+ return API.v1.unauthorized();
+ }
+
+ const busboy = new Busboy({ headers: this.request.headers });
+ const files = [];
+ const fields = {};
+
+ Meteor.wrapAsync((callback) => {
+ busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
+ if (fieldname !== 'file') {
+ return files.push(new Meteor.Error('invalid-field'));
+ }
+
+ const fileDate = [];
+ file.on('data', (data) => fileDate.push(data));
+
+ file.on('end', () => {
+ files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) });
+ });
+ });
+
+ busboy.on('field', (fieldname, value) => fields[fieldname] = value);
+
+ busboy.on('finish', Meteor.bindEnvironment(() => callback()));
+
+ this.request.pipe(busboy);
+ })();
+
+ if (files.length === 0) {
+ return API.v1.failure('File required');
+ }
+
+ if (files.length > 1) {
+ return API.v1.failure('Just 1 file is allowed');
+ }
+
+ const file = files[0];
+
+ if (!fileUploadIsValidContentType(file.mimetype)) {
+ return API.v1.failure({
+ reason: 'error-type-not-allowed',
+ });
+ }
+
+ // -1 maxFileSize means there is no limit
+ if (maxFileSize > -1 && file.fileBuffer.length > maxFileSize) {
+ return API.v1.failure({
+ reason: 'error-size-not-allowed',
+ sizeAllowed: filesize(maxFileSize),
+ });
+ }
+
+ const fileStore = FileUpload.getStore('Uploads');
+
+ const details = {
+ name: file.filename,
+ size: file.fileBuffer.length,
+ type: file.mimetype,
+ rid: this.urlParams.rid,
+ visitorToken,
+ };
+
+ const uploadedFile = Meteor.wrapAsync(fileStore.insert.bind(fileStore))(details, file.fileBuffer);
+
+ uploadedFile.description = fields.description;
+
+ delete fields.description;
+ API.v1.success(Meteor.call('sendFileLivechatMessage', this.urlParams.rid, visitorToken, uploadedFile, fields));
+ },
+});
diff --git a/app/livechat/imports/server/rest/users.js b/app/livechat/imports/server/rest/users.js
new file mode 100644
index 000000000000..6da74562a86f
--- /dev/null
+++ b/app/livechat/imports/server/rest/users.js
@@ -0,0 +1,146 @@
+import { check } from 'meteor/check';
+import { hasPermission, getUsersInRole } from '../../../../authorization';
+import { API } from '../../../../api';
+import { Users } from '../../../../models';
+import { Livechat } from '../../../server/lib/Livechat';
+import _ from 'underscore';
+
+API.v1.addRoute('livechat/users/:type', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ type: String,
+ });
+
+ let role;
+ if (this.urlParams.type === 'agent') {
+ role = 'livechat-agent';
+ } else if (this.urlParams.type === 'manager') {
+ role = 'livechat-manager';
+ } else {
+ throw 'Invalid type';
+ }
+
+ const users = getUsersInRole(role);
+
+ return API.v1.success({
+ users: users.fetch().map((user) => _.pick(user, '_id', 'username', 'name', 'status', 'statusLivechat')),
+ });
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ post() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+ try {
+ check(this.urlParams, {
+ type: String,
+ });
+
+ check(this.bodyParams, {
+ username: String,
+ });
+
+ if (this.urlParams.type === 'agent') {
+ const user = Livechat.addAgent(this.bodyParams.username);
+ if (user) {
+ return API.v1.success({ user });
+ }
+ } else if (this.urlParams.type === 'manager') {
+ const user = Livechat.addManager(this.bodyParams.username);
+ if (user) {
+ return API.v1.success({ user });
+ }
+ } else {
+ throw 'Invalid type';
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/users/:type/:_id', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ type: String,
+ _id: String,
+ });
+
+ const user = Users.findOneById(this.urlParams._id);
+
+ if (!user) {
+ return API.v1.failure('User not found');
+ }
+
+ let role;
+
+ if (this.urlParams.type === 'agent') {
+ role = 'livechat-agent';
+ } else if (this.urlParams.type === 'manager') {
+ role = 'livechat-manager';
+ } else {
+ throw 'Invalid type';
+ }
+
+ if (user.roles.indexOf(role) !== -1) {
+ return API.v1.success({
+ user: _.pick(user, '_id', 'username'),
+ });
+ }
+
+ return API.v1.success({
+ user: null,
+ });
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ delete() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ try {
+ check(this.urlParams, {
+ type: String,
+ _id: String,
+ });
+
+ const user = Users.findOneById(this.urlParams._id);
+
+ if (!user) {
+ return API.v1.failure();
+ }
+
+ if (this.urlParams.type === 'agent') {
+ if (Livechat.removeAgent(user.username)) {
+ return API.v1.success();
+ }
+ } else if (this.urlParams.type === 'manager') {
+ if (Livechat.removeManager(user.username)) {
+ return API.v1.success();
+ }
+ } else {
+ throw 'Invalid type';
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
diff --git a/app/livechat/lib/Assets.js b/app/livechat/lib/Assets.js
new file mode 100644
index 000000000000..c7252b5406ae
--- /dev/null
+++ b/app/livechat/lib/Assets.js
@@ -0,0 +1,29 @@
+import { Autoupdate } from 'meteor/autoupdate';
+
+export const addServerUrlToIndex = (file) => file.replace('', ``);
+
+export const addServerUrlToHead = (head) => {
+ let baseUrl;
+ if (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX && __meteor_runtime_config__.ROOT_URL_PATH_PREFIX.trim() !== '') {
+ baseUrl = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
+ } else {
+ baseUrl = '/';
+ }
+ if (/\/$/.test(baseUrl) === false) {
+ baseUrl += '/';
+ }
+
+ return `
+
+
+
+
+ ${ head }
+
+
+
+
+ `;
+};
diff --git a/app/livechat/lib/LivechatExternalMessage.js b/app/livechat/lib/LivechatExternalMessage.js
new file mode 100644
index 000000000000..b391143680a9
--- /dev/null
+++ b/app/livechat/lib/LivechatExternalMessage.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from '../../models';
+
+class LivechatExternalMessageClass extends Base {
+ constructor() {
+ super('livechat_external_message');
+
+ if (Meteor.isClient) {
+ this._initModel('livechat_external_message');
+ }
+ }
+
+ // FIND
+ findByRoomId(roomId, sort = { ts: -1 }) {
+ const query = { rid: roomId };
+
+ return this.find(query, { sort });
+ }
+}
+
+export const LivechatExternalMessage = new LivechatExternalMessageClass();
diff --git a/packages/rocketchat-livechat/lib/LivechatInquiry.js b/app/livechat/lib/LivechatInquiry.js
similarity index 89%
rename from packages/rocketchat-livechat/lib/LivechatInquiry.js
rename to app/livechat/lib/LivechatInquiry.js
index 80e3685c6da4..9da291e925ef 100644
--- a/packages/rocketchat-livechat/lib/LivechatInquiry.js
+++ b/app/livechat/lib/LivechatInquiry.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from '../../models';
import { Mongo } from 'meteor/mongo';
export let LivechatInquiry;
@@ -9,7 +9,7 @@ if (Meteor.isClient) {
}
if (Meteor.isServer) {
- class LivechatInquiryClass extends RocketChat.models._Base {
+ class LivechatInquiryClass extends Base {
constructor() {
super('livechat_inquiry');
@@ -101,6 +101,5 @@ if (Meteor.isServer) {
}
}
- RocketChat.models.LivechatInquiry = new LivechatInquiryClass();
- LivechatInquiry = RocketChat.models.LivechatInquiry;
+ LivechatInquiry = new LivechatInquiryClass();
}
diff --git a/app/livechat/lib/LivechatRoomType.js b/app/livechat/lib/LivechatRoomType.js
new file mode 100644
index 000000000000..3bcf75289988
--- /dev/null
+++ b/app/livechat/lib/LivechatRoomType.js
@@ -0,0 +1,106 @@
+import { Session } from 'meteor/session';
+import { ChatRoom } from '../../models';
+import { settings } from '../../settings';
+import { hasPermission } from '../../authorization';
+import { openRoom } from '../../ui-utils';
+import { RoomSettingsEnum, UiTextContext, RoomTypeRouteConfig, RoomTypeConfig } from '../../utils';
+import { LivechatInquiry } from './LivechatInquiry';
+import { getAvatarURL } from '../../utils/lib/getAvatarURL';
+
+class LivechatRoomRoute extends RoomTypeRouteConfig {
+ constructor() {
+ super({
+ name: 'live',
+ path: '/live/:id',
+ });
+ }
+
+ action(params) {
+ openRoom('l', params.id);
+ }
+
+ link(sub) {
+ return {
+ id: sub.rid,
+ };
+ }
+}
+
+export default class LivechatRoomType extends RoomTypeConfig {
+ constructor() {
+ super({
+ identifier: 'l',
+ order: 5,
+ icon: 'livechat',
+ label: 'Livechat',
+ route: new LivechatRoomRoute(),
+ });
+
+ this.notSubscribedTpl = 'livechatNotSubscribed';
+ this.readOnlyTpl = 'livechatReadOnly';
+ }
+
+ findRoom(identifier) {
+ return ChatRoom.findOne({ _id: identifier });
+ }
+
+ roomName(roomData) {
+ return roomData.name || roomData.fname || roomData.label;
+ }
+
+ condition() {
+ return settings.get('Livechat_enabled') && hasPermission('view-l-room');
+ }
+
+ canSendMessage(rid) {
+ const room = ChatRoom.findOne({ _id: rid }, { fields: { open: 1 } });
+ return room && room.open === true;
+ }
+
+ getUserStatus(rid) {
+ const room = Session.get(`roomData${ rid }`);
+ if (room) {
+ return room.v && room.v.status;
+ }
+ const inquiry = LivechatInquiry.findOne({ rid });
+ return inquiry && inquiry.v && inquiry.v.status;
+ }
+
+ allowRoomSettingChange(room, setting) {
+ switch (setting) {
+ case RoomSettingsEnum.JOIN_CODE:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ getUiText(context) {
+ switch (context) {
+ case UiTextContext.HIDE_WARNING:
+ return 'Hide_Livechat_Warning';
+ case UiTextContext.LEAVE_WARNING:
+ return 'Hide_Livechat_Warning';
+ default:
+ return '';
+ }
+ }
+
+ readOnly(rid, user) {
+ const room = ChatRoom.findOne({ _id: rid }, { fields: { open: 1, servedBy: 1 } });
+ if (!room || !room.open) {
+ return true;
+ }
+
+ const inquiry = LivechatInquiry.findOne({ rid }, { fields: { status: 1 } });
+ if (inquiry && inquiry.status === 'open') {
+ return true;
+ }
+
+ return (!room.servedBy || room.servedBy._id !== user._id) && !hasPermission('view-livechat-rooms');
+ }
+
+ getAvatarPath(roomData) {
+ return getAvatarURL({ username: `@${ this.roomName(roomData) }` });
+ }
+}
diff --git a/app/livechat/lib/messageTypes.js b/app/livechat/lib/messageTypes.js
new file mode 100644
index 000000000000..aeb53a6d5ae5
--- /dev/null
+++ b/app/livechat/lib/messageTypes.js
@@ -0,0 +1,54 @@
+import { Meteor } from 'meteor/meteor';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { MessageTypes } from '../../ui-utils';
+import { actionLinks } from '../../action-links';
+import { Notifications } from '../../notifications';
+import { Messages, Rooms } from '../../models';
+import { settings } from '../../settings';
+import { Livechat } from 'meteor/rocketchat:livechat';
+
+MessageTypes.registerType({
+ id: 'livechat_navigation_history',
+ system: true,
+ message: 'New_visitor_navigation',
+ data(message) {
+ if (!message.navigation || !message.navigation.page) {
+ return;
+ }
+ return {
+ history: `${ (message.navigation.page.title ? `${ message.navigation.page.title } - ` : '') + message.navigation.page.location.href }`,
+ };
+ },
+});
+
+MessageTypes.registerType({
+ id: 'livechat_video_call',
+ system: true,
+ message: 'New_videocall_request',
+});
+
+actionLinks.register('createLivechatCall', function(message, params, instance) {
+ if (Meteor.isClient) {
+ instance.tabBar.open('video');
+ }
+});
+
+actionLinks.register('denyLivechatCall', function(message/* , params*/) {
+ if (Meteor.isServer) {
+ const user = Meteor.user();
+
+ Messages.createWithTypeRoomIdMessageAndUser('command', message.rid, 'endCall', user);
+ Notifications.notifyRoom(message.rid, 'deleteMessage', { _id: message._id });
+
+ const language = user.language || settings.get('Language') || 'en';
+
+ Livechat.closeRoom({
+ user,
+ room: Rooms.findOneById(message.rid),
+ comment: TAPi18n.__('Videocall_declined', { lng: language }),
+ });
+ Meteor.defer(() => {
+ Messages.setHiddenById(message._id);
+ });
+ }
+});
diff --git a/app/livechat/server/agentStatus.js b/app/livechat/server/agentStatus.js
new file mode 100644
index 000000000000..f9e499ffa47b
--- /dev/null
+++ b/app/livechat/server/agentStatus.js
@@ -0,0 +1,9 @@
+import { hasRole } from '../../authorization';
+import { UserPresenceMonitor } from 'meteor/konecty:user-presence';
+import { Livechat } from './lib/Livechat';
+
+UserPresenceMonitor.onSetUserStatus((user, status) => {
+ if (hasRole(user._id, 'livechat-manager') || hasRole(user._id, 'livechat-agent')) {
+ Livechat.notifyAgentStatusChanged(user._id, status);
+ }
+});
diff --git a/packages/rocketchat-livechat/server/api.js b/app/livechat/server/api.js
similarity index 100%
rename from packages/rocketchat-livechat/server/api.js
rename to app/livechat/server/api.js
diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js
new file mode 100644
index 000000000000..a9be66be2974
--- /dev/null
+++ b/app/livechat/server/api/lib/livechat.js
@@ -0,0 +1,142 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { Users, Rooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models';
+import _ from 'underscore';
+import { Livechat } from '../../lib/Livechat';
+import { settings as rcSettings } from '../../../../settings';
+
+export function online() {
+ const onlineAgents = Livechat.getOnlineAgents();
+ return (onlineAgents && onlineAgents.count() > 0) || rcSettings.get('Livechat_guest_pool_with_no_agents');
+}
+
+export function findTriggers() {
+ return LivechatTrigger.findEnabled().fetch().map((trigger) => _.pick(trigger, '_id', 'actions', 'conditions', 'runOnce'));
+}
+
+export function findDepartments() {
+ return LivechatDepartment.findEnabledWithAgents().fetch().map((department) => _.pick(department, '_id', 'name', 'showOnRegistration', 'showOnOfflineForm'));
+}
+
+export function findGuest(token) {
+ return LivechatVisitors.getVisitorByToken(token, {
+ fields: {
+ name: 1,
+ username: 1,
+ token: 1,
+ visitorEmails: 1,
+ department: 1,
+ },
+ });
+}
+
+export function findRoom(token, rid) {
+ const fields = {
+ t: 1,
+ departmentId: 1,
+ servedBy: 1,
+ open: 1,
+ v: 1,
+ };
+
+ if (!rid) {
+ return Rooms.findLivechatByVisitorToken(token, fields);
+ }
+
+ return Rooms.findLivechatByIdAndVisitorToken(rid, token, fields);
+}
+
+export function findOpenRoom(token, departmentId) {
+ const options = {
+ fields: {
+ departmentId: 1,
+ servedBy: 1,
+ open: 1,
+ },
+ };
+
+ let room;
+ const rooms = departmentId ? Rooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options).fetch() : Rooms.findOpenByVisitorToken(token, options).fetch();
+ if (rooms && rooms.length > 0) {
+ room = rooms[0];
+ }
+
+ return room;
+}
+
+export function getRoom({ guest, rid, roomInfo, agent }) {
+ const token = guest && guest.token;
+
+ const message = {
+ _id: Random.id(),
+ rid,
+ msg: '',
+ token,
+ ts: new Date(),
+ };
+
+ return Livechat.getRoom(guest, message, roomInfo, agent);
+}
+
+export function findAgent(agentId) {
+ return Users.getAgentInfo(agentId);
+}
+
+export function normalizeHttpHeaderData(headers = {}) {
+ const httpHeaders = Object.assign({}, headers);
+ return { httpHeaders };
+}
+export function settings() {
+ const initSettings = Livechat.getInitSettings();
+ const triggers = findTriggers();
+ const departments = findDepartments();
+ const sound = `${ Meteor.absoluteUrl() }sounds/chime.mp3`;
+
+ return {
+ enabled: initSettings.Livechat_enabled,
+ settings: {
+ registrationForm: initSettings.Livechat_registration_form,
+ allowSwitchingDepartments: initSettings.Livechat_allow_switching_departments,
+ nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form,
+ emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form,
+ displayOfflineForm: initSettings.Livechat_display_offline_form,
+ videoCall: initSettings.Livechat_videocall_enabled === true && initSettings.Jitsi_Enabled === true,
+ fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled,
+ language: initSettings.Language,
+ transcript: initSettings.Livechat_enable_transcript,
+ historyMonitorType: initSettings.Livechat_history_monitor_type,
+ forceAcceptDataProcessingConsent: initSettings.Livechat_force_accept_data_processing_consent,
+ showConnecting: initSettings.Livechat_Show_Connecting,
+ },
+ theme: {
+ title: initSettings.Livechat_title,
+ color: initSettings.Livechat_title_color,
+ offlineTitle: initSettings.Livechat_offline_title,
+ offlineColor: initSettings.Livechat_offline_title_color,
+ actionLinks: [
+ { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' },
+ { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' },
+ ],
+ },
+ messages: {
+ offlineMessage: initSettings.Livechat_offline_message,
+ offlineSuccessMessage: initSettings.Livechat_offline_success_message,
+ offlineUnavailableMessage: initSettings.Livechat_offline_form_unavailable,
+ conversationFinishedMessage: initSettings.Livechat_conversation_finished_message,
+ transcriptMessage: initSettings.Livechat_transcript_message,
+ registrationFormMessage: initSettings.Livechat_registration_form_message,
+ dataProcessingConsentText: initSettings.Livechat_data_processing_consent_text,
+ },
+ survey: {
+ items: ['satisfaction', 'agentKnowledge', 'agentResposiveness', 'agentFriendliness'],
+ values: ['1', '2', '3', '4', '5'],
+ },
+ triggers,
+ departments,
+ resources: {
+ sound,
+ },
+ };
+}
+
+
diff --git a/packages/rocketchat-livechat/server/api/rest.js b/app/livechat/server/api/rest.js
similarity index 100%
rename from packages/rocketchat-livechat/server/api/rest.js
rename to app/livechat/server/api/rest.js
diff --git a/app/livechat/server/api/v1/agent.js b/app/livechat/server/api/v1/agent.js
new file mode 100644
index 000000000000..ec091fa43b32
--- /dev/null
+++ b/app/livechat/server/api/v1/agent.js
@@ -0,0 +1,77 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { API } from '../../../../api';
+import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/agent.info/:rid/:token', {
+ get() {
+ try {
+ check(this.urlParams, {
+ rid: String,
+ token: String,
+ });
+
+ const visitor = findGuest(this.urlParams.token);
+ if (!visitor) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(this.urlParams.token, this.urlParams.rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const agent = room && room.servedBy && findAgent(room.servedBy._id);
+ if (!agent) {
+ throw new Meteor.Error('invalid-agent');
+ }
+
+ return API.v1.success({ agent });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/agent.next/:token', {
+ get() {
+ try {
+ check(this.urlParams, {
+ token: String,
+ });
+
+ check(this.queryParams, {
+ department: Match.Maybe(String),
+ });
+
+ const { token } = this.urlParams;
+ const room = findOpenRoom(token);
+ if (room) {
+ return API.v1.success();
+ }
+
+ let { department } = this.queryParams;
+ if (!department) {
+ const requireDeparment = Livechat.getRequiredDepartment();
+ if (requireDeparment) {
+ department = requireDeparment._id;
+ }
+ }
+
+ const agentData = Livechat.getNextAgent(department);
+ if (!agentData) {
+ throw new Meteor.Error('agent-not-found');
+ }
+
+ const agent = findAgent(agentData.agentId);
+ if (!agent) {
+ throw new Meteor.Error('invalid-agent');
+ }
+
+ return API.v1.success({ agent });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js
new file mode 100644
index 000000000000..23ef0330f101
--- /dev/null
+++ b/app/livechat/server/api/v1/config.js
@@ -0,0 +1,38 @@
+import { Users } from '../../../../models';
+import { API } from '../../../../api';
+import { findGuest, settings, online, findOpenRoom } from '../lib/livechat';
+import { Match, check } from 'meteor/check';
+
+API.v1.addRoute('livechat/config', {
+ get() {
+ try {
+ check(this.queryParams, {
+ token: Match.Maybe(String),
+ });
+
+ const config = settings();
+ if (!config.enabled) {
+ return API.v1.success({ config: { enabled: false } });
+ }
+
+ const status = online();
+
+ const { token } = this.queryParams;
+ const guest = token && findGuest(token);
+
+ let room;
+ let agent;
+
+ if (guest) {
+ room = findOpenRoom(token);
+ agent = room && room.servedBy && Users.getAgentInfo(room.servedBy._id);
+ }
+
+ Object.assign(config, { online: status, guest, room, agent });
+
+ return API.v1.success({ config });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/customField.js b/app/livechat/server/api/v1/customField.js
new file mode 100644
index 000000000000..d210a0913cce
--- /dev/null
+++ b/app/livechat/server/api/v1/customField.js
@@ -0,0 +1,66 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { API } from '../../../../api';
+import { findGuest } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/custom.field', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ token: String,
+ key: String,
+ value: String,
+ overwrite: Boolean,
+ });
+
+ const { token, key, value, overwrite } = this.bodyParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ if (!Livechat.setCustomFields({ token, key, value, overwrite })) {
+ return API.v1.failure();
+ }
+
+ return API.v1.success({ field: { key, value, overwrite } });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/custom.fields', {
+ post() {
+ check(this.bodyParams, {
+ token: String,
+ customFields: [
+ Match.ObjectIncluding({
+ key: String,
+ value: String,
+ overwrite: Boolean,
+ }),
+ ],
+ });
+
+ const { token } = this.bodyParams;
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const fields = this.bodyParams.customFields.map((customField) => {
+ const data = Object.assign({ token }, customField);
+ if (!Livechat.setCustomFields(data)) {
+ return API.v1.failure();
+ }
+
+ return { Key: customField.key, value: customField.value, overwrite: customField.overwrite };
+ });
+
+ return API.v1.success({ fields });
+ },
+});
+
diff --git a/app/livechat/server/api/v1/message.js b/app/livechat/server/api/v1/message.js
new file mode 100644
index 000000000000..d3f1c8ed0a9f
--- /dev/null
+++ b/app/livechat/server/api/v1/message.js
@@ -0,0 +1,303 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { Messages, Rooms, LivechatVisitors } from '../../../../models';
+import { hasPermission } from '../../../../authorization';
+import { API } from '../../../../api';
+import { loadMessageHistory } from '../../../../lib';
+import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/message', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ _id: Match.Maybe(String),
+ token: String,
+ rid: String,
+ msg: String,
+ agent: Match.Maybe({
+ agentId: String,
+ username: String,
+ }),
+ });
+
+ const { token, rid, agent, msg } = this.bodyParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ if (!room.open) {
+ throw new Meteor.Error('room-closed');
+ }
+
+ const _id = this.bodyParams._id || Random.id();
+
+ const sendMessage = {
+ guest,
+ message: {
+ _id,
+ rid,
+ msg,
+ token,
+ },
+ agent,
+ };
+
+ const result = Livechat.sendMessage(sendMessage);
+ if (result) {
+ const message = Messages.findOneById(_id);
+ return API.v1.success({ message });
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/message/:_id', {
+ get() {
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ check(this.queryParams, {
+ token: String,
+ rid: String,
+ });
+
+ const { token, rid } = this.queryParams;
+ const { _id } = this.urlParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const message = Messages.findOneById(_id);
+ if (!message) {
+ throw new Meteor.Error('invalid-message');
+ }
+
+ return API.v1.success({ message });
+
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+
+ put() {
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ check(this.bodyParams, {
+ token: String,
+ rid: String,
+ msg: String,
+ });
+
+ const { token, rid } = this.bodyParams;
+ const { _id } = this.urlParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const msg = Messages.findOneById(_id);
+ if (!msg) {
+ throw new Meteor.Error('invalid-message');
+ }
+
+ const result = Livechat.updateMessage({ guest, message: { _id: msg._id, msg: this.bodyParams.msg } });
+ if (result) {
+ const message = Messages.findOneById(_id);
+ return API.v1.success({ message });
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ delete() {
+ try {
+ check(this.urlParams, {
+ _id: String,
+ });
+
+ check(this.bodyParams, {
+ token: String,
+ rid: String,
+ });
+
+ const { token, rid } = this.bodyParams;
+ const { _id } = this.urlParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const message = Messages.findOneById(_id);
+ if (!message) {
+ throw new Meteor.Error('invalid-message');
+ }
+
+ const result = Livechat.deleteMessage({ guest, message });
+ if (result) {
+ return API.v1.success({
+ message: {
+ _id,
+ ts: new Date().toISOString(),
+ },
+ });
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/messages.history/:rid', {
+ get() {
+ try {
+ check(this.urlParams, {
+ rid: String,
+ });
+
+ const { rid } = this.urlParams;
+ const { token } = this.queryParams;
+
+ if (!token) {
+ throw new Meteor.Error('error-token-param-not-provided', 'The required "token" query param is missing.');
+ }
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ let ls = undefined;
+ if (this.queryParams.ls) {
+ ls = new Date(this.queryParams.ls);
+ }
+
+ let end = undefined;
+ if (this.queryParams.end) {
+ end = new Date(this.queryParams.end);
+ }
+
+ let limit = 20;
+ if (this.queryParams.limit) {
+ limit = parseInt(this.queryParams.limit);
+ }
+
+ const messages = loadMessageHistory({ userId: guest._id, rid, end, limit, ls });
+ return API.v1.success(messages);
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/messages', { authRequired: true }, {
+ post() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ if (!this.bodyParams.visitor) {
+ return API.v1.failure('Body param "visitor" is required');
+ }
+ if (!this.bodyParams.visitor.token) {
+ return API.v1.failure('Body param "visitor.token" is required');
+ }
+ if (!this.bodyParams.messages) {
+ return API.v1.failure('Body param "messages" is required');
+ }
+ if (!(this.bodyParams.messages instanceof Array)) {
+ return API.v1.failure('Body param "messages" is not an array');
+ }
+ if (this.bodyParams.messages.length === 0) {
+ return API.v1.failure('Body param "messages" is empty');
+ }
+
+ const visitorToken = this.bodyParams.visitor.token;
+
+ let visitor = LivechatVisitors.getVisitorByToken(visitorToken);
+ let rid;
+ if (visitor) {
+ const rooms = Rooms.findOpenByVisitorToken(visitorToken).fetch();
+ if (rooms && rooms.length > 0) {
+ rid = rooms[0]._id;
+ } else {
+ rid = Random.id();
+ }
+ } else {
+ rid = Random.id();
+
+ const guest = this.bodyParams.visitor;
+ guest.connectionData = normalizeHttpHeaderData(this.request.headers);
+
+ const visitorId = Livechat.registerGuest(guest);
+ visitor = LivechatVisitors.findOneById(visitorId);
+ }
+
+ const sentMessages = this.bodyParams.messages.map((message) => {
+ const sendMessage = {
+ guest: visitor,
+ message: {
+ _id: Random.id(),
+ rid,
+ token: visitorToken,
+ msg: message.msg,
+ },
+ };
+ const sentMessage = Livechat.sendMessage(sendMessage);
+ return {
+ username: sentMessage.u.username,
+ msg: sentMessage.msg,
+ ts: sentMessage.ts,
+ };
+ });
+
+ return API.v1.success({
+ messages: sentMessages,
+ });
+ },
+});
diff --git a/app/livechat/server/api/v1/offlineMessage.js b/app/livechat/server/api/v1/offlineMessage.js
new file mode 100644
index 000000000000..8f97520933d0
--- /dev/null
+++ b/app/livechat/server/api/v1/offlineMessage.js
@@ -0,0 +1,26 @@
+import { Match, check } from 'meteor/check';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { API } from '../../../../api';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/offline.message', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ name: String,
+ email: String,
+ message: String,
+ department: Match.Maybe(String),
+ });
+
+ const { name, email, message, department } = this.bodyParams;
+ if (!Livechat.sendOfflineMessage({ name, email, message, department })) {
+ return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_offline_message') });
+ }
+
+ return API.v1.success({ message: TAPi18n.__('Livechat_offline_message_sent') });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/pageVisited.js b/app/livechat/server/api/v1/pageVisited.js
new file mode 100644
index 000000000000..1182717e28b7
--- /dev/null
+++ b/app/livechat/server/api/v1/pageVisited.js
@@ -0,0 +1,46 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { API } from '../../../../api';
+import _ from 'underscore';
+import { findGuest, findRoom } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/page.visited', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ token: String,
+ rid: String,
+ pageInfo: Match.ObjectIncluding({
+ change: String,
+ title: String,
+ location: Match.ObjectIncluding({
+ href: String,
+ }),
+ }),
+ });
+
+ const { token, rid, pageInfo } = this.bodyParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const obj = Livechat.savePageHistory(token, rid, pageInfo);
+ if (obj) {
+ const page = _.pick(obj, 'msg', 'navigation');
+ return API.v1.success({ page });
+ }
+
+ return API.v1.success();
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/room.js b/app/livechat/server/api/v1/room.js
new file mode 100644
index 000000000000..707d619d7150
--- /dev/null
+++ b/app/livechat/server/api/v1/room.js
@@ -0,0 +1,173 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings as rcSettings } from '../../../../settings';
+import { Messages, Rooms } from '../../../../models';
+import { API } from '../../../../api';
+import { findGuest, findRoom, getRoom, settings, findAgent } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/room', {
+ get() {
+ try {
+ check(this.queryParams, {
+ token: String,
+ rid: Match.Maybe(String),
+ agentId: Match.Maybe(String),
+ });
+
+ const { token } = this.queryParams;
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ let agent;
+ const { agentId } = this.queryParams;
+ const agentObj = agentId && findAgent(agentId);
+ if (agentObj) {
+ const { username } = agentObj;
+ agent = Object.assign({}, { agentId, username });
+ }
+
+ const rid = this.queryParams.rid || Random.id();
+ const room = getRoom({ guest, rid, agent });
+
+ return API.v1.success(room);
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/room.close', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ rid: String,
+ token: String,
+ });
+
+ const { rid, token } = this.bodyParams;
+
+ const visitor = findGuest(token);
+ if (!visitor) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ if (!room.open) {
+ throw new Meteor.Error('room-closed');
+ }
+
+ const language = rcSettings.get('Language') || 'en';
+ const comment = TAPi18n.__('Closed_by_visitor', { lng: language });
+
+ if (!Livechat.closeRoom({ visitor, room, comment })) {
+ return API.v1.failure();
+ }
+
+ return API.v1.success({ rid, comment });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/room.transfer', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ rid: String,
+ token: String,
+ department: String,
+ });
+
+ const { rid, token, department } = this.bodyParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ let room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ // update visited page history to not expire
+ Messages.keepHistoryForToken(token);
+
+ if (!Livechat.transfer(room, guest, { roomId: rid, departmentId: department })) {
+ return API.v1.failure();
+ }
+
+ room = findRoom(token, rid);
+ return API.v1.success({ room });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/room.survey', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ rid: String,
+ token: String,
+ data: [Match.ObjectIncluding({
+ name: String,
+ value: String,
+ })],
+ });
+
+ const { rid, token, data } = this.bodyParams;
+
+ const visitor = findGuest(token);
+ if (!visitor) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const room = findRoom(token, rid);
+ if (!room) {
+ throw new Meteor.Error('invalid-room');
+ }
+
+ const config = settings();
+ if (!config.survey || !config.survey.items || !config.survey.values) {
+ throw new Meteor.Error('invalid-livechat-config');
+ }
+
+ const updateData = {};
+ for (const item of data) {
+ if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') {
+ updateData[item.name] = item.value;
+ }
+ }
+
+ if (Object.keys(updateData).length === 0) {
+ throw new Meteor.Error('invalid-data');
+ }
+
+ if (!Rooms.updateSurveyFeedbackById(room._id, updateData)) {
+ return API.v1.failure();
+ }
+
+ return API.v1.success({ rid, data: updateData });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/room.forward', { authRequired: true }, {
+ post() {
+ API.v1.success(Meteor.runAsUser(this.userId, () => Meteor.call('livechat:transfer', this.bodyParams)));
+ },
+});
diff --git a/app/livechat/server/api/v1/transcript.js b/app/livechat/server/api/v1/transcript.js
new file mode 100644
index 000000000000..c517d546a4e4
--- /dev/null
+++ b/app/livechat/server/api/v1/transcript.js
@@ -0,0 +1,25 @@
+import { check } from 'meteor/check';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { API } from '../../../../api';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/transcript', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ token: String,
+ rid: String,
+ email: String,
+ });
+
+ const { token, rid, email } = this.bodyParams;
+ if (!Livechat.sendTranscript({ token, rid, email })) {
+ return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') });
+ }
+
+ return API.v1.success({ message: TAPi18n.__('Livechat_transcript_sent') });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/videoCall.js b/app/livechat/server/api/v1/videoCall.js
new file mode 100644
index 000000000000..7845b4ffe26f
--- /dev/null
+++ b/app/livechat/server/api/v1/videoCall.js
@@ -0,0 +1,52 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { Messages } from '../../../../models';
+import { settings as rcSettings } from '../../../../settings';
+import { API } from '../../../../api';
+import { findGuest, getRoom, settings } from '../lib/livechat';
+
+API.v1.addRoute('livechat/video.call/:token', {
+ get() {
+ try {
+ check(this.urlParams, {
+ token: String,
+ });
+
+ check(this.queryParams, {
+ rid: Match.Maybe(String),
+ });
+
+ const { token } = this.urlParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const rid = this.queryParams.rid || Random.id();
+ const roomInfo = { jitsiTimeout: new Date(Date.now() + 3600 * 1000) };
+ const { room } = getRoom({ guest, rid, roomInfo });
+ const config = settings();
+ if (!config.theme || !config.theme.actionLinks) {
+ throw new Meteor.Error('invalid-livechat-config');
+ }
+
+ Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, {
+ actionLinks: config.theme.actionLinks,
+ });
+
+ const videoCall = {
+ rid,
+ domain: rcSettings.get('Jitsi_Domain'),
+ provider: 'jitsi',
+ room: rcSettings.get('Jitsi_URL_Room_Prefix') + rcSettings.get('uniqueID') + rid,
+ timeout: new Date(Date.now() + 3600 * 1000),
+ };
+
+ return API.v1.success({ videoCall });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/api/v1/visitor.js b/app/livechat/server/api/v1/visitor.js
new file mode 100644
index 000000000000..f7adf1550db2
--- /dev/null
+++ b/app/livechat/server/api/v1/visitor.js
@@ -0,0 +1,152 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Rooms, LivechatVisitors, LivechatCustomField } from '../../../../models';
+import { hasPermission } from '../../../../authorization';
+import { API } from '../../../../api';
+import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
+import { Livechat } from '../../lib/Livechat';
+
+API.v1.addRoute('livechat/visitor', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ visitor: Match.ObjectIncluding({
+ token: String,
+ name: Match.Maybe(String),
+ email: Match.Maybe(String),
+ department: Match.Maybe(String),
+ phone: Match.Maybe(String),
+ username: Match.Maybe(String),
+ customFields: Match.Maybe([
+ Match.ObjectIncluding({
+ key: String,
+ value: String,
+ overwrite: Boolean,
+ }),
+ ]),
+ }),
+ });
+
+ const { token, customFields } = this.bodyParams.visitor;
+ const guest = this.bodyParams.visitor;
+
+ if (this.bodyParams.visitor.phone) {
+ guest.phone = { number: this.bodyParams.visitor.phone };
+ }
+
+ guest.connectionData = normalizeHttpHeaderData(this.request.headers);
+ const visitorId = Livechat.registerGuest(guest);
+
+ let visitor = LivechatVisitors.getVisitorByToken(token);
+ // If it's updating an existing visitor, it must also update the roomInfo
+ const cursor = Rooms.findOpenByVisitorToken(token);
+ cursor.forEach((room) => {
+ Livechat.saveRoomInfo(room, visitor);
+ });
+
+ if (customFields && customFields instanceof Array) {
+ customFields.forEach((field) => {
+ const customField = LivechatCustomField.findOneById(field.key);
+ if (!customField) {
+ return;
+ }
+ const { key, value, overwrite } = field;
+ if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) {
+ return API.v1.failure();
+ }
+ });
+ }
+
+ visitor = LivechatVisitors.findOneById(visitorId);
+ return API.v1.success({ visitor });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/visitor/:token', {
+ get() {
+ try {
+ check(this.urlParams, {
+ token: String,
+ });
+
+ const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
+ return API.v1.success({ visitor });
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+ delete() {
+ try {
+ check(this.urlParams, {
+ token: String,
+ });
+
+ const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
+ if (!visitor) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ const { _id } = visitor;
+ const result = Livechat.removeGuest(_id);
+ if (result) {
+ return API.v1.success({
+ visitor: {
+ _id,
+ ts: new Date().toISOString(),
+ },
+ });
+ }
+
+ return API.v1.failure();
+ } catch (e) {
+ return API.v1.failure(e.error);
+ }
+ },
+});
+
+API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, {
+ get() {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return API.v1.unauthorized();
+ }
+
+ const rooms = Rooms.findOpenByVisitorToken(this.urlParams.token, {
+ fields: {
+ name: 1,
+ t: 1,
+ cl: 1,
+ u: 1,
+ usernames: 1,
+ servedBy: 1,
+ },
+ }).fetch();
+ return API.v1.success({ rooms });
+ },
+});
+
+API.v1.addRoute('livechat/visitor.status', {
+ post() {
+ try {
+ check(this.bodyParams, {
+ token: String,
+ status: String,
+ });
+
+ const { token, status } = this.bodyParams;
+
+ const guest = findGuest(token);
+ if (!guest) {
+ throw new Meteor.Error('invalid-token');
+ }
+
+ Livechat.notifyGuestStatusChanged(token, status);
+
+ return API.v1.success({ token, status });
+ } catch (e) {
+ return API.v1.failure(e);
+ }
+ },
+});
diff --git a/app/livechat/server/config.js b/app/livechat/server/config.js
new file mode 100644
index 000000000000..193ad780edce
--- /dev/null
+++ b/app/livechat/server/config.js
@@ -0,0 +1,430 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ settings.addGroup('Livechat');
+
+ settings.add('Livechat_enabled', false, { type: 'boolean', group: 'Livechat', public: true });
+
+ settings.add('Livechat_title', 'Rocket.Chat', { type: 'string', group: 'Livechat', public: true });
+ settings.add('Livechat_title_color', '#C1272D', {
+ type: 'color',
+ editor: 'color',
+ allowedTypes: ['color', 'expression'],
+ group: 'Livechat',
+ public: true,
+ });
+
+ settings.add('Livechat_display_offline_form', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Display_offline_form',
+ });
+
+ settings.add('Livechat_validate_offline_email', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Validate_email_address',
+ });
+
+ settings.add('Livechat_offline_form_unavailable', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Offline_form_unavailable_message',
+ });
+
+ settings.add('Livechat_offline_title', 'Leave a message', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Title',
+ });
+ settings.add('Livechat_offline_title_color', '#666666', {
+ type: 'color',
+ editor: 'color',
+ allowedTypes: ['color', 'expression'],
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Color',
+ });
+ settings.add('Livechat_offline_message', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Instructions',
+ i18nDescription: 'Instructions_to_your_visitor_fill_the_form_to_send_a_message',
+ });
+ settings.add('Livechat_offline_email', '', {
+ type: 'string',
+ group: 'Livechat',
+ i18nLabel: 'Email_address_to_send_offline_messages',
+ section: 'Offline',
+ });
+ settings.add('Livechat_offline_success_message', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ section: 'Offline',
+ i18nLabel: 'Offline_success_message',
+ });
+
+ settings.add('Livechat_allow_switching_departments', true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Allow_switching_departments' });
+ settings.add('Livechat_show_agent_email', true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Show_agent_email' });
+
+ settings.add('Livechat_request_comment_when_closing_conversation', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Request_comment_when_closing_conversation',
+ });
+
+ settings.add('Livechat_conversation_finished_message', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Conversation_finished_message',
+ });
+
+ settings.add('Livechat_registration_form', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Show_preregistration_form',
+ });
+
+ settings.add('Livechat_name_field_registration_form', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Show_name_field',
+ });
+
+ settings.add('Livechat_email_field_registration_form', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Show_email_field',
+ });
+
+ settings.add('Livechat_guest_count', 1, { type: 'int', group: 'Livechat' });
+
+ settings.add('Livechat_Room_Count', 1, {
+ type: 'int',
+ group: 'Livechat',
+ i18nLabel: 'Livechat_room_count',
+ });
+
+ settings.add('Livechat_agent_leave_action', 'none', {
+ type: 'select',
+ group: 'Livechat',
+ values: [
+ { key: 'none', i18nLabel: 'None' },
+ { key: 'forward', i18nLabel: 'Forward' },
+ { key: 'close', i18nLabel: 'Close' },
+ ],
+ i18nLabel: 'How_to_handle_open_sessions_when_agent_goes_offline',
+ });
+
+ settings.add('Livechat_agent_leave_action_timeout', 60, {
+ type: 'int',
+ group: 'Livechat',
+ enableQuery: { _id: 'Livechat_agent_leave_action', value: { $ne: 'none' } },
+ i18nLabel: 'How_long_to_wait_after_agent_goes_offline',
+ i18nDescription: 'Time_in_seconds',
+ });
+
+ settings.add('Livechat_agent_leave_comment', '', {
+ type: 'string',
+ group: 'Livechat',
+ enableQuery: { _id: 'Livechat_agent_leave_action', value: 'close' },
+ i18nLabel: 'Comment_to_leave_on_closing_session',
+ });
+
+ settings.add('Livechat_webhookUrl', false, {
+ type: 'string',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Webhook_URL',
+ });
+
+ settings.add('Livechat_secret_token', false, {
+ type: 'string',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Secret_token',
+ });
+
+ settings.add('Livechat_webhook_on_close', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_request_on_chat_close',
+ });
+
+ settings.add('Livechat_webhook_on_offline_msg', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_request_on_offline_messages',
+ });
+
+ settings.add('Livechat_webhook_on_visitor_message', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_request_on_visitor_message',
+ });
+
+ settings.add('Livechat_webhook_on_agent_message', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_request_on_agent_message',
+ });
+
+ settings.add('Send_visitor_navigation_history_livechat_webhook_request', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_visitor_navigation_history_on_request',
+ i18nDescription: 'Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled',
+ enableQuery: { _id: 'Livechat_Visitor_navigation_as_a_message', value: true },
+ });
+
+ settings.add('Livechat_webhook_on_capture', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Send_request_on_lead_capture',
+ });
+
+ settings.add('Livechat_lead_email_regex', '\\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,4}\\b', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Lead_capture_email_regex',
+ });
+
+ settings.add('Livechat_lead_phone_regex', '((?:\\([0-9]{1,3}\\)|[0-9]{2})[ \\-]*?[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$)|[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$))', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'CRM_Integration',
+ i18nLabel: 'Lead_capture_phone_regex',
+ });
+
+ settings.add('Livechat_Knowledge_Enabled', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'Knowledge_Base',
+ public: true,
+ i18nLabel: 'Enabled',
+ });
+
+ settings.add('Livechat_Knowledge_Apiai_Key', '', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'Knowledge_Base',
+ public: true,
+ i18nLabel: 'Apiai_Key',
+ });
+
+ settings.add('Livechat_Knowledge_Apiai_Language', 'en', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'Knowledge_Base',
+ public: true,
+ i18nLabel: 'Apiai_Language',
+ });
+
+ settings.add('Livechat_history_monitor_type', 'url', {
+ type: 'select',
+ group: 'Livechat',
+ i18nLabel: 'Monitor_history_for_changes_on',
+ values: [
+ { key: 'url', i18nLabel: 'Page_URL' },
+ { key: 'title', i18nLabel: 'Page_title' },
+ ],
+ });
+
+ settings.add('Livechat_Visitor_navigation_as_a_message', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Send_Visitor_navigation_history_as_a_message',
+ });
+
+ settings.add('Livechat_enable_office_hours', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Office_hours_enabled',
+ });
+
+ settings.add('Livechat_continuous_sound_notification_new_livechat_room', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Continuous_sound_notifications_for_new_livechat_room',
+ });
+
+ settings.add('Livechat_videocall_enabled', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Videocall_enabled',
+ i18nDescription: 'Beta_feature_Depends_on_Video_Conference_to_be_enabled',
+ enableQuery: { _id: 'Jitsi_Enabled', value: true },
+ });
+
+ settings.add('Livechat_fileupload_enabled', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'FileUpload_Enabled',
+ enableQuery: { _id: 'FileUpload_Enabled', value: true },
+ });
+
+ settings.add('Livechat_enable_transcript', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Transcript_Enabled',
+ });
+
+ settings.add('Livechat_transcript_message', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Transcript_message',
+ enableQuery: { _id: 'Livechat_enable_transcript', value: true },
+ });
+
+ settings.add('Livechat_registration_form_message', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Livechat_registration_form_message',
+ });
+
+ settings.add('Livechat_AllowedDomainsList', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Livechat_AllowedDomainsList',
+ i18nDescription: 'Domains_allowed_to_embed_the_livechat_widget',
+ });
+
+ settings.add('Livechat_Facebook_Enabled', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'Facebook',
+ });
+
+ settings.add('Livechat_Facebook_API_Key', '', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'Facebook',
+ i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours',
+ });
+
+ settings.add('Livechat_Facebook_API_Secret', '', {
+ type: 'string',
+ group: 'Livechat',
+ section: 'Facebook',
+ i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours',
+ });
+
+ settings.add('Livechat_RDStation_Token', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: false,
+ section: 'RD Station',
+ i18nLabel: 'RDStation_Token',
+ });
+
+ settings.add('Livechat_Routing_Method', 'Least_Amount', {
+ type: 'select',
+ group: 'Livechat',
+ public: true,
+ section: 'Routing',
+ values: [
+ { key: 'External', i18nLabel: 'External_Service' },
+ { key: 'Least_Amount', i18nLabel: 'Least_Amount' },
+ { key: 'Guest_Pool', i18nLabel: 'Guest_Pool' },
+ ],
+ });
+
+ settings.add('Livechat_guest_pool_with_no_agents', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ section: 'Routing',
+ i18nLabel: 'Accept_with_no_online_agents',
+ i18nDescription: 'Accept_incoming_livechat_requests_even_if_there_are_no_online_agents',
+ enableQuery: { _id: 'Livechat_Routing_Method', value: 'Guest_Pool' },
+ });
+
+ settings.add('Livechat_show_queue_list_link', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ section: 'Routing',
+ i18nLabel: 'Show_queue_list_to_all_agents',
+ enableQuery: { _id: 'Livechat_Routing_Method', value: { $ne: 'External' } },
+ });
+
+ settings.add('Livechat_External_Queue_URL', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: false,
+ section: 'Routing',
+ i18nLabel: 'External_Queue_Service_URL',
+ i18nDescription: 'For_more_details_please_check_our_docs',
+ enableQuery: { _id: 'Livechat_Routing_Method', value: 'External' },
+ });
+
+ settings.add('Livechat_External_Queue_Token', '', {
+ type: 'string',
+ group: 'Livechat',
+ public: false,
+ section: 'Routing',
+ i18nLabel: 'Secret_token',
+ enableQuery: { _id: 'Livechat_Routing_Method', value: 'External' },
+ });
+
+ settings.add('Livechat_Allow_collect_and_store_HTTP_header_informations', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Allow_collect_and_store_HTTP_header_informations',
+ i18nDescription: 'Allow_collect_and_store_HTTP_header_informations_description',
+ });
+
+ settings.add('Livechat_force_accept_data_processing_consent', false, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ alert: 'Force_visitor_to_accept_data_processing_consent_enabled_alert',
+ i18nLabel: 'Force_visitor_to_accept_data_processing_consent',
+ i18nDescription: 'Force_visitor_to_accept_data_processing_consent_description',
+ });
+
+ settings.add('Livechat_data_processing_consent_text', '', {
+ type: 'string',
+ multiline: true,
+ group: 'Livechat',
+ public: true,
+ i18nLabel: 'Data_processing_consent_text',
+ i18nDescription: 'Data_processing_consent_text_description',
+ enableQuery: { _id: 'Livechat_force_accept_data_processing_consent', value: true },
+ });
+
+});
diff --git a/app/livechat/server/hooks/RDStation.js b/app/livechat/server/hooks/RDStation.js
new file mode 100644
index 000000000000..5254ae348771
--- /dev/null
+++ b/app/livechat/server/hooks/RDStation.js
@@ -0,0 +1,60 @@
+import { HTTP } from 'meteor/http';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { Livechat } from '../lib/Livechat';
+
+function sendToRDStation(room) {
+ if (!settings.get('Livechat_RDStation_Token')) {
+ return room;
+ }
+
+ const livechatData = Livechat.getLivechatRoomGuestInfo(room);
+
+ if (!livechatData.visitor.email) {
+ return room;
+ }
+
+ const email = Array.isArray(livechatData.visitor.email) ? livechatData.visitor.email[0].address : livechatData.visitor.email;
+
+ const options = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: {
+ token_rdstation: settings.get('Livechat_RDStation_Token'),
+ identificador: 'rocketchat-livechat',
+ client_id: livechatData.visitor._id,
+ email,
+ },
+ };
+
+ options.data.nome = livechatData.visitor.name || livechatData.visitor.username;
+
+ if (livechatData.visitor.phone) {
+ options.data.telefone = livechatData.visitor.phone;
+ }
+
+ if (livechatData.tags) {
+ options.data.tags = livechatData.tags;
+ }
+
+ Object.keys(livechatData.customFields || {}).forEach((field) => {
+ options.data[field] = livechatData.customFields[field];
+ });
+
+ Object.keys(livechatData.visitor.customFields || {}).forEach((field) => {
+ options.data[field] = livechatData.visitor.customFields[field];
+ });
+
+ try {
+ HTTP.call('POST', 'https://www.rdstation.com.br/api/1.3/conversions', options);
+ } catch (e) {
+ console.error('Error sending lead to RD Station ->', e);
+ }
+
+ return room;
+}
+
+callbacks.add('livechat.closeRoom', sendToRDStation, callbacks.priority.MEDIUM, 'livechat-rd-station-close-room');
+
+callbacks.add('livechat.saveInfo', sendToRDStation, callbacks.priority.MEDIUM, 'livechat-rd-station-save-info');
diff --git a/app/livechat/server/hooks/externalMessage.js b/app/livechat/server/hooks/externalMessage.js
new file mode 100644
index 000000000000..2597deaeeaa2
--- /dev/null
+++ b/app/livechat/server/hooks/externalMessage.js
@@ -0,0 +1,69 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { SystemLogger } from '../../../logger';
+import { HTTP } from 'meteor/http';
+import { LivechatExternalMessage } from '../../lib/LivechatExternalMessage';
+import _ from 'underscore';
+
+let knowledgeEnabled = false;
+let apiaiKey = '';
+let apiaiLanguage = 'en';
+settings.get('Livechat_Knowledge_Enabled', function(key, value) {
+ knowledgeEnabled = value;
+});
+settings.get('Livechat_Knowledge_Apiai_Key', function(key, value) {
+ apiaiKey = value;
+});
+settings.get('Livechat_Knowledge_Apiai_Language', function(key, value) {
+ apiaiLanguage = value;
+});
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ // skips this callback if the message was edited
+ if (!message || message.editedAt) {
+ return message;
+ }
+
+ if (!knowledgeEnabled) {
+ return message;
+ }
+
+ if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) {
+ return message;
+ }
+
+ // if the message hasn't a token, it was not sent by the visitor, so ignore it
+ if (!message.token) {
+ return message;
+ }
+
+ Meteor.defer(() => {
+ try {
+ const response = HTTP.post('https://api.api.ai/api/query?v=20150910', {
+ data: {
+ query: message.msg,
+ lang: apiaiLanguage,
+ sessionId: room._id,
+ },
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Bearer ${ apiaiKey }`,
+ },
+ });
+
+ if (response.data && response.data.status.code === 200 && !_.isEmpty(response.data.result.fulfillment.speech)) {
+ LivechatExternalMessage.insert({
+ rid: message.rid,
+ msg: response.data.result.fulfillment.speech,
+ orig: message._id,
+ ts: new Date(),
+ });
+ }
+ } catch (e) {
+ SystemLogger.error('Error using Api.ai ->', e);
+ }
+ });
+
+ return message;
+}, callbacks.priority.LOW, 'externalWebHook');
diff --git a/app/livechat/server/hooks/leadCapture.js b/app/livechat/server/hooks/leadCapture.js
new file mode 100644
index 000000000000..7dd2092246b6
--- /dev/null
+++ b/app/livechat/server/hooks/leadCapture.js
@@ -0,0 +1,47 @@
+import { callbacks } from '../../../callbacks';
+import { settings } from '../../../settings';
+import { LivechatVisitors } from '../../../models';
+
+function validateMessage(message, room) {
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ return false;
+ }
+
+ // message valid only if it is a livechat room
+ if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) {
+ return false;
+ }
+
+ // if the message hasn't a token, it was NOT sent from the visitor, so ignore it
+ if (!message.token) {
+ return false;
+ }
+
+ // if the message has a type means it is a special message (like the closing comment), so skips
+ if (message.t) {
+ return false;
+ }
+
+ return true;
+}
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ if (!validateMessage(message, room)) {
+ return message;
+ }
+
+ const phoneRegexp = new RegExp(settings.get('Livechat_lead_phone_regex'), 'g');
+ const msgPhones = message.msg.match(phoneRegexp);
+
+ const emailRegexp = new RegExp(settings.get('Livechat_lead_email_regex'), 'gi');
+ const msgEmails = message.msg.match(emailRegexp);
+
+ if (msgEmails || msgPhones) {
+ LivechatVisitors.saveGuestEmailPhoneById(room.v._id, msgEmails, msgPhones);
+
+ callbacks.run('livechat.leadCapture', room);
+ }
+
+ return message;
+}, callbacks.priority.LOW, 'leadCapture');
diff --git a/app/livechat/server/hooks/markRoomResponded.js b/app/livechat/server/hooks/markRoomResponded.js
new file mode 100644
index 000000000000..7a4c2a9a6215
--- /dev/null
+++ b/app/livechat/server/hooks/markRoomResponded.js
@@ -0,0 +1,31 @@
+import { Meteor } from 'meteor/meteor';
+import { callbacks } from '../../../callbacks';
+import { Rooms } from '../../../models';
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ // skips this callback if the message was edited
+ if (!message || message.editedAt) {
+ return message;
+ }
+
+ // check if room is yet awaiting for response
+ if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse)) {
+ return message;
+ }
+
+ // if the message has a token, it was sent by the visitor, so ignore it
+ if (message.token) {
+ return message;
+ }
+
+ Meteor.defer(() => {
+ Rooms.setResponseByRoomId(room._id, {
+ user: {
+ _id: message.u._id,
+ username: message.u.username,
+ },
+ });
+ });
+
+ return message;
+}, callbacks.priority.LOW, 'markRoomResponded');
diff --git a/app/livechat/server/hooks/offlineMessage.js b/app/livechat/server/hooks/offlineMessage.js
new file mode 100644
index 000000000000..91a0641c3fdc
--- /dev/null
+++ b/app/livechat/server/hooks/offlineMessage.js
@@ -0,0 +1,21 @@
+import { callbacks } from '../../../callbacks';
+import { settings } from '../../../settings';
+import { Livechat } from '../lib/Livechat';
+
+callbacks.add('livechat.offlineMessage', (data) => {
+ if (!settings.get('Livechat_webhook_on_offline_msg')) {
+ return data;
+ }
+
+ const postData = {
+ type: 'LivechatOfflineMessage',
+ sentAt: new Date(),
+ visitor: {
+ name: data.name,
+ email: data.email,
+ },
+ message: data.message,
+ };
+
+ Livechat.sendRequest(postData);
+}, callbacks.priority.MEDIUM, 'livechat-send-email-offline-message');
diff --git a/packages/rocketchat-livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js
similarity index 88%
rename from packages/rocketchat-livechat/server/hooks/saveAnalyticsData.js
rename to app/livechat/server/hooks/saveAnalyticsData.js
index 31b178fede11..286542847486 100644
--- a/packages/rocketchat-livechat/server/hooks/saveAnalyticsData.js
+++ b/app/livechat/server/hooks/saveAnalyticsData.js
@@ -1,7 +1,8 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { callbacks } from '../../../callbacks';
+import { Rooms } from '../../../models';
-RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
+callbacks.add('afterSaveMessage', function(message, room) {
// skips this callback if the message was edited
if (!message || message.editedAt) {
return message;
@@ -58,8 +59,8 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
} // ignore, its continuing response
}
- RocketChat.models.Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData);
+ Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData);
});
return message;
-}, RocketChat.callbacks.priority.LOW, 'saveAnalyticsData');
+}, callbacks.priority.LOW, 'saveAnalyticsData');
diff --git a/app/livechat/server/hooks/sendToCRM.js b/app/livechat/server/hooks/sendToCRM.js
new file mode 100644
index 000000000000..a09b06177e90
--- /dev/null
+++ b/app/livechat/server/hooks/sendToCRM.js
@@ -0,0 +1,119 @@
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { Messages, Rooms } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+const msgNavType = 'livechat_navigation_history';
+
+const crmEnabled = () => {
+ const secretToken = settings.get('Livechat_secret_token');
+ const webhookUrl = settings.get('Livechat_webhookUrl');
+ return secretToken !== '' && secretToken !== undefined && webhookUrl !== '' && webhookUrl !== undefined;
+};
+
+const sendMessageType = (msgType) => {
+ const sendNavHistory = settings.get('Livechat_Visitor_navigation_as_a_message') && settings.get('Send_visitor_navigation_history_livechat_webhook_request');
+
+ return sendNavHistory && msgType === msgNavType;
+};
+
+function sendToCRM(type, room, includeMessages = true) {
+ if (crmEnabled() === false) {
+ return room;
+ }
+
+ const postData = Livechat.getLivechatRoomGuestInfo(room);
+
+ postData.type = type;
+
+ postData.messages = [];
+
+ let messages;
+ if (typeof includeMessages === 'boolean' && includeMessages) {
+ messages = Messages.findVisibleByRoomId(room._id, { sort: { ts: 1 } });
+ } else if (includeMessages instanceof Array) {
+ messages = includeMessages;
+ }
+
+ if (messages) {
+ messages.forEach((message) => {
+ if (message.t && !sendMessageType(message.t)) {
+ return;
+ }
+ const msg = {
+ _id: message._id,
+ username: message.u.username,
+ msg: message.msg,
+ ts: message.ts,
+ editedAt: message.editedAt,
+ };
+
+ if (message.u.username !== postData.visitor.username) {
+ msg.agentId = message.u._id;
+ }
+
+ if (message.t === msgNavType) {
+ msg.navigation = message.navigation;
+ }
+
+ postData.messages.push(msg);
+ });
+ }
+
+ const response = Livechat.sendRequest(postData);
+
+ if (response && response.data && response.data.data) {
+ Rooms.saveCRMDataByRoomId(room._id, response.data.data);
+ }
+
+ return room;
+}
+
+callbacks.add('livechat.closeRoom', (room) => {
+ if (!settings.get('Livechat_webhook_on_close')) {
+ return room;
+ }
+
+ return sendToCRM('LivechatSession', room);
+}, callbacks.priority.MEDIUM, 'livechat-send-crm-close-room');
+
+callbacks.add('livechat.saveInfo', (room) => {
+ // Do not send to CRM if the chat is still open
+ if (room.open) {
+ return room;
+ }
+
+ return sendToCRM('LivechatEdit', room);
+}, callbacks.priority.MEDIUM, 'livechat-send-crm-save-info');
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ // only call webhook if it is a livechat room
+ if (room.t !== 'l' || room.v == null || room.v.token == null) {
+ return message;
+ }
+
+ // if the message has a token, it was sent from the visitor
+ // if not, it was sent from the agent
+ if (message.token) {
+ if (!settings.get('Livechat_webhook_on_visitor_message')) {
+ return message;
+ }
+ } else if (!settings.get('Livechat_webhook_on_agent_message')) {
+ return message;
+ }
+ // if the message has a type means it is a special message (like the closing comment), so skips
+ // unless the settings that handle with visitor navigation history are enabled
+ if (message.t && !sendMessageType(message.t)) {
+ return message;
+ }
+
+ sendToCRM('Message', room, [message]);
+ return message;
+}, callbacks.priority.MEDIUM, 'livechat-send-crm-message');
+
+callbacks.add('livechat.leadCapture', (room) => {
+ if (!settings.get('Livechat_webhook_on_capture')) {
+ return room;
+ }
+ return sendToCRM('LeadCapture', room, false);
+}, callbacks.priority.MEDIUM, 'livechat-send-crm-lead-capture');
diff --git a/app/livechat/server/hooks/sendToFacebook.js b/app/livechat/server/hooks/sendToFacebook.js
new file mode 100644
index 000000000000..1977dffe270b
--- /dev/null
+++ b/app/livechat/server/hooks/sendToFacebook.js
@@ -0,0 +1,38 @@
+import { callbacks } from '../../../callbacks';
+import { settings } from '../../../settings';
+import OmniChannel from '../lib/OmniChannel';
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ return message;
+ }
+
+ if (!settings.get('Livechat_Facebook_Enabled') || !settings.get('Livechat_Facebook_API_Key')) {
+ return message;
+ }
+
+ // only send the sms by SMS if it is a livechat room with SMS set to true
+ if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.facebook && room.v && room.v.token)) {
+ return message;
+ }
+
+ // if the message has a token, it was sent from the visitor, so ignore it
+ if (message.token) {
+ return message;
+ }
+
+ // if the message has a type means it is a special message (like the closing comment), so skips
+ if (message.t) {
+ return message;
+ }
+
+ OmniChannel.reply({
+ page: room.facebook.page.id,
+ token: room.v.token,
+ text: message.msg,
+ });
+
+ return message;
+
+}, callbacks.priority.LOW, 'sendMessageToFacebook');
diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js
new file mode 100644
index 000000000000..3458109384d3
--- /dev/null
+++ b/app/livechat/server/index.js
@@ -0,0 +1,90 @@
+import './livechat';
+import './startup';
+import './visitorStatus';
+import './agentStatus';
+import './permissions';
+import '../lib/messageTypes';
+import './config';
+import './roomType';
+import './hooks/externalMessage';
+import './hooks/leadCapture';
+import './hooks/markRoomResponded';
+import './hooks/offlineMessage';
+import './hooks/RDStation';
+import './hooks/saveAnalyticsData';
+import './hooks/sendToCRM';
+import './hooks/sendToFacebook';
+import './methods/addAgent';
+import './methods/addManager';
+import './methods/changeLivechatStatus';
+import './methods/closeByVisitor';
+import './methods/closeRoom';
+import './methods/facebook';
+import './methods/getCustomFields';
+import './methods/getAgentData';
+import './methods/getAgentOverviewData';
+import './methods/getAnalyticsChartData';
+import './methods/getAnalyticsOverviewData';
+import './methods/getInitialData';
+import './methods/getNextAgent';
+import './methods/loadHistory';
+import './methods/loginByToken';
+import './methods/pageVisited';
+import './methods/registerGuest';
+import './methods/removeAgent';
+import './methods/removeCustomField';
+import './methods/removeDepartment';
+import './methods/removeManager';
+import './methods/removeTrigger';
+import './methods/removeRoom';
+import './methods/saveAppearance';
+import './methods/saveCustomField';
+import './methods/saveDepartment';
+import './methods/saveInfo';
+import './methods/saveIntegration';
+import './methods/saveSurveyFeedback';
+import './methods/saveTrigger';
+import './methods/searchAgent';
+import './methods/sendMessageLivechat';
+import './methods/sendFileLivechatMessage';
+import './methods/sendOfflineMessage';
+import './methods/setCustomField';
+import './methods/setDepartmentForVisitor';
+import './methods/startVideoCall';
+import './methods/startFileUploadRoom';
+import './methods/transfer';
+import './methods/webhookTest';
+import './methods/setUpConnection';
+import './methods/takeInquiry';
+import './methods/returnAsInquiry';
+import './methods/saveOfficeHours';
+import './methods/sendTranscript';
+import './methods/getFirstRoomMessage';
+import '../lib/LivechatExternalMessage';
+import '../lib/LivechatInquiry';
+export { Livechat } from './lib/Livechat';
+import './lib/Analytics';
+import './lib/QueueMethods';
+import './lib/OfficeClock';
+import './sendMessageBySMS';
+import './unclosedLivechats';
+import './publications/customFields';
+import './publications/departmentAgents';
+import './publications/externalMessages';
+import './publications/livechatAgents';
+import './publications/livechatAppearance';
+import './publications/livechatDepartments';
+import './publications/livechatIntegration';
+import './publications/livechatManagers';
+import './publications/livechatMonitoring';
+import './publications/livechatRooms';
+import './publications/livechatQueue';
+import './publications/livechatTriggers';
+import './publications/livechatVisitors';
+import './publications/visitorHistory';
+import './publications/visitorInfo';
+import './publications/visitorPageVisited';
+import './publications/livechatInquiries';
+import './publications/livechatOfficeHours';
+import './api';
+import './api/rest';
diff --git a/packages/rocketchat-livechat/server/lib/Analytics.js b/app/livechat/server/lib/Analytics.js
similarity index 92%
rename from packages/rocketchat-livechat/server/lib/Analytics.js
rename to app/livechat/server/lib/Analytics.js
index b70301f6b424..4e9e5b88d781 100644
--- a/packages/rocketchat-livechat/server/lib/Analytics.js
+++ b/app/livechat/server/lib/Analytics.js
@@ -1,4 +1,4 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Rooms } from '../../../models';
import moment from 'moment';
/**
@@ -119,14 +119,14 @@ export const Analytics = {
* @returns {Integer}
*/
Total_conversations(date) {
- return RocketChat.models.Rooms.getTotalConversationsBetweenDate('l', date);
+ return Rooms.getTotalConversationsBetweenDate('l', date);
},
Avg_chat_duration(date) {
let total = 0;
let count = 0;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
if (metrics && metrics.chatDuration) {
total += metrics.chatDuration;
count++;
@@ -140,7 +140,7 @@ export const Analytics = {
Total_messages(date) {
let total = 0;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ msgs }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ msgs }) => {
if (msgs) {
total += msgs;
}
@@ -158,7 +158,7 @@ export const Analytics = {
Avg_first_response_time(date) {
let frt = 0;
let count = 0;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
if (metrics && metrics.response && metrics.response.ft) {
frt += metrics.response.ft;
count++;
@@ -178,7 +178,7 @@ export const Analytics = {
Best_first_response_time(date) {
let maxFrt;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
if (metrics && metrics.response && metrics.response.ft) {
maxFrt = (maxFrt) ? Math.min(maxFrt, metrics.response.ft) : metrics.response.ft;
}
@@ -198,7 +198,7 @@ export const Analytics = {
Avg_response_time(date) {
let art = 0;
let count = 0;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
if (metrics && metrics.response && metrics.response.avg) {
art += metrics.response.avg;
count++;
@@ -219,7 +219,7 @@ export const Analytics = {
Avg_reaction_time(date) {
let arnt = 0;
let count = 0;
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({ metrics }) => {
if (metrics && metrics.reaction && metrics.reaction.ft) {
arnt += metrics.reaction.ft;
count++;
@@ -284,7 +284,7 @@ export const Analytics = {
lt: moment(m).add(1, 'days'),
};
- const result = RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date);
+ const result = Rooms.getAnalyticsMetricsBetweenDate('l', date);
totalConversations += result.count();
result.forEach(summarize(m));
@@ -302,7 +302,7 @@ export const Analytics = {
lt: moment(h).add(1, 'hours'),
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
msgs,
}) => {
const dayHour = h.format('H'); // @int : 0, 1, ... 23
@@ -354,7 +354,7 @@ export const Analytics = {
lt: to.add(1, 'days'),
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
}) => {
if (metrics && metrics.response && metrics.reaction) {
@@ -436,7 +436,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
servedBy,
}) => {
if (servedBy) {
@@ -486,7 +486,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
servedBy,
}) => {
@@ -546,7 +546,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
servedBy,
msgs,
}) => {
@@ -590,7 +590,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
servedBy,
}) => {
@@ -650,7 +650,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
servedBy,
}) => {
@@ -702,7 +702,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
servedBy,
}) => {
@@ -762,7 +762,7 @@ export const Analytics = {
data: [],
};
- RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
+ Rooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
metrics,
servedBy,
}) => {
diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js
new file mode 100644
index 000000000000..c716b84ee326
--- /dev/null
+++ b/app/livechat/server/lib/Livechat.js
@@ -0,0 +1,955 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { HTTP } from 'meteor/http';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { Users, Rooms, Messages, Subscriptions, Settings, LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, LivechatVisitors } from '../../../models';
+import { Logger } from '../../../logger';
+import { sendMessage, deleteMessage, updateMessage } from '../../../lib';
+import { addUserRoles, removeUserFromRoles } from '../../../authorization';
+import _ from 'underscore';
+import s from 'underscore.string';
+import moment from 'moment';
+import dns from 'dns';
+import UAParser from 'ua-parser-js';
+import * as Mailer from '../../../mailer';
+import { LivechatInquiry } from '../../lib/LivechatInquiry';
+import { QueueMethods } from './QueueMethods';
+import { Analytics } from './Analytics';
+
+export const Livechat = {
+ Analytics,
+ historyMonitorType: 'url',
+
+ logger: new Logger('Livechat', {
+ sections: {
+ webhook: 'Webhook',
+ },
+ }),
+
+ getNextAgent(department) {
+ if (settings.get('Livechat_Routing_Method') === 'External') {
+ for (let i = 0; i < 10; i++) {
+ try {
+ const queryString = department ? `?departmentId=${ department }` : '';
+ const result = HTTP.call('GET', `${ settings.get('Livechat_External_Queue_URL') }${ queryString }`, {
+ headers: {
+ 'User-Agent': 'RocketChat Server',
+ Accept: 'application/json',
+ 'X-RocketChat-Secret-Token': settings.get('Livechat_External_Queue_Token'),
+ },
+ });
+
+ if (result && result.data && result.data.username) {
+ const agent = Users.findOneOnlineAgentByUsername(result.data.username);
+
+ if (agent) {
+ return {
+ agentId: agent._id,
+ username: agent.username,
+ };
+ }
+ }
+ } catch (e) {
+ console.error('Error requesting agent from external queue.', e);
+ break;
+ }
+ }
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ } else if (department) {
+ return LivechatDepartmentAgents.getNextAgentForDepartment(department);
+ }
+ return Users.getNextAgent();
+ },
+ getAgents(department) {
+ if (department) {
+ return LivechatDepartmentAgents.findByDepartmentId(department);
+ }
+ return Users.findAgents();
+ },
+ getOnlineAgents(department) {
+ if (department) {
+ return LivechatDepartmentAgents.getOnlineForDepartment(department);
+ }
+ return Users.findOnlineAgents();
+ },
+ getRequiredDepartment(onlineRequired = true) {
+ const departments = LivechatDepartment.findEnabledWithAgents();
+
+ return departments.fetch().find((dept) => {
+ if (!dept.showOnRegistration) {
+ return false;
+ }
+ if (!onlineRequired) {
+ return true;
+ }
+ const onlineAgents = LivechatDepartmentAgents.getOnlineForDepartment(dept._id);
+ return onlineAgents && onlineAgents.count() > 0;
+ });
+ },
+ getRoom(guest, message, roomInfo, agent) {
+ let room = Rooms.findOneById(message.rid);
+ let newRoom = false;
+
+ if (room && !room.open) {
+ message.rid = Random.id();
+ room = null;
+ }
+
+ if (room == null) {
+ // if no department selected verify if there is at least one active and pick the first
+ if (!agent && !guest.department) {
+ const department = this.getRequiredDepartment();
+
+ if (department) {
+ guest.department = department._id;
+ }
+ }
+
+ // delegate room creation to QueueMethods
+ const routingMethod = settings.get('Livechat_Routing_Method');
+ room = QueueMethods[routingMethod](guest, message, roomInfo, agent);
+
+ newRoom = true;
+ }
+
+ if (!room || room.v.token !== guest.token) {
+ throw new Meteor.Error('cannot-access-room');
+ }
+
+ if (newRoom) {
+ Messages.setRoomIdByToken(guest.token, room._id);
+ }
+
+ return { room, newRoom };
+ },
+
+ sendMessage({ guest, message, roomInfo, agent }) {
+ const { room, newRoom } = this.getRoom(guest, message, roomInfo, agent);
+ if (guest.name) {
+ message.alias = guest.name;
+ }
+
+ // return messages;
+ return _.extend(sendMessage(guest, message, room), { newRoom, showConnecting: this.showConnecting() });
+ },
+
+ updateMessage({ guest, message }) {
+ check(message, Match.ObjectIncluding({ _id: String }));
+
+ const originalMessage = Messages.findOneById(message._id);
+ if (!originalMessage || !originalMessage._id) {
+ return;
+ }
+
+ const editAllowed = settings.get('Message_AllowEditing');
+ const editOwn = originalMessage.u && originalMessage.u._id === guest._id;
+
+ if (!editAllowed || !editOwn) {
+ throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { method: 'livechatUpdateMessage' });
+ }
+
+ updateMessage(message, guest);
+
+ return true;
+ },
+
+ deleteMessage({ guest, message }) {
+ check(message, Match.ObjectIncluding({ _id: String }));
+
+ const msg = Messages.findOneById(message._id);
+ if (!msg || !msg._id) {
+ return;
+ }
+
+ const deleteAllowed = settings.get('Message_AllowDeleting');
+ const editOwn = msg.u && msg.u._id === guest._id;
+
+ if (!deleteAllowed || !editOwn) {
+ throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { method: 'livechatDeleteMessage' });
+ }
+
+ deleteMessage(message, guest);
+
+ return true;
+ },
+
+ registerGuest({ token, name, email, department, phone, username, connectionData } = {}) {
+ check(token, String);
+
+ let userId;
+ const updateUser = {
+ $set: {
+ token,
+ },
+ };
+
+ const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });
+
+ if (user) {
+ userId = user._id;
+ } else {
+ if (!username) {
+ username = LivechatVisitors.getNextVisitorUsername();
+ }
+
+ let existingUser = null;
+
+ if (s.trim(email) !== '' && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(email))) {
+ userId = existingUser._id;
+ } else {
+ const userData = {
+ username,
+ };
+
+ if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) {
+
+ const connection = this.connection || connectionData;
+ if (connection && connection.httpHeaders) {
+ userData.userAgent = connection.httpHeaders['user-agent'];
+ userData.ip = connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress;
+ userData.host = connection.httpHeaders.host;
+ }
+ }
+
+ userId = LivechatVisitors.insert(userData);
+ }
+ }
+
+ if (phone) {
+ updateUser.$set.phone = [
+ { phoneNumber: phone.number },
+ ];
+ }
+
+ if (email && email.trim() !== '') {
+ updateUser.$set.visitorEmails = [
+ { address: email },
+ ];
+ }
+
+ if (name) {
+ updateUser.$set.name = name;
+ }
+
+ if (!department) {
+ Object.assign(updateUser, { $unset: { department: 1 } });
+ } else {
+ const dep = LivechatDepartment.findOneByIdOrName(department);
+ updateUser.$set.department = dep && dep._id;
+ }
+
+ LivechatVisitors.updateById(userId, updateUser);
+
+ return userId;
+ },
+
+ setDepartmentForGuest({ token, department } = {}) {
+ check(token, String);
+
+ const updateUser = {
+ $set: {
+ department,
+ },
+ };
+
+ const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });
+ if (user) {
+ return LivechatVisitors.updateById(user._id, updateUser);
+ }
+ return false;
+ },
+
+ saveGuest({ _id, name, email, phone }) {
+ const updateData = {};
+
+ if (name) {
+ updateData.name = name;
+ }
+ if (email) {
+ updateData.email = email;
+ }
+ if (phone) {
+ updateData.phone = phone;
+ }
+ const ret = LivechatVisitors.saveGuestById(_id, updateData);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.saveGuest', updateData);
+ });
+
+ return ret;
+ },
+
+ closeRoom({ user, visitor, room, comment }) {
+ if (!room || room.t !== 'l' || !room.open) {
+ return false;
+ }
+
+ const now = new Date();
+
+ const closeData = {
+ closedAt: now,
+ chatDuration: (now.getTime() - room.ts) / 1000,
+ };
+
+ if (user) {
+ closeData.closer = 'user';
+ closeData.closedBy = {
+ _id: user._id,
+ username: user.username,
+ };
+ } else if (visitor) {
+ closeData.closer = 'visitor';
+ closeData.closedBy = {
+ _id: visitor._id,
+ username: visitor.username,
+ };
+ }
+
+ Rooms.closeByRoomId(room._id, closeData);
+ LivechatInquiry.closeByRoomId(room._id, closeData);
+
+ const message = {
+ t: 'livechat-close',
+ msg: comment,
+ groupable: false,
+ };
+
+ // Retreive the closed room
+ room = Rooms.findOneByIdOrName(room._id);
+
+ sendMessage(user, message, room);
+
+ if (room.servedBy) {
+ Subscriptions.hideByRoomIdAndUserId(room._id, room.servedBy._id);
+ }
+ Messages.createCommandWithRoomIdAndUser('promptTranscript', room._id, closeData.closedBy);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.closeRoom', room);
+ });
+
+ return true;
+ },
+
+ setCustomFields({ token, key, value, overwrite } = {}) {
+ check(token, String);
+ check(key, String);
+ check(value, String);
+ check(overwrite, Boolean);
+
+ const customField = LivechatCustomField.findOneById(key);
+ if (!customField) {
+ throw new Meteor.Error('invalid-custom-field');
+ }
+
+ if (customField.scope === 'room') {
+ return Rooms.updateLivechatDataByToken(token, key, value, overwrite);
+ } else {
+ return LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite);
+ }
+ },
+
+ getInitSettings() {
+ const rcSettings = {};
+
+ Settings.findNotHiddenPublic([
+ 'Livechat_title',
+ 'Livechat_title_color',
+ 'Livechat_enabled',
+ 'Livechat_registration_form',
+ 'Livechat_allow_switching_departments',
+ 'Livechat_offline_title',
+ 'Livechat_offline_title_color',
+ 'Livechat_offline_message',
+ 'Livechat_offline_success_message',
+ 'Livechat_offline_form_unavailable',
+ 'Livechat_display_offline_form',
+ 'Livechat_videocall_enabled',
+ 'Jitsi_Enabled',
+ 'Language',
+ 'Livechat_enable_transcript',
+ 'Livechat_transcript_message',
+ 'Livechat_fileupload_enabled',
+ 'FileUpload_Enabled',
+ 'Livechat_conversation_finished_message',
+ 'Livechat_name_field_registration_form',
+ 'Livechat_email_field_registration_form',
+ 'Livechat_registration_form_message',
+ 'Livechat_force_accept_data_processing_consent',
+ 'Livechat_data_processing_consent_text',
+ ]).forEach((setting) => {
+ rcSettings[setting._id] = setting.value;
+ });
+
+ settings.get('Livechat_history_monitor_type', (key, value) => {
+ rcSettings[key] = value;
+ });
+
+ rcSettings.Livechat_Show_Connecting = this.showConnecting();
+
+ return rcSettings;
+ },
+
+ saveRoomInfo(roomData, guestData) {
+ if ((roomData.topic != null || roomData.tags != null) && !Rooms.setTopicAndTagsById(roomData._id, roomData.topic, roomData.tags)) {
+ return false;
+ }
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.saveRoom', roomData);
+ });
+
+ if (!_.isEmpty(guestData.name)) {
+ return Rooms.setNameById(roomData._id, guestData.name, guestData.name) && Subscriptions.updateDisplayNameByRoomId(roomData._id, guestData.name);
+ }
+ },
+
+ closeOpenChats(userId, comment) {
+ const user = Users.findOneById(userId);
+ Rooms.findOpenByAgent(userId).forEach((room) => {
+ this.closeRoom({ user, room, comment });
+ });
+ },
+
+ forwardOpenChats(userId) {
+ Rooms.findOpenByAgent(userId).forEach((room) => {
+ const guest = LivechatVisitors.findOneById(room.v._id);
+ this.transfer(room, guest, { departmentId: guest.department });
+ });
+ },
+
+ savePageHistory(token, roomId, pageInfo) {
+ if (pageInfo.change === Livechat.historyMonitorType) {
+
+ const user = Users.findOneById('rocket.cat');
+
+ const pageTitle = pageInfo.title;
+ const pageUrl = pageInfo.location.href;
+ const extraData = {
+ navigation: {
+ page: pageInfo,
+ token,
+ },
+ };
+
+ if (!roomId) {
+ // keep history of unregistered visitors for 1 month
+ const keepHistoryMiliseconds = 2592000000;
+ extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds;
+ }
+
+ if (!settings.get('Livechat_Visitor_navigation_as_a_message')) {
+ extraData._hidden = true;
+ }
+
+ return Messages.createNavigationHistoryWithRoomIdMessageAndUser(roomId, `${ pageTitle } - ${ pageUrl }`, user, extraData);
+ }
+
+ return;
+ },
+
+ transfer(room, guest, transferData) {
+ let agent;
+
+ if (transferData.userId) {
+ const user = Users.findOneOnlineAgentById(transferData.userId);
+ if (!user) {
+ return false;
+ }
+
+ const { _id: agentId, username } = user;
+ agent = Object.assign({}, { agentId, username });
+ } else if (settings.get('Livechat_Routing_Method') !== 'Guest_Pool') {
+ agent = Livechat.getNextAgent(transferData.departmentId);
+ } else {
+ return Livechat.returnRoomAsInquiry(room._id, transferData.departmentId);
+ }
+
+ const { servedBy } = room;
+
+ if (agent && servedBy && agent.agentId !== servedBy._id) {
+ Rooms.changeAgentByRoomId(room._id, agent);
+
+ if (transferData.departmentId) {
+ Rooms.changeDepartmentIdByRoomId(room._id, transferData.departmentId);
+ }
+
+ const subscriptionData = {
+ rid: room._id,
+ name: guest.name || guest.username,
+ alert: true,
+ open: true,
+ unread: 1,
+ userMentions: 1,
+ groupMentions: 0,
+ u: {
+ _id: agent.agentId,
+ username: agent.username,
+ },
+ t: 'l',
+ desktopNotifications: 'all',
+ mobilePushNotifications: 'all',
+ emailNotifications: 'all',
+ };
+ Subscriptions.removeByRoomIdAndUserId(room._id, servedBy._id);
+
+ Subscriptions.insert(subscriptionData);
+ Rooms.incUsersCountById(room._id);
+
+ Messages.createUserLeaveWithRoomIdAndUser(room._id, { _id: servedBy._id, username: servedBy.username });
+ Messages.createUserJoinWithRoomIdAndUser(room._id, { _id: agent.agentId, username: agent.username });
+
+ const guestData = {
+ token: guest.token,
+ department: transferData.departmentId,
+ };
+
+ this.setDepartmentForGuest(guestData);
+ const data = Users.getAgentInfo(agent.agentId);
+
+ Livechat.stream.emit(room._id, {
+ type: 'agentData',
+ data,
+ });
+
+ return true;
+ }
+
+ return false;
+ },
+
+ returnRoomAsInquiry(rid, departmentId) {
+ const room = Rooms.findOneById(rid);
+ if (!room) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:returnRoomAsInquiry' });
+ }
+
+ if (!room.servedBy) {
+ return false;
+ }
+
+ const user = Users.findOne(room.servedBy._id);
+ if (!user || !user._id) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:returnRoomAsInquiry' });
+ }
+
+ const agentIds = [];
+ // get the agents of the department
+ if (departmentId) {
+ const agents = Livechat.getAgents(departmentId);
+ if (!agents || agents.count() === 0) {
+ return false;
+ }
+
+ agents.forEach((agent) => {
+ agentIds.push(agent.agentId);
+ });
+
+ Rooms.changeDepartmentIdByRoomId(room._id, departmentId);
+ }
+
+ // delete agent and room subscription
+ Subscriptions.removeByRoomId(rid);
+
+ // remove agent from room
+ Rooms.removeAgentByRoomId(rid);
+
+ // find inquiry corresponding to room
+ const inquiry = LivechatInquiry.findOne({ rid });
+ if (!inquiry) {
+ return false;
+ }
+
+ let openInq;
+ // mark inquiry as open
+ if (agentIds.length === 0) {
+ openInq = LivechatInquiry.openInquiry(inquiry._id);
+ } else {
+ openInq = LivechatInquiry.openInquiryWithAgents(inquiry._id, agentIds);
+ }
+
+ if (openInq) {
+ Messages.createUserLeaveWithRoomIdAndUser(rid, { _id: room.servedBy._id, username: room.servedBy.username });
+
+ Livechat.stream.emit(rid, {
+ type: 'agentData',
+ data: null,
+ });
+ }
+
+ return openInq;
+ },
+
+ sendRequest(postData, callback, trying = 1) {
+ try {
+ const options = {
+ headers: {
+ 'X-RocketChat-Livechat-Token': settings.get('Livechat_secret_token'),
+ },
+ data: postData,
+ };
+ return HTTP.post(settings.get('Livechat_webhookUrl'), options);
+ } catch (e) {
+ Livechat.logger.webhook.error(`Response error on ${ trying } try ->`, e);
+ // try 10 times after 10 seconds each
+ if (trying < 10) {
+ Livechat.logger.webhook.warn('Will try again in 10 seconds ...');
+ trying++;
+ setTimeout(Meteor.bindEnvironment(() => {
+ Livechat.sendRequest(postData, callback, trying);
+ }), 10000);
+ }
+ }
+ },
+
+ getLivechatRoomGuestInfo(room) {
+ const visitor = LivechatVisitors.findOneById(room.v._id);
+ const agent = Users.findOneById(room.servedBy && room.servedBy._id);
+
+ const ua = new UAParser();
+ ua.setUA(visitor.userAgent);
+
+ const postData = {
+ _id: room._id,
+ label: room.fname || room.label, // using same field for compatibility
+ topic: room.topic,
+ createdAt: room.ts,
+ lastMessageAt: room.lm,
+ tags: room.tags,
+ customFields: room.livechatData,
+ visitor: {
+ _id: visitor._id,
+ token: visitor.token,
+ name: visitor.name,
+ username: visitor.username,
+ email: null,
+ phone: null,
+ department: visitor.department,
+ ip: visitor.ip,
+ os: ua.getOS().name && (`${ ua.getOS().name } ${ ua.getOS().version }`),
+ browser: ua.getBrowser().name && (`${ ua.getBrowser().name } ${ ua.getBrowser().version }`),
+ customFields: visitor.livechatData,
+ },
+ };
+
+ if (agent) {
+ postData.agent = {
+ _id: agent._id,
+ username: agent.username,
+ name: agent.name,
+ email: null,
+ };
+
+ if (agent.emails && agent.emails.length > 0) {
+ postData.agent.email = agent.emails[0].address;
+ }
+ }
+
+ if (room.crmData) {
+ postData.crmData = room.crmData;
+ }
+
+ if (visitor.visitorEmails && visitor.visitorEmails.length > 0) {
+ postData.visitor.email = visitor.visitorEmails;
+ }
+ if (visitor.phone && visitor.phone.length > 0) {
+ postData.visitor.phone = visitor.phone;
+ }
+
+ return postData;
+ },
+
+ addAgent(username) {
+ check(username, String);
+
+ const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addAgent' });
+ }
+
+ if (addUserRoles(user._id, 'livechat-agent')) {
+ Users.setOperator(user._id, true);
+ Users.setLivechatStatus(user._id, 'available');
+ return user;
+ }
+
+ return false;
+ },
+
+ addManager(username) {
+ check(username, String);
+
+ const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addManager' });
+ }
+
+ if (addUserRoles(user._id, 'livechat-manager')) {
+ return user;
+ }
+
+ return false;
+ },
+
+ removeAgent(username) {
+ check(username, String);
+
+ const user = Users.findOneByUsername(username, { fields: { _id: 1 } });
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeAgent' });
+ }
+
+ if (removeUserFromRoles(user._id, 'livechat-agent')) {
+ Users.setOperator(user._id, false);
+ Users.setLivechatStatus(user._id, 'not-available');
+ return true;
+ }
+
+ return false;
+ },
+
+ removeManager(username) {
+ check(username, String);
+
+ const user = Users.findOneByUsername(username, { fields: { _id: 1 } });
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeManager' });
+ }
+
+ return removeUserFromRoles(user._id, 'livechat-manager');
+ },
+
+ removeGuest(_id) {
+ check(_id, String);
+
+ const guest = LivechatVisitors.findById(_id);
+ if (!guest) {
+ throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest' });
+ }
+
+ this.cleanGuestHistory(_id);
+ return LivechatVisitors.removeById(_id);
+ },
+
+ cleanGuestHistory(_id) {
+ const guest = LivechatVisitors.findById(_id);
+ if (!guest) {
+ throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:cleanGuestHistory' });
+ }
+
+ const { token } = guest;
+
+ Rooms.findByVisitorToken(token).forEach((room) => {
+ Messages.removeFilesByRoomId(room._id);
+ Messages.removeByRoomId(room._id);
+ });
+
+ Subscriptions.removeByVisitorToken(token);
+ Rooms.removeByVisitorToken(token);
+ },
+
+ saveDepartment(_id, departmentData, departmentAgents) {
+ check(_id, Match.Maybe(String));
+
+ check(departmentData, {
+ enabled: Boolean,
+ name: String,
+ description: Match.Optional(String),
+ showOnRegistration: Boolean,
+ email: String,
+ showOnOfflineForm: Boolean,
+ });
+
+ check(departmentAgents, [
+ Match.ObjectIncluding({
+ agentId: String,
+ username: String,
+ }),
+ ]);
+
+ if (_id) {
+ const department = LivechatDepartment.findOneById(_id);
+ if (!department) {
+ throw new Meteor.Error('error-department-not-found', 'Department not found', { method: 'livechat:saveDepartment' });
+ }
+ }
+
+ return LivechatDepartment.createOrUpdateDepartment(_id, departmentData, departmentAgents);
+ },
+
+ removeDepartment(_id) {
+ check(_id, String);
+
+ const department = LivechatDepartment.findOneById(_id, { fields: { _id: 1 } });
+
+ if (!department) {
+ throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' });
+ }
+
+ return LivechatDepartment.removeById(_id);
+ },
+
+ showConnecting() {
+ return settings.get('Livechat_Routing_Method') === 'Guest_Pool';
+ },
+
+ sendEmail(from, to, replyTo, subject, html) {
+ Mailer.send({
+ to,
+ from,
+ replyTo,
+ subject,
+ html,
+ });
+ },
+
+ sendTranscript({ token, rid, email }) {
+ check(rid, String);
+ check(email, String);
+
+ const room = Rooms.findOneById(rid);
+
+ const visitor = LivechatVisitors.getVisitorByToken(token);
+ const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en';
+
+ // allow to only user to send transcripts from their own chats
+ if (!room || room.t !== 'l' || !room.v || room.v.token !== token) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room');
+ }
+
+ const messages = Messages.findVisibleByRoomIdNotContainingTypes(rid, ['livechat_navigation_history'], { sort: { ts: 1 } });
+
+ let html = '
';
+ messages.forEach((message) => {
+ if (message.t && ['command', 'livechat-close', 'livechat_video_call'].indexOf(message.t) !== -1) {
+ return;
+ }
+
+ let author;
+ if (message.u._id === visitor._id) {
+ author = TAPi18n.__('You', { lng: userLanguage });
+ } else {
+ author = message.u.username;
+ }
+
+ const datetime = moment(message.ts).locale(userLanguage).format('LLL');
+ const singleMessage = `
+
${ author } ${ datetime }
+
${ message.msg }
+ `;
+ html = html + singleMessage;
+ });
+
+ html = `${ html }
`;
+
+ let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);
+
+ if (fromEmail) {
+ fromEmail = fromEmail[0];
+ } else {
+ fromEmail = settings.get('From_Email');
+ }
+
+ const subject = TAPi18n.__('Transcript_of_your_livechat_conversation', { lng: userLanguage });
+
+ this.sendEmail(fromEmail, email, fromEmail, subject, html);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.sendTranscript', messages, email);
+ });
+
+ return true;
+ },
+
+ notifyGuestStatusChanged(token, status) {
+ LivechatInquiry.updateVisitorStatus(token, status);
+ Rooms.updateVisitorStatus(token, status);
+ },
+
+ sendOfflineMessage(data = {}) {
+ if (!settings.get('Livechat_display_offline_form')) {
+ return false;
+ }
+
+ const message = (`${ data.message }`).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '
' + '$2');
+
+ const html = `
+
New livechat message
+
Visitor name: ${ data.name }
+
Visitor email: ${ data.email }
+
Message: ${ message }
`;
+
+ let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);
+
+ if (fromEmail) {
+ fromEmail = fromEmail[0];
+ } else {
+ fromEmail = settings.get('From_Email');
+ }
+
+ if (settings.get('Livechat_validate_offline_email')) {
+ const emailDomain = data.email.substr(data.email.lastIndexOf('@') + 1);
+
+ try {
+ Meteor.wrapAsync(dns.resolveMx)(emailDomain);
+ } catch (e) {
+ throw new Meteor.Error('error-invalid-email-address', 'Invalid email address', { method: 'livechat:sendOfflineMessage' });
+ }
+ }
+
+ let emailTo = settings.get('Livechat_offline_email');
+ if (data.department) {
+ const dep = LivechatDepartment.findOneByIdOrName(data.department);
+ emailTo = dep.email || emailTo;
+ }
+
+ const from = `${ data.name } - ${ data.email } <${ fromEmail }>`;
+ const replyTo = `${ data.name } <${ data.email }>`;
+ const subject = `Livechat offline message from ${ data.name }: ${ (`${ data.message }`).substring(0, 20) }`;
+
+ this.sendEmail(from, emailTo, replyTo, subject, html);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.offlineMessage', data);
+ });
+
+ return true;
+ },
+
+ notifyAgentStatusChanged(userId, status) {
+ Rooms.findOpenByAgent(userId).forEach((room) => {
+ Livechat.stream.emit(room._id, {
+ type: 'agentStatus',
+ status,
+ });
+ });
+ },
+};
+
+Livechat.stream = new Meteor.Streamer('livechat-room');
+
+Livechat.stream.allowRead((roomId, extraData) => {
+ const room = Rooms.findOneById(roomId);
+
+ if (!room) {
+ console.warn(`Invalid eventName: "${ roomId }"`);
+ return false;
+ }
+
+ if (room.t === 'l' && extraData && extraData.visitorToken && room.v.token === extraData.visitorToken) {
+ return true;
+ }
+ return false;
+});
+
+settings.get('Livechat_history_monitor_type', (key, value) => {
+ Livechat.historyMonitorType = value;
+});
diff --git a/app/livechat/server/lib/OfficeClock.js b/app/livechat/server/lib/OfficeClock.js
new file mode 100644
index 000000000000..8c5c96d2ea35
--- /dev/null
+++ b/app/livechat/server/lib/OfficeClock.js
@@ -0,0 +1,14 @@
+// Every minute check if office closed
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../../settings';
+import { Users, LivechatOfficeHour } from '../../../models';
+
+Meteor.setInterval(function() {
+ if (settings.get('Livechat_enable_office_hours')) {
+ if (LivechatOfficeHour.isOpeningTime()) {
+ Users.openOffice();
+ } else if (LivechatOfficeHour.isClosingTime()) {
+ Users.closeOffice();
+ }
+ }
+}, 60000);
diff --git a/app/livechat/server/lib/OmniChannel.js b/app/livechat/server/lib/OmniChannel.js
new file mode 100644
index 000000000000..b1eb1139b0dc
--- /dev/null
+++ b/app/livechat/server/lib/OmniChannel.js
@@ -0,0 +1,69 @@
+import { HTTP } from 'meteor/http';
+import { settings } from '../../../settings';
+
+const gatewayURL = 'https://omni.rocket.chat';
+
+export default {
+ enable() {
+ const result = HTTP.call('POST', `${ gatewayURL }/facebook/enable`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ 'content-type': 'application/json',
+ },
+ data: {
+ url: settings.get('Site_Url'),
+ },
+ });
+ return result.data;
+ },
+
+ disable() {
+ const result = HTTP.call('DELETE', `${ gatewayURL }/facebook/enable`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ 'content-type': 'application/json',
+ },
+ });
+ return result.data;
+ },
+
+ listPages() {
+ const result = HTTP.call('GET', `${ gatewayURL }/facebook/pages`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ },
+ });
+ return result.data;
+ },
+
+ subscribe(pageId) {
+ const result = HTTP.call('POST', `${ gatewayURL }/facebook/page/${ pageId }/subscribe`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ },
+ });
+ return result.data;
+ },
+
+ unsubscribe(pageId) {
+ const result = HTTP.call('DELETE', `${ gatewayURL }/facebook/page/${ pageId }/subscribe`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ },
+ });
+ return result.data;
+ },
+
+ reply({ page, token, text }) {
+ return HTTP.call('POST', `${ gatewayURL }/facebook/reply`, {
+ headers: {
+ authorization: `Bearer ${ settings.get('Livechat_Facebook_API_Key') }`,
+ },
+ data: {
+ page,
+ token,
+ text,
+ },
+ });
+ },
+};
diff --git a/app/livechat/server/lib/QueueMethods.js b/app/livechat/server/lib/QueueMethods.js
new file mode 100644
index 000000000000..aed907b4f6f9
--- /dev/null
+++ b/app/livechat/server/lib/QueueMethods.js
@@ -0,0 +1,197 @@
+import { Meteor } from 'meteor/meteor';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Subscriptions, Users } from '../../../models';
+import { settings } from '../../../settings';
+import _ from 'underscore';
+import { sendNotification } from '../../../lib';
+import { LivechatInquiry } from '../../lib/LivechatInquiry';
+import { Livechat } from './Livechat';
+
+export const QueueMethods = {
+ /* Least Amount Queuing method:
+ *
+ * default method where the agent with the least number
+ * of open chats is paired with the incoming livechat
+ */
+ 'Least_Amount'(guest, message, roomInfo, agent) {
+ if (!agent || (agent.username && !Users.findOneOnlineAgentByUsername(agent.username))) {
+ agent = Livechat.getNextAgent(guest.department);
+ if (!agent) {
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ }
+ }
+
+ Rooms.updateLivechatRoomCount();
+
+ const room = _.extend({
+ _id: message.rid,
+ msgs: 0,
+ usersCount: 1,
+ lm: new Date(),
+ fname: (roomInfo && roomInfo.fname) || guest.name || guest.username,
+ // usernames: [agent.username, guest.username],
+ t: 'l',
+ ts: new Date(),
+ v: {
+ _id: guest._id,
+ username: guest.username,
+ token: message.token,
+ status: guest.status || 'online',
+ },
+ servedBy: {
+ _id: agent.agentId,
+ username: agent.username,
+ ts: new Date(),
+ },
+ cl: false,
+ open: true,
+ waitingResponse: true,
+ }, roomInfo);
+
+ const subscriptionData = {
+ rid: message.rid,
+ fname: guest.name || guest.username,
+ alert: true,
+ open: true,
+ unread: 1,
+ userMentions: 1,
+ groupMentions: 0,
+ u: {
+ _id: agent.agentId,
+ username: agent.username,
+ },
+ t: 'l',
+ desktopNotifications: 'all',
+ mobilePushNotifications: 'all',
+ emailNotifications: 'all',
+ };
+
+ if (guest.department) {
+ room.departmentId = guest.department;
+ }
+
+ Rooms.insert(room);
+
+ Subscriptions.insert(subscriptionData);
+
+ Livechat.stream.emit(room._id, {
+ type: 'agentData',
+ data: Users.getAgentInfo(agent.agentId),
+ });
+
+ return room;
+ },
+ /* Guest Pool Queuing Method:
+ *
+ * An incomming livechat is created as an Inquiry
+ * which is picked up from an agent.
+ * An Inquiry is visible to all agents (TODO: in the correct department)
+ *
+ * A room is still created with the initial message, but it is occupied by
+ * only the client until paired with an agent
+ */
+ 'Guest_Pool'(guest, message, roomInfo) {
+ const onlineAgents = Livechat.getOnlineAgents(guest.department);
+ if (settings.get('Livechat_guest_pool_with_no_agents') === false) {
+ if (!onlineAgents || onlineAgents.count() === 0) {
+ throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
+ }
+ }
+
+ const allAgents = Livechat.getAgents(guest.department);
+ if (allAgents.count() === 0) {
+ throw new Meteor.Error('no-agent-available', 'Sorry, no available agents.');
+ }
+
+ Rooms.updateLivechatRoomCount();
+
+ const agentIds = [];
+
+ allAgents.forEach((agent) => {
+ if (guest.department) {
+ agentIds.push(agent.agentId);
+ } else {
+ agentIds.push(agent._id);
+ }
+ });
+
+ const inquiry = {
+ rid: message.rid,
+ message: message.msg,
+ name: guest.name || guest.username,
+ ts: new Date(),
+ department: guest.department,
+ agents: agentIds,
+ status: 'open',
+ v: {
+ _id: guest._id,
+ username: guest.username,
+ token: message.token,
+ status: guest.status || 'online',
+ },
+ t: 'l',
+ };
+
+ const room = _.extend({
+ _id: message.rid,
+ msgs: 0,
+ usersCount: 0,
+ lm: new Date(),
+ fname: guest.name || guest.username,
+ // usernames: [guest.username],
+ t: 'l',
+ ts: new Date(),
+ v: {
+ _id: guest._id,
+ username: guest.username,
+ token: message.token,
+ status: guest.status,
+ },
+ cl: false,
+ open: true,
+ waitingResponse: true,
+ }, roomInfo);
+
+ if (guest.department) {
+ room.departmentId = guest.department;
+ }
+
+ LivechatInquiry.insert(inquiry);
+ Rooms.insert(room);
+
+ // Alert only the online agents of the queued request
+ onlineAgents.forEach((agent) => {
+ const { _id, active, emails, language, status, statusConnection, username } = agent;
+
+ sendNotification({
+ // fake a subscription in order to make use of the function defined above
+ subscription: {
+ rid: room._id,
+ t : room.t,
+ u: {
+ _id,
+ },
+ receiver: [{
+ active,
+ emails,
+ language,
+ status,
+ statusConnection,
+ username,
+ }],
+ },
+ sender: room.v,
+ hasMentionToAll: true, // consider all agents to be in the room
+ hasMentionToHere: false,
+ message: Object.assign(message, { u: room.v }),
+ notificationMessage: message.msg,
+ room: Object.assign(room, { name: TAPi18n.__('New_livechat_in_queue') }),
+ mentionIds: [],
+ });
+ });
+ return room;
+ },
+ 'External'(guest, message, roomInfo, agent) {
+ return this['Least_Amount'](guest, message, roomInfo, agent); // eslint-disable-line
+ },
+};
diff --git a/app/livechat/server/livechat.js b/app/livechat/server/livechat.js
new file mode 100644
index 000000000000..84b6edc7beb4
--- /dev/null
+++ b/app/livechat/server/livechat.js
@@ -0,0 +1,43 @@
+import _ from 'underscore';
+import url from 'url';
+
+import { Meteor } from 'meteor/meteor';
+import { WebApp } from 'meteor/webapp';
+
+import { settings } from '../../settings/server';
+import { addServerUrlToIndex, addServerUrlToHead } from '../lib/Assets';
+
+const latestVersion = '1.0.0';
+const indexHtmlWithServerURL = addServerUrlToIndex(Assets.getText('livechat/index.html'));
+const headHtmlWithServerURL = addServerUrlToHead(Assets.getText('livechat/head.html'));
+const isLatestVersion = (version) => version && version === latestVersion;
+
+WebApp.connectHandlers.use('/livechat', Meteor.bindEnvironment((req, res, next) => {
+ const reqUrl = url.parse(req.url);
+ if (reqUrl.pathname !== '/') {
+ return next();
+ }
+
+ const { version } = req.query;
+ const html = isLatestVersion(version) ? indexHtmlWithServerURL : headHtmlWithServerURL;
+
+ res.setHeader('content-type', 'text/html; charset=utf-8');
+
+ let domainWhiteList = settings.get('Livechat_AllowedDomainsList');
+ if (req.headers.referer && !_.isEmpty(domainWhiteList.trim())) {
+ domainWhiteList = _.map(domainWhiteList.split(','), function(domain) {
+ return domain.trim();
+ });
+
+ const referer = url.parse(req.headers.referer);
+ if (!_.contains(domainWhiteList, referer.host)) {
+ res.setHeader('X-FRAME-OPTIONS', 'DENY');
+ return next();
+ }
+
+ res.setHeader('X-FRAME-OPTIONS', `ALLOW-FROM ${ referer.protocol }//${ referer.host }`);
+ }
+
+ res.write(html);
+ res.end();
+}));
diff --git a/app/livechat/server/methods/addAgent.js b/app/livechat/server/methods/addAgent.js
new file mode 100644
index 000000000000..21b6884ffe64
--- /dev/null
+++ b/app/livechat/server/methods/addAgent.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:addAgent'(username) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addAgent' });
+ }
+
+ return Livechat.addAgent(username);
+ },
+});
diff --git a/app/livechat/server/methods/addManager.js b/app/livechat/server/methods/addManager.js
new file mode 100644
index 000000000000..dc4cb052de05
--- /dev/null
+++ b/app/livechat/server/methods/addManager.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:addManager'(username) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addManager' });
+ }
+
+ return Livechat.addManager(username);
+ },
+});
diff --git a/app/livechat/server/methods/changeLivechatStatus.js b/app/livechat/server/methods/changeLivechatStatus.js
new file mode 100644
index 000000000000..31a302e139b1
--- /dev/null
+++ b/app/livechat/server/methods/changeLivechatStatus.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+import { Users } from '../../../models';
+
+Meteor.methods({
+ 'livechat:changeLivechatStatus'() {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:changeLivechatStatus' });
+ }
+
+ const user = Meteor.user();
+
+ const newStatus = user.statusLivechat === 'available' ? 'not-available' : 'available';
+
+ return Users.setLivechatStatus(user._id, newStatus);
+ },
+});
diff --git a/app/livechat/server/methods/closeByVisitor.js b/app/livechat/server/methods/closeByVisitor.js
new file mode 100644
index 000000000000..c3829f232b2c
--- /dev/null
+++ b/app/livechat/server/methods/closeByVisitor.js
@@ -0,0 +1,19 @@
+import { Meteor } from 'meteor/meteor';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../../settings';
+import { Rooms, LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:closeByVisitor'({ roomId, token }) {
+ const visitor = LivechatVisitors.getVisitorByToken(token);
+
+ const language = (visitor && visitor.language) || settings.get('Language') || 'en';
+
+ return Livechat.closeRoom({
+ visitor,
+ room: Rooms.findOneOpenByRoomIdAndVisitorToken(roomId, token),
+ comment: TAPi18n.__('Closed_by_visitor', { lng: language }),
+ });
+ },
+});
diff --git a/app/livechat/server/methods/closeRoom.js b/app/livechat/server/methods/closeRoom.js
new file mode 100644
index 000000000000..7d49719a0465
--- /dev/null
+++ b/app/livechat/server/methods/closeRoom.js
@@ -0,0 +1,26 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Subscriptions, Rooms } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:closeRoom'(roomId, comment) {
+ const userId = Meteor.userId();
+ if (!userId || !hasPermission(userId, 'close-livechat-room')) {
+ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeRoom' });
+ }
+
+ const user = Meteor.user();
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 });
+ if (!subscription && !hasPermission(userId, 'close-others-livechat-room')) {
+ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeRoom' });
+ }
+
+ return Livechat.closeRoom({
+ user,
+ room: Rooms.findOneById(roomId),
+ comment,
+ });
+ },
+});
diff --git a/app/livechat/server/methods/facebook.js b/app/livechat/server/methods/facebook.js
new file mode 100644
index 000000000000..075165f3bf74
--- /dev/null
+++ b/app/livechat/server/methods/facebook.js
@@ -0,0 +1,65 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { settings } from '../../../settings';
+import OmniChannel from '../lib/OmniChannel';
+
+Meteor.methods({
+ 'livechat:facebook'(options) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addAgent' });
+ }
+
+ try {
+ switch (options.action) {
+ case 'initialState': {
+ return {
+ enabled: settings.get('Livechat_Facebook_Enabled'),
+ hasToken: !!settings.get('Livechat_Facebook_API_Key'),
+ };
+ }
+
+ case 'enable': {
+ const result = OmniChannel.enable();
+
+ if (!result.success) {
+ return result;
+ }
+
+ return settings.updateById('Livechat_Facebook_Enabled', true);
+ }
+
+ case 'disable': {
+ OmniChannel.disable();
+
+ return settings.updateById('Livechat_Facebook_Enabled', false);
+ }
+
+ case 'list-pages': {
+ return OmniChannel.listPages();
+ }
+
+ case 'subscribe': {
+ return OmniChannel.subscribe(options.page);
+ }
+
+ case 'unsubscribe': {
+ return OmniChannel.unsubscribe(options.page);
+ }
+ }
+ } catch (e) {
+ if (e.response && e.response.data && e.response.data.error) {
+ if (e.response.data.error.error) {
+ throw new Meteor.Error(e.response.data.error.error, e.response.data.error.message);
+ }
+ if (e.response.data.error.response) {
+ throw new Meteor.Error('integration-error', e.response.data.error.response.error.message);
+ }
+ if (e.response.data.error.message) {
+ throw new Meteor.Error('integration-error', e.response.data.error.message);
+ }
+ }
+ console.error('Error contacting omni.rocket.chat:', e);
+ throw new Meteor.Error('integration-error', e.error);
+ }
+ },
+});
diff --git a/app/livechat/server/methods/getAgentData.js b/app/livechat/server/methods/getAgentData.js
new file mode 100644
index 000000000000..77bdf81adbde
--- /dev/null
+++ b/app/livechat/server/methods/getAgentData.js
@@ -0,0 +1,23 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Users, Rooms, LivechatVisitors } from '../../../models';
+
+Meteor.methods({
+ 'livechat:getAgentData'({ roomId, token }) {
+ check(roomId, String);
+ check(token, String);
+
+ const room = Rooms.findOneById(roomId);
+ const visitor = LivechatVisitors.getVisitorByToken(token);
+
+ if (!room || room.t !== 'l' || !room.v || room.v.token !== visitor.token) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room');
+ }
+
+ if (!room.servedBy) {
+ return;
+ }
+
+ return Users.getAgentInfo(room.servedBy._id);
+ },
+});
diff --git a/app/livechat/server/methods/getAgentOverviewData.js b/app/livechat/server/methods/getAgentOverviewData.js
new file mode 100644
index 000000000000..e6c325a9195a
--- /dev/null
+++ b/app/livechat/server/methods/getAgentOverviewData.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:getAgentOverviewData'(options) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', {
+ method: 'livechat:getAgentOverviewData',
+ });
+ }
+
+ if (!(options.chartOptions && options.chartOptions.name)) {
+ console.log('Incorrect analytics options');
+ return;
+ }
+
+ return Livechat.Analytics.getAgentOverviewData(options);
+ },
+});
diff --git a/app/livechat/server/methods/getAnalyticsChartData.js b/app/livechat/server/methods/getAnalyticsChartData.js
new file mode 100644
index 000000000000..918ff200d1ad
--- /dev/null
+++ b/app/livechat/server/methods/getAnalyticsChartData.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:getAnalyticsChartData'(options) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', {
+ method: 'livechat:getAnalyticsChartData',
+ });
+ }
+
+ if (!(options.chartOptions && options.chartOptions.name)) {
+ console.log('Incorrect chart options');
+ return;
+ }
+
+ return Livechat.Analytics.getAnalyticsChartData(options);
+ },
+});
diff --git a/app/livechat/server/methods/getAnalyticsOverviewData.js b/app/livechat/server/methods/getAnalyticsOverviewData.js
new file mode 100644
index 000000000000..0958c8085017
--- /dev/null
+++ b/app/livechat/server/methods/getAnalyticsOverviewData.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:getAnalyticsOverviewData'(options) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', {
+ method: 'livechat:getAnalyticsOverviewData',
+ });
+ }
+
+ if (!(options.analyticsOptions && options.analyticsOptions.name)) {
+ console.log('Incorrect analytics options');
+ return;
+ }
+
+ return Livechat.Analytics.getAnalyticsOverviewData(options);
+ },
+});
diff --git a/app/livechat/server/methods/getCustomFields.js b/app/livechat/server/methods/getCustomFields.js
new file mode 100644
index 000000000000..8d2020520481
--- /dev/null
+++ b/app/livechat/server/methods/getCustomFields.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+import { LivechatCustomField } from '../../../models';
+
+Meteor.methods({
+ 'livechat:getCustomFields'() {
+ return LivechatCustomField.find().fetch();
+ },
+});
diff --git a/app/livechat/server/methods/getFirstRoomMessage.js b/app/livechat/server/methods/getFirstRoomMessage.js
new file mode 100644
index 000000000000..2bac3b053b4f
--- /dev/null
+++ b/app/livechat/server/methods/getFirstRoomMessage.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Rooms, Messages } from '../../../models';
+import { hasPermission } from '../../../authorization';
+
+Meteor.methods({
+ 'livechat:getFirstRoomMessage'({ rid }) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:getFirstRoomMessage' });
+ }
+
+ check(rid, String);
+
+ const room = Rooms.findOneById(rid);
+
+ if (!room || room.t !== 'l') {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room');
+ }
+
+ return Messages.findOne({ rid }, { sort: { ts: 1 } });
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/getInitialData.js b/app/livechat/server/methods/getInitialData.js
similarity index 78%
rename from packages/rocketchat-livechat/server/methods/getInitialData.js
rename to app/livechat/server/methods/getInitialData.js
index d2e7ad337f69..4099074a86ad 100644
--- a/packages/rocketchat-livechat/server/methods/getInitialData.js
+++ b/app/livechat/server/methods/getInitialData.js
@@ -1,8 +1,7 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Rooms, Users, LivechatDepartment, LivechatTrigger, LivechatVisitors } from '../../../models';
import _ from 'underscore';
-
-import LivechatVisitors from '../models/LivechatVisitors';
+import { Livechat } from '../lib/Livechat';
Meteor.methods({
'livechat:getInitialData'(visitorToken, departmentId) {
@@ -28,6 +27,7 @@ Meteor.methods({
nameFieldRegistrationForm: null,
emailFieldRegistrationForm: null,
registrationFormMessage: null,
+ showConnecting: false,
};
const options = {
@@ -42,7 +42,7 @@ Meteor.methods({
departmentId: 1,
},
};
- const room = (departmentId) ? RocketChat.models.Rooms.findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options).fetch() : RocketChat.models.Rooms.findOpenByVisitorToken(visitorToken, options).fetch();
+ const room = (departmentId) ? Rooms.findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options).fetch() : Rooms.findOpenByVisitorToken(visitorToken, options).fetch();
if (room && room.length > 0) {
info.room = room[0];
}
@@ -60,7 +60,7 @@ Meteor.methods({
info.visitor = visitor;
}
- const initSettings = RocketChat.Livechat.getInitSettings();
+ const initSettings = Livechat.getInitSettings();
info.title = initSettings.Livechat_title;
info.color = initSettings.Livechat_title_color;
@@ -81,19 +81,20 @@ Meteor.methods({
info.nameFieldRegistrationForm = initSettings.Livechat_name_field_registration_form;
info.emailFieldRegistrationForm = initSettings.Livechat_email_field_registration_form;
info.registrationFormMessage = initSettings.Livechat_registration_form_message;
+ info.showConnecting = initSettings.Livechat_Show_Connecting;
- info.agentData = room && room[0] && room[0].servedBy && RocketChat.models.Users.getAgentInfo(room[0].servedBy._id);
+ info.agentData = room && room[0] && room[0].servedBy && Users.getAgentInfo(room[0].servedBy._id);
- RocketChat.models.LivechatTrigger.findEnabled().forEach((trigger) => {
+ LivechatTrigger.findEnabled().forEach((trigger) => {
info.triggers.push(_.pick(trigger, '_id', 'actions', 'conditions', 'runOnce'));
});
- RocketChat.models.LivechatDepartment.findEnabledWithAgents().forEach((department) => {
+ LivechatDepartment.findEnabledWithAgents().forEach((department) => {
info.departments.push(department);
});
info.allowSwitchingDepartments = initSettings.Livechat_allow_switching_departments;
- info.online = RocketChat.models.Users.findOnlineAgents().count() > 0;
+ info.online = Users.findOnlineAgents().count() > 0;
return info;
},
});
diff --git a/app/livechat/server/methods/getNextAgent.js b/app/livechat/server/methods/getNextAgent.js
new file mode 100644
index 000000000000..6946c01d0039
--- /dev/null
+++ b/app/livechat/server/methods/getNextAgent.js
@@ -0,0 +1,30 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Rooms, Users } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:getNextAgent'({ token, department }) {
+ check(token, String);
+
+ const room = Rooms.findOpenByVisitorToken(token).fetch();
+
+ if (room && room.length > 0) {
+ return;
+ }
+
+ if (!department) {
+ const requireDeparment = Livechat.getRequiredDepartment();
+ if (requireDeparment) {
+ department = requireDeparment._id;
+ }
+ }
+
+ const agent = Livechat.getNextAgent(department);
+ if (!agent) {
+ return;
+ }
+
+ return Users.getAgentInfo(agent.agentId);
+ },
+});
diff --git a/app/livechat/server/methods/loadHistory.js b/app/livechat/server/methods/loadHistory.js
new file mode 100644
index 000000000000..060a5c6b549f
--- /dev/null
+++ b/app/livechat/server/methods/loadHistory.js
@@ -0,0 +1,15 @@
+import { Meteor } from 'meteor/meteor';
+import { loadMessageHistory } from '../../../lib';
+import { LivechatVisitors } from '../../../models';
+
+Meteor.methods({
+ 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) {
+ const visitor = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });
+
+ if (!visitor) {
+ return;
+ }
+
+ return loadMessageHistory({ userId: visitor._id, rid, end, limit, ls });
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/loginByToken.js b/app/livechat/server/methods/loginByToken.js
similarity index 81%
rename from packages/rocketchat-livechat/server/methods/loginByToken.js
rename to app/livechat/server/methods/loginByToken.js
index 1eaae13492bc..18bbad251dd7 100644
--- a/packages/rocketchat-livechat/server/methods/loginByToken.js
+++ b/app/livechat/server/methods/loginByToken.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import LivechatVisitors from '../models/LivechatVisitors';
+import { LivechatVisitors } from '../../../models';
Meteor.methods({
'livechat:loginByToken'(token) {
diff --git a/app/livechat/server/methods/pageVisited.js b/app/livechat/server/methods/pageVisited.js
new file mode 100644
index 000000000000..114108f87fc5
--- /dev/null
+++ b/app/livechat/server/methods/pageVisited.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:pageVisited'(token, room, pageInfo) {
+ Livechat.savePageHistory(token, room, pageInfo);
+ },
+});
diff --git a/app/livechat/server/methods/registerGuest.js b/app/livechat/server/methods/registerGuest.js
new file mode 100644
index 000000000000..46995a549319
--- /dev/null
+++ b/app/livechat/server/methods/registerGuest.js
@@ -0,0 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages, Rooms, LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) {
+ const userId = Livechat.registerGuest.call(this, {
+ token,
+ name,
+ email,
+ department,
+ });
+
+ // update visited page history to not expire
+ Messages.keepHistoryForToken(token);
+
+ const visitor = LivechatVisitors.getVisitorByToken(token, {
+ fields: {
+ token: 1,
+ name: 1,
+ username: 1,
+ visitorEmails: 1,
+ department: 1,
+ },
+ });
+
+ // If it's updating an existing visitor, it must also update the roomInfo
+ const cursor = Rooms.findOpenByVisitorToken(token);
+ cursor.forEach((room) => {
+ Livechat.saveRoomInfo(room, visitor);
+ });
+
+ if (customFields && customFields instanceof Array) {
+ customFields.forEach((customField) => {
+ if (typeof customField !== 'object') {
+ return;
+ }
+
+ if (!customField.scope || customField.scope !== 'room') {
+ const { key, value, overwrite } = customField;
+ LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite);
+ }
+ });
+ }
+
+ return {
+ userId,
+ visitor,
+ };
+ },
+});
diff --git a/app/livechat/server/methods/removeAgent.js b/app/livechat/server/methods/removeAgent.js
new file mode 100644
index 000000000000..e6bc9fcf0cc2
--- /dev/null
+++ b/app/livechat/server/methods/removeAgent.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:removeAgent'(username) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeAgent' });
+ }
+
+ return Livechat.removeAgent(username);
+ },
+});
diff --git a/app/livechat/server/methods/removeCustomField.js b/app/livechat/server/methods/removeCustomField.js
new file mode 100644
index 000000000000..a53efa59ee0c
--- /dev/null
+++ b/app/livechat/server/methods/removeCustomField.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { hasPermission } from '../../../authorization';
+import { LivechatCustomField } from '../../../models';
+
+Meteor.methods({
+ 'livechat:removeCustomField'(_id) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeCustomField' });
+ }
+
+ check(_id, String);
+
+ const customField = LivechatCustomField.findOneById(_id, { fields: { _id: 1 } });
+
+ if (!customField) {
+ throw new Meteor.Error('error-invalid-custom-field', 'Custom field not found', { method: 'livechat:removeCustomField' });
+ }
+
+ return LivechatCustomField.removeById(_id);
+ },
+});
diff --git a/app/livechat/server/methods/removeDepartment.js b/app/livechat/server/methods/removeDepartment.js
new file mode 100644
index 000000000000..d08fee853401
--- /dev/null
+++ b/app/livechat/server/methods/removeDepartment.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:removeDepartment'(_id) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeDepartment' });
+ }
+
+ return Livechat.removeDepartment(_id);
+ },
+});
diff --git a/app/livechat/server/methods/removeManager.js b/app/livechat/server/methods/removeManager.js
new file mode 100644
index 000000000000..9fa635e7d6bd
--- /dev/null
+++ b/app/livechat/server/methods/removeManager.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:removeManager'(username) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeManager' });
+ }
+
+ return Livechat.removeManager(username);
+ },
+});
diff --git a/app/livechat/server/methods/removeRoom.js b/app/livechat/server/methods/removeRoom.js
new file mode 100644
index 000000000000..13ef3aa62d02
--- /dev/null
+++ b/app/livechat/server/methods/removeRoom.js
@@ -0,0 +1,35 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Rooms, Messages, Subscriptions } from '../../../models';
+
+Meteor.methods({
+ 'livechat:removeRoom'(rid) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'remove-closed-livechat-rooms')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeRoom' });
+ }
+
+ const room = Rooms.findOneById(rid);
+
+ if (!room) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', {
+ method: 'livechat:removeRoom',
+ });
+ }
+
+ if (room.t !== 'l') {
+ throw new Meteor.Error('error-this-is-not-a-livechat-room', 'This is not a Livechat room', {
+ method: 'livechat:removeRoom',
+ });
+ }
+
+ if (room.open) {
+ throw new Meteor.Error('error-room-is-not-closed', 'Room is not closed', {
+ method: 'livechat:removeRoom',
+ });
+ }
+
+ Messages.removeByRoomId(rid);
+ Subscriptions.removeByRoomId(rid);
+ return Rooms.removeById(rid);
+ },
+});
diff --git a/app/livechat/server/methods/removeTrigger.js b/app/livechat/server/methods/removeTrigger.js
new file mode 100644
index 000000000000..164f0f56b4fc
--- /dev/null
+++ b/app/livechat/server/methods/removeTrigger.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { hasPermission } from '../../../authorization';
+import { LivechatTrigger } from '../../../models';
+
+Meteor.methods({
+ 'livechat:removeTrigger'(triggerId) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeTrigger' });
+ }
+
+ check(triggerId, String);
+
+ return LivechatTrigger.removeById(triggerId);
+ },
+});
diff --git a/app/livechat/server/methods/returnAsInquiry.js b/app/livechat/server/methods/returnAsInquiry.js
new file mode 100644
index 000000000000..2a7c5971bb4e
--- /dev/null
+++ b/app/livechat/server/methods/returnAsInquiry.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:returnAsInquiry'(rid, departmentId) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveDepartment' });
+ }
+
+ return Livechat.returnRoomAsInquiry(rid, departmentId);
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/saveAppearance.js b/app/livechat/server/methods/saveAppearance.js
similarity index 79%
rename from packages/rocketchat-livechat/server/methods/saveAppearance.js
rename to app/livechat/server/methods/saveAppearance.js
index 368863bf2bf8..18ac479061a7 100644
--- a/packages/rocketchat-livechat/server/methods/saveAppearance.js
+++ b/app/livechat/server/methods/saveAppearance.js
@@ -1,9 +1,10 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { hasPermission } from '../../../authorization';
+import { settings as rcSettings } from '../../../settings';
Meteor.methods({
'livechat:saveAppearance'(settings) {
- if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveAppearance' });
}
@@ -32,7 +33,7 @@ Meteor.methods({
}
settings.forEach((setting) => {
- RocketChat.settings.updateById(setting._id, setting.value);
+ rcSettings.updateById(setting._id, setting.value);
});
return;
diff --git a/app/livechat/server/methods/saveCustomField.js b/app/livechat/server/methods/saveCustomField.js
new file mode 100644
index 000000000000..8a41b25e1dc1
--- /dev/null
+++ b/app/livechat/server/methods/saveCustomField.js
@@ -0,0 +1,31 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { hasPermission } from '../../../authorization';
+import { LivechatCustomField } from '../../../models';
+
+Meteor.methods({
+ 'livechat:saveCustomField'(_id, customFieldData) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveCustomField' });
+ }
+
+ if (_id) {
+ check(_id, String);
+ }
+
+ check(customFieldData, Match.ObjectIncluding({ field: String, label: String, scope: String, visibility: String }));
+
+ if (!/^[0-9a-zA-Z-_]+$/.test(customFieldData.field)) {
+ throw new Meteor.Error('error-invalid-custom-field-nmae', 'Invalid custom field name. Use only letters, numbers, hyphens and underscores.', { method: 'livechat:saveCustomField' });
+ }
+
+ if (_id) {
+ const customField = LivechatCustomField.findOneById(_id);
+ if (!customField) {
+ throw new Meteor.Error('error-invalid-custom-field', 'Custom Field Not found', { method: 'livechat:saveCustomField' });
+ }
+ }
+
+ return LivechatCustomField.createOrUpdateCustomField(_id, customFieldData.field, customFieldData.label, customFieldData.scope, customFieldData.visibility);
+ },
+});
diff --git a/app/livechat/server/methods/saveDepartment.js b/app/livechat/server/methods/saveDepartment.js
new file mode 100644
index 000000000000..067f49592aef
--- /dev/null
+++ b/app/livechat/server/methods/saveDepartment.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:saveDepartment'(_id, departmentData, departmentAgents) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveDepartment' });
+ }
+
+ return Livechat.saveDepartment(_id, departmentData, departmentAgents);
+ },
+});
diff --git a/app/livechat/server/methods/saveInfo.js b/app/livechat/server/methods/saveInfo.js
new file mode 100644
index 000000000000..4318fd163c7d
--- /dev/null
+++ b/app/livechat/server/methods/saveInfo.js
@@ -0,0 +1,45 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { hasPermission } from '../../../authorization';
+import { Rooms } from '../../../models';
+import { callbacks } from '../../../callbacks';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:saveInfo'(guestData, roomData) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveInfo' });
+ }
+
+ check(guestData, Match.ObjectIncluding({
+ _id: String,
+ name: Match.Optional(String),
+ email: Match.Optional(String),
+ phone: Match.Optional(String),
+ }));
+
+ check(roomData, Match.ObjectIncluding({
+ _id: String,
+ topic: Match.Optional(String),
+ tags: Match.Optional(String),
+ }));
+
+ const room = Rooms.findOneById(roomData._id, { fields: { t: 1, servedBy: 1 } });
+
+ if (room == null || room.t !== 'l') {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:saveInfo' });
+ }
+
+ if ((!room.servedBy || room.servedBy._id !== Meteor.userId()) && !hasPermission(Meteor.userId(), 'save-others-livechat-room-info')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveInfo' });
+ }
+
+ const ret = Livechat.saveGuest(guestData) && Livechat.saveRoomInfo(roomData, guestData);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat.saveInfo', Rooms.findOneById(roomData._id));
+ });
+
+ return ret;
+ },
+});
diff --git a/app/livechat/server/methods/saveIntegration.js b/app/livechat/server/methods/saveIntegration.js
new file mode 100644
index 000000000000..b4097b76e534
--- /dev/null
+++ b/app/livechat/server/methods/saveIntegration.js
@@ -0,0 +1,38 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { settings } from '../../../settings';
+import s from 'underscore.string';
+
+Meteor.methods({
+ 'livechat:saveIntegration'(values) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveIntegration' });
+ }
+
+ if (typeof values.Livechat_webhookUrl !== 'undefined') {
+ settings.updateById('Livechat_webhookUrl', s.trim(values.Livechat_webhookUrl));
+ }
+
+ if (typeof values.Livechat_secret_token !== 'undefined') {
+ settings.updateById('Livechat_secret_token', s.trim(values.Livechat_secret_token));
+ }
+
+ if (typeof values.Livechat_webhook_on_close !== 'undefined') {
+ settings.updateById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close);
+ }
+
+ if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') {
+ settings.updateById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg);
+ }
+
+ if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') {
+ settings.updateById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message);
+ }
+
+ if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') {
+ settings.updateById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message);
+ }
+
+ return;
+ },
+});
diff --git a/app/livechat/server/methods/saveOfficeHours.js b/app/livechat/server/methods/saveOfficeHours.js
new file mode 100644
index 000000000000..874777929071
--- /dev/null
+++ b/app/livechat/server/methods/saveOfficeHours.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+import { LivechatOfficeHour } from '../../../models';
+
+Meteor.methods({
+ 'livechat:saveOfficeHours'(day, start, finish, open) {
+ LivechatOfficeHour.updateHours(day, start, finish, open);
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/saveSurveyFeedback.js b/app/livechat/server/methods/saveSurveyFeedback.js
similarity index 78%
rename from packages/rocketchat-livechat/server/methods/saveSurveyFeedback.js
rename to app/livechat/server/methods/saveSurveyFeedback.js
index 4636a42a7a4b..5b5a571178dc 100644
--- a/packages/rocketchat-livechat/server/methods/saveSurveyFeedback.js
+++ b/app/livechat/server/methods/saveSurveyFeedback.js
@@ -1,7 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import LivechatVisitors from '../models/LivechatVisitors';
+import { Rooms, LivechatVisitors } from '../../../models';
import _ from 'underscore';
Meteor.methods({
@@ -11,7 +10,7 @@ Meteor.methods({
check(formData, [Match.ObjectIncluding({ name: String, value: String })]);
const visitor = LivechatVisitors.getVisitorByToken(visitorToken);
- const room = RocketChat.models.Rooms.findOneById(visitorRoom);
+ const room = Rooms.findOneById(visitorRoom);
if (visitor !== undefined && room !== undefined && room.v !== undefined && room.v.token === visitor.token) {
const updateData = {};
@@ -23,7 +22,7 @@ Meteor.methods({
}
}
if (!_.isEmpty(updateData)) {
- return RocketChat.models.Rooms.updateSurveyFeedbackById(room._id, updateData);
+ return Rooms.updateSurveyFeedbackById(room._id, updateData);
}
}
},
diff --git a/app/livechat/server/methods/saveTrigger.js b/app/livechat/server/methods/saveTrigger.js
new file mode 100644
index 000000000000..8452a42ea9d4
--- /dev/null
+++ b/app/livechat/server/methods/saveTrigger.js
@@ -0,0 +1,28 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { hasPermission } from '../../../authorization';
+import { LivechatTrigger } from '../../../models';
+
+Meteor.methods({
+ 'livechat:saveTrigger'(trigger) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveTrigger' });
+ }
+
+ check(trigger, {
+ _id: Match.Maybe(String),
+ name: String,
+ description: String,
+ enabled: Boolean,
+ runOnce: Boolean,
+ conditions: Array,
+ actions: Array,
+ });
+
+ if (trigger._id) {
+ return LivechatTrigger.updateById(trigger._id, trigger);
+ } else {
+ return LivechatTrigger.insert(trigger);
+ }
+ },
+});
diff --git a/app/livechat/server/methods/searchAgent.js b/app/livechat/server/methods/searchAgent.js
new file mode 100644
index 000000000000..18607f7110a2
--- /dev/null
+++ b/app/livechat/server/methods/searchAgent.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Users } from '../../../models';
+import _ from 'underscore';
+
+Meteor.methods({
+ 'livechat:searchAgent'(username) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:searchAgent' });
+ }
+
+ if (!username || !_.isString(username)) {
+ throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'livechat:searchAgent' });
+ }
+
+ const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:searchAgent' });
+ }
+
+ return user;
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/sendFileLivechatMessage.js b/app/livechat/server/methods/sendFileLivechatMessage.js
similarity index 86%
rename from packages/rocketchat-livechat/server/methods/sendFileLivechatMessage.js
rename to app/livechat/server/methods/sendFileLivechatMessage.js
index 187b4e5c0da3..9095d2971b4c 100644
--- a/packages/rocketchat-livechat/server/methods/sendFileLivechatMessage.js
+++ b/app/livechat/server/methods/sendFileLivechatMessage.js
@@ -1,9 +1,8 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import { FileUpload } from 'meteor/rocketchat:file-upload';
-import LivechatVisitors from '../models/LivechatVisitors';
+import { Rooms, LivechatVisitors } from '../../../models';
+import { FileUpload } from '../../../file-upload';
Meteor.methods({
async 'sendFileLivechatMessage'(roomId, visitorToken, file, msgData = {}) {
@@ -13,7 +12,7 @@ Meteor.methods({
return false;
}
- const room = RocketChat.models.Rooms.findOneOpenByRoomIdAndVisitorToken(roomId, visitorToken);
+ const room = Rooms.findOneOpenByRoomIdAndVisitorToken(roomId, visitorToken);
if (!room) {
return false;
diff --git a/packages/rocketchat-livechat/server/methods/sendMessageLivechat.js b/app/livechat/server/methods/sendMessageLivechat.js
similarity index 81%
rename from packages/rocketchat-livechat/server/methods/sendMessageLivechat.js
rename to app/livechat/server/methods/sendMessageLivechat.js
index ec4d05abec33..e3b7e7b56721 100644
--- a/packages/rocketchat-livechat/server/methods/sendMessageLivechat.js
+++ b/app/livechat/server/methods/sendMessageLivechat.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import LivechatVisitors from '../models/LivechatVisitors';
+import { LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
Meteor.methods({
sendMessageLivechat({ token, _id, rid, msg, attachments }, agent) {
@@ -28,7 +28,7 @@ Meteor.methods({
throw new Meteor.Error('invalid-token');
}
- return RocketChat.Livechat.sendMessage({
+ return Livechat.sendMessage({
guest,
message: {
_id,
diff --git a/packages/rocketchat-livechat/server/methods/sendOfflineMessage.js b/app/livechat/server/methods/sendOfflineMessage.js
similarity index 79%
rename from packages/rocketchat-livechat/server/methods/sendOfflineMessage.js
rename to app/livechat/server/methods/sendOfflineMessage.js
index 2ace9354065d..d9dde597fedb 100644
--- a/packages/rocketchat-livechat/server/methods/sendOfflineMessage.js
+++ b/app/livechat/server/methods/sendOfflineMessage.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
+import { Livechat } from '../lib/Livechat';
Meteor.methods({
'livechat:sendOfflineMessage'(data) {
@@ -11,7 +11,7 @@ Meteor.methods({
message: String,
});
- return RocketChat.Livechat.sendOfflineMessage(data);
+ return Livechat.sendOfflineMessage(data);
},
});
diff --git a/packages/rocketchat-livechat/server/methods/sendTranscript.js b/app/livechat/server/methods/sendTranscript.js
similarity index 76%
rename from packages/rocketchat-livechat/server/methods/sendTranscript.js
rename to app/livechat/server/methods/sendTranscript.js
index f6b2eb05f532..2ba0aa49343d 100644
--- a/packages/rocketchat-livechat/server/methods/sendTranscript.js
+++ b/app/livechat/server/methods/sendTranscript.js
@@ -1,14 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
+import { Livechat } from '../lib/Livechat';
Meteor.methods({
'livechat:sendTranscript'(token, rid, email) {
check(rid, String);
check(email, String);
- return RocketChat.Livechat.sendTranscript({ token, rid, email });
+ return Livechat.sendTranscript({ token, rid, email });
},
});
diff --git a/app/livechat/server/methods/setCustomField.js b/app/livechat/server/methods/setCustomField.js
new file mode 100644
index 000000000000..1e840732e3c8
--- /dev/null
+++ b/app/livechat/server/methods/setCustomField.js
@@ -0,0 +1,18 @@
+import { Meteor } from 'meteor/meteor';
+import { Rooms, LivechatVisitors, LivechatCustomField } from '../../../models';
+
+Meteor.methods({
+ 'livechat:setCustomField'(token, key, value, overwrite = true) {
+ const customField = LivechatCustomField.findOneById(key);
+ if (customField) {
+ if (customField.scope === 'room') {
+ return Rooms.updateLivechatDataByToken(token, key, value, overwrite);
+ } else {
+ // Save in user
+ return LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite);
+ }
+ }
+
+ return true;
+ },
+});
diff --git a/app/livechat/server/methods/setDepartmentForVisitor.js b/app/livechat/server/methods/setDepartmentForVisitor.js
new file mode 100644
index 000000000000..3ba3690b4316
--- /dev/null
+++ b/app/livechat/server/methods/setDepartmentForVisitor.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Rooms, Messages, LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:setDepartmentForVisitor'({ roomId, visitorToken, departmentId } = {}) {
+ check(roomId, String);
+ check(visitorToken, String);
+ check(departmentId, String);
+
+ const room = Rooms.findOneById(roomId);
+ const visitor = LivechatVisitors.getVisitorByToken(visitorToken);
+
+ if (!room || room.t !== 'l' || !room.v || room.v.token !== visitor.token) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room');
+ }
+
+ // update visited page history to not expire
+ Messages.keepHistoryForToken(visitorToken);
+
+ const transferData = {
+ roomId,
+ departmentId,
+ };
+
+ return Livechat.transfer(room, visitor, transferData);
+ },
+});
diff --git a/app/livechat/server/methods/setUpConnection.js b/app/livechat/server/methods/setUpConnection.js
new file mode 100644
index 000000000000..bb040ff62131
--- /dev/null
+++ b/app/livechat/server/methods/setUpConnection.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:setUpConnection'(data) {
+
+ check(data, {
+ token: String,
+ });
+
+ const { token } = data;
+
+ if (!this.connection.livechatToken) {
+ this.connection.livechatToken = token;
+ this.connection.onClose(() => {
+ Livechat.notifyGuestStatusChanged(token, 'offline');
+ });
+ }
+ },
+});
diff --git a/app/livechat/server/methods/startFileUploadRoom.js b/app/livechat/server/methods/startFileUploadRoom.js
new file mode 100644
index 000000000000..6d70dbf15843
--- /dev/null
+++ b/app/livechat/server/methods/startFileUploadRoom.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:startFileUploadRoom'(roomId, token) {
+ const guest = LivechatVisitors.getVisitorByToken(token);
+
+ const message = {
+ _id: Random.id(),
+ rid: roomId || Random.id(),
+ msg: '',
+ ts: new Date(),
+ token: guest.token,
+ };
+
+ return Livechat.getRoom(guest, message);
+ },
+});
diff --git a/app/livechat/server/methods/startVideoCall.js b/app/livechat/server/methods/startVideoCall.js
new file mode 100644
index 000000000000..45c636ce9126
--- /dev/null
+++ b/app/livechat/server/methods/startVideoCall.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { Messages } from '../../../models';
+import { settings } from '../../../settings';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:startVideoCall'(roomId) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeByVisitor' });
+ }
+
+ const guest = Meteor.user();
+
+ const message = {
+ _id: Random.id(),
+ rid: roomId || Random.id(),
+ msg: '',
+ ts: new Date(),
+ };
+
+ const { room } = Livechat.getRoom(guest, message, { jitsiTimeout: new Date(Date.now() + 3600 * 1000) });
+ message.rid = room._id;
+
+ Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, {
+ actionLinks: [
+ { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' },
+ { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' },
+ ],
+ });
+
+ return {
+ roomId: room._id,
+ domain: settings.get('Jitsi_Domain'),
+ jitsiRoom: settings.get('Jitsi_URL_Room_Prefix') + settings.get('uniqueID') + roomId,
+ };
+ },
+});
+
diff --git a/app/livechat/server/methods/takeInquiry.js b/app/livechat/server/methods/takeInquiry.js
new file mode 100644
index 000000000000..0ad662b1a87f
--- /dev/null
+++ b/app/livechat/server/methods/takeInquiry.js
@@ -0,0 +1,76 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Users, Rooms, Subscriptions, Messages } from '../../../models';
+import { LivechatInquiry } from '../../lib/LivechatInquiry';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:takeInquiry'(inquiryId) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:takeInquiry' });
+ }
+
+ const inquiry = LivechatInquiry.findOneById(inquiryId);
+
+ if (!inquiry || inquiry.status === 'taken') {
+ throw new Meteor.Error('error-not-allowed', 'Inquiry already taken', { method: 'livechat:takeInquiry' });
+ }
+
+ const user = Users.findOneById(Meteor.userId());
+
+ const agent = {
+ agentId: user._id,
+ username: user.username,
+ ts: new Date(),
+ };
+
+ // add subscription
+ const subscriptionData = {
+ rid: inquiry.rid,
+ name: inquiry.name,
+ alert: true,
+ open: true,
+ unread: 1,
+ userMentions: 1,
+ groupMentions: 0,
+ u: {
+ _id: agent.agentId,
+ username: agent.username,
+ },
+ t: 'l',
+ desktopNotifications: 'all',
+ mobilePushNotifications: 'all',
+ emailNotifications: 'all',
+ };
+
+ Subscriptions.insert(subscriptionData);
+ Rooms.incUsersCountById(inquiry.rid);
+
+ // update room
+ const room = Rooms.findOneById(inquiry.rid);
+
+ Rooms.changeAgentByRoomId(inquiry.rid, agent);
+
+ room.servedBy = {
+ _id: agent.agentId,
+ username: agent.username,
+ ts: agent.ts,
+ };
+
+ // mark inquiry as taken
+ LivechatInquiry.takeInquiry(inquiry._id);
+
+ // remove sending message from guest widget
+ // dont check if setting is true, because if settingwas switched off inbetween guest entered pool,
+ // and inquiry being taken, message would not be switched off.
+ Messages.createCommandWithRoomIdAndUser('connected', room._id, user);
+
+ Livechat.stream.emit(room._id, {
+ type: 'agentData',
+ data: Users.getAgentInfo(agent.agentId),
+ });
+
+ // return inquiry (for redirecting agent to the room route)
+ return inquiry;
+ },
+});
diff --git a/app/livechat/server/methods/transfer.js b/app/livechat/server/methods/transfer.js
new file mode 100644
index 000000000000..1b08e5a64066
--- /dev/null
+++ b/app/livechat/server/methods/transfer.js
@@ -0,0 +1,33 @@
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { hasPermission, hasRole } from '../../../authorization';
+import { Rooms, Subscriptions, LivechatVisitors } from '../../../models';
+import { Livechat } from '../lib/Livechat';
+
+Meteor.methods({
+ 'livechat:transfer'(transferData) {
+ if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' });
+ }
+
+ check(transferData, {
+ roomId: String,
+ userId: Match.Optional(String),
+ departmentId: Match.Optional(String),
+ });
+
+ const room = Rooms.findOneById(transferData.roomId);
+ if (!room) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:transfer' });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, Meteor.userId(), { fields: { _id: 1 } });
+ if (!subscription && !hasRole(Meteor.userId(), 'livechat-manager')) {
+ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:transfer' });
+ }
+
+ const guest = LivechatVisitors.findOneById(room.v && room.v._id);
+
+ return Livechat.transfer(room, guest, transferData);
+ },
+});
diff --git a/packages/rocketchat-livechat/server/methods/webhookTest.js b/app/livechat/server/methods/webhookTest.js
similarity index 87%
rename from packages/rocketchat-livechat/server/methods/webhookTest.js
rename to app/livechat/server/methods/webhookTest.js
index eb8221d51fd3..7f88b722c928 100644
--- a/packages/rocketchat-livechat/server/methods/webhookTest.js
+++ b/app/livechat/server/methods/webhookTest.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../settings';
import { HTTP } from 'meteor/http';
const postCatchError = Meteor.wrapAsync(function(url, options, resolve) {
@@ -65,12 +65,12 @@ Meteor.methods({
const options = {
headers: {
- 'X-RocketChat-Livechat-Token': RocketChat.settings.get('Livechat_secret_token'),
+ 'X-RocketChat-Livechat-Token': settings.get('Livechat_secret_token'),
},
data: sampleData,
};
- const response = postCatchError(RocketChat.settings.get('Livechat_webhookUrl'), options);
+ const response = postCatchError(settings.get('Livechat_webhookUrl'), options);
console.log('response ->', response);
diff --git a/app/livechat/server/permissions.js b/app/livechat/server/permissions.js
new file mode 100644
index 000000000000..d8e9153549dd
--- /dev/null
+++ b/app/livechat/server/permissions.js
@@ -0,0 +1,26 @@
+import { Meteor } from 'meteor/meteor';
+import { Roles, Permissions } from '../../models';
+import _ from 'underscore';
+
+Meteor.startup(() => {
+ const roles = _.pluck(Roles.find().fetch(), 'name');
+ if (roles.indexOf('livechat-agent') === -1) {
+ Roles.createOrUpdate('livechat-agent');
+ }
+ if (roles.indexOf('livechat-manager') === -1) {
+ Roles.createOrUpdate('livechat-manager');
+ }
+ if (roles.indexOf('livechat-guest') === -1) {
+ Roles.createOrUpdate('livechat-guest');
+ }
+ if (Permissions) {
+ Permissions.createOrUpdate('view-l-room', ['livechat-agent', 'livechat-manager', 'admin']);
+ Permissions.createOrUpdate('view-livechat-manager', ['livechat-manager', 'admin']);
+ Permissions.createOrUpdate('view-livechat-rooms', ['livechat-manager', 'admin']);
+ Permissions.createOrUpdate('close-livechat-room', ['livechat-agent', 'livechat-manager', 'admin']);
+ Permissions.createOrUpdate('close-others-livechat-room', ['livechat-manager', 'admin']);
+ Permissions.createOrUpdate('save-others-livechat-room-info', ['livechat-manager']);
+ Permissions.createOrUpdate('remove-closed-livechat-rooms', ['livechat-manager', 'admin']);
+ Permissions.createOrUpdate('view-livechat-analytics', ['livechat-manager', 'admin']);
+ }
+});
diff --git a/app/livechat/server/publications/customFields.js b/app/livechat/server/publications/customFields.js
new file mode 100644
index 000000000000..95283a4f4f36
--- /dev/null
+++ b/app/livechat/server/publications/customFields.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatCustomField } from '../../../models';
+import s from 'underscore.string';
+
+Meteor.publish('livechat:customFields', function(_id) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:customFields' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:customFields' }));
+ }
+
+ if (s.trim(_id)) {
+ return LivechatCustomField.find({ _id });
+ }
+
+ return LivechatCustomField.find();
+
+});
diff --git a/app/livechat/server/publications/departmentAgents.js b/app/livechat/server/publications/departmentAgents.js
new file mode 100644
index 000000000000..f55b2b214916
--- /dev/null
+++ b/app/livechat/server/publications/departmentAgents.js
@@ -0,0 +1,15 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatDepartmentAgents } from '../../../models';
+
+Meteor.publish('livechat:departmentAgents', function(departmentId) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departmentAgents' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-livechat-rooms')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departmentAgents' }));
+ }
+
+ return LivechatDepartmentAgents.find({ departmentId });
+});
diff --git a/app/livechat/server/publications/externalMessages.js b/app/livechat/server/publications/externalMessages.js
new file mode 100644
index 000000000000..106962b81da2
--- /dev/null
+++ b/app/livechat/server/publications/externalMessages.js
@@ -0,0 +1,6 @@
+import { Meteor } from 'meteor/meteor';
+import { LivechatExternalMessage } from '../../lib/LivechatExternalMessage';
+
+Meteor.publish('livechat:externalMessages', function(roomId) {
+ return LivechatExternalMessage.findByRoomId(roomId);
+});
diff --git a/packages/rocketchat-livechat/server/publications/livechatAgents.js b/app/livechat/server/publications/livechatAgents.js
similarity index 75%
rename from packages/rocketchat-livechat/server/publications/livechatAgents.js
rename to app/livechat/server/publications/livechatAgents.js
index c5d333039c1f..874b35af4e18 100644
--- a/packages/rocketchat-livechat/server/publications/livechatAgents.js
+++ b/app/livechat/server/publications/livechatAgents.js
@@ -1,18 +1,18 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { hasPermission, getUsersInRole } from '../../../authorization';
Meteor.publish('livechat:agents', function() {
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
}
- if (!RocketChat.authz.hasPermission(this.userId, 'view-l-room')) {
+ if (!hasPermission(this.userId, 'view-l-room')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
}
const self = this;
- const handle = RocketChat.authz.getUsersInRole('livechat-agent').observeChanges({
+ const handle = getUsersInRole('livechat-agent').observeChanges({
added(id, fields) {
self.added('agentUsers', id, fields);
},
diff --git a/app/livechat/server/publications/livechatAppearance.js b/app/livechat/server/publications/livechatAppearance.js
new file mode 100644
index 000000000000..1d304d1696b5
--- /dev/null
+++ b/app/livechat/server/publications/livechatAppearance.js
@@ -0,0 +1,55 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Settings } from '../../../models';
+
+Meteor.publish('livechat:appearance', function() {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:appearance' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:appearance' }));
+ }
+
+ const query = {
+ _id: {
+ $in: [
+ 'Livechat_title',
+ 'Livechat_title_color',
+ 'Livechat_show_agent_email',
+ 'Livechat_display_offline_form',
+ 'Livechat_offline_form_unavailable',
+ 'Livechat_offline_message',
+ 'Livechat_offline_success_message',
+ 'Livechat_offline_title',
+ 'Livechat_offline_title_color',
+ 'Livechat_offline_email',
+ 'Livechat_conversation_finished_message',
+ 'Livechat_registration_form',
+ 'Livechat_name_field_registration_form',
+ 'Livechat_email_field_registration_form',
+ 'Livechat_registration_form_message',
+ ],
+ },
+ };
+
+ const self = this;
+
+ const handle = Settings.find(query).observeChanges({
+ added(id, fields) {
+ self.added('livechatAppearance', id, fields);
+ },
+ changed(id, fields) {
+ self.changed('livechatAppearance', id, fields);
+ },
+ removed(id) {
+ self.removed('livechatAppearance', id);
+ },
+ });
+
+ this.ready();
+
+ this.onStop(() => {
+ handle.stop();
+ });
+});
diff --git a/app/livechat/server/publications/livechatDepartments.js b/app/livechat/server/publications/livechatDepartments.js
new file mode 100644
index 000000000000..c89de01ce749
--- /dev/null
+++ b/app/livechat/server/publications/livechatDepartments.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatDepartment } from '../../../models';
+
+Meteor.publish('livechat:departments', function(_id) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
+ }
+
+ if (_id !== undefined) {
+ return LivechatDepartment.findByDepartmentId(_id);
+ } else {
+ return LivechatDepartment.find();
+ }
+
+});
diff --git a/app/livechat/server/publications/livechatInquiries.js b/app/livechat/server/publications/livechatInquiries.js
new file mode 100644
index 000000000000..356e1364f7e6
--- /dev/null
+++ b/app/livechat/server/publications/livechatInquiries.js
@@ -0,0 +1,37 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatInquiry } from '../../lib/LivechatInquiry';
+
+Meteor.publish('livechat:inquiry', function(_id) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:inquiry' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:inquiry' }));
+ }
+
+ const publication = this;
+
+ const cursorHandle = LivechatInquiry.find({
+ agents: this.userId,
+ status: 'open',
+ ...(_id && { _id }),
+ }).observeChanges({
+ added(_id, record) {
+ return publication.added('rocketchat_livechat_inquiry', _id, record);
+ },
+ changed(_id, record) {
+ return publication.changed('rocketchat_livechat_inquiry', _id, record);
+ },
+ removed(_id) {
+ return publication.removed('rocketchat_livechat_inquiry', _id);
+ },
+ });
+
+ this.ready();
+ return this.onStop(function() {
+ return cursorHandle.stop();
+ });
+
+});
diff --git a/app/livechat/server/publications/livechatIntegration.js b/app/livechat/server/publications/livechatIntegration.js
new file mode 100644
index 000000000000..d9d80fd3caf7
--- /dev/null
+++ b/app/livechat/server/publications/livechatIntegration.js
@@ -0,0 +1,33 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Settings } from '../../../models';
+
+Meteor.publish('livechat:integration', function() {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:integration' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:integration' }));
+ }
+
+ const self = this;
+
+ const handle = Settings.findByIds(['Livechat_webhookUrl', 'Livechat_secret_token', 'Livechat_webhook_on_close', 'Livechat_webhook_on_offline_msg', 'Livechat_webhook_on_visitor_message', 'Livechat_webhook_on_agent_message']).observeChanges({
+ added(id, fields) {
+ self.added('livechatIntegration', id, fields);
+ },
+ changed(id, fields) {
+ self.changed('livechatIntegration', id, fields);
+ },
+ removed(id) {
+ self.removed('livechatIntegration', id);
+ },
+ });
+
+ self.ready();
+
+ self.onStop(function() {
+ handle.stop();
+ });
+});
diff --git a/packages/rocketchat-livechat/server/publications/livechatManagers.js b/app/livechat/server/publications/livechatManagers.js
similarity index 75%
rename from packages/rocketchat-livechat/server/publications/livechatManagers.js
rename to app/livechat/server/publications/livechatManagers.js
index 6f9aa3dec657..eefcf21f1d56 100644
--- a/packages/rocketchat-livechat/server/publications/livechatManagers.js
+++ b/app/livechat/server/publications/livechatManagers.js
@@ -1,18 +1,18 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { hasPermission, getUsersInRole } from '../../../authorization';
Meteor.publish('livechat:managers', function() {
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:managers' }));
}
- if (!RocketChat.authz.hasPermission(this.userId, 'view-livechat-rooms')) {
+ if (!hasPermission(this.userId, 'view-livechat-rooms')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:managers' }));
}
const self = this;
- const handle = RocketChat.authz.getUsersInRole('livechat-manager').observeChanges({
+ const handle = getUsersInRole('livechat-manager').observeChanges({
added(id, fields) {
self.added('managerUsers', id, fields);
},
diff --git a/packages/rocketchat-livechat/server/publications/livechatMonitoring.js b/app/livechat/server/publications/livechatMonitoring.js
similarity index 77%
rename from packages/rocketchat-livechat/server/publications/livechatMonitoring.js
rename to app/livechat/server/publications/livechatMonitoring.js
index 99707fc23324..7257184e4fbd 100644
--- a/packages/rocketchat-livechat/server/publications/livechatMonitoring.js
+++ b/app/livechat/server/publications/livechatMonitoring.js
@@ -1,13 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { hasPermission } from '../../../authorization';
+import { Rooms } from '../../../models';
Meteor.publish('livechat:monitoring', function(date) {
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:monitoring' }));
}
- if (!RocketChat.authz.hasPermission(this.userId, 'view-livechat-manager')) {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:monitoring' }));
}
@@ -21,7 +22,7 @@ Meteor.publish('livechat:monitoring', function(date) {
const self = this;
- const handle = RocketChat.models.Rooms.getAnalyticsMetricsBetweenDate('l', date).observeChanges({
+ const handle = Rooms.getAnalyticsMetricsBetweenDate('l', date).observeChanges({
added(id, fields) {
self.added('livechatMonitoring', id, fields);
},
diff --git a/app/livechat/server/publications/livechatOfficeHours.js b/app/livechat/server/publications/livechatOfficeHours.js
new file mode 100644
index 000000000000..6962b7898d4e
--- /dev/null
+++ b/app/livechat/server/publications/livechatOfficeHours.js
@@ -0,0 +1,11 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatOfficeHour } from '../../../models';
+
+Meteor.publish('livechat:officeHour', function() {
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
+ }
+
+ return LivechatOfficeHour.find();
+});
diff --git a/app/livechat/server/publications/livechatQueue.js b/app/livechat/server/publications/livechatQueue.js
new file mode 100644
index 000000000000..2940473890cc
--- /dev/null
+++ b/app/livechat/server/publications/livechatQueue.js
@@ -0,0 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatDepartmentAgents } from '../../../models';
+
+Meteor.publish('livechat:queue', function() {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
+ }
+
+ // let sort = { count: 1, sort: 1, username: 1 };
+ // let onlineUsers = {};
+
+ // let handleUsers = RocketChat.models.Users.findOnlineAgents().observeChanges({
+ // added(id, fields) {
+ // onlineUsers[fields.username] = 1;
+ // // this.added('livechatQueueUser', id, fields);
+ // },
+ // changed(id, fields) {
+ // onlineUsers[fields.username] = 1;
+ // // this.changed('livechatQueueUser', id, fields);
+ // },
+ // removed(id) {
+ // this.removed('livechatQueueUser', id);
+ // }
+ // });
+
+ const self = this;
+
+ const handleDepts = LivechatDepartmentAgents.findUsersInQueue().observeChanges({
+ added(id, fields) {
+ self.added('livechatQueueUser', id, fields);
+ },
+ changed(id, fields) {
+ self.changed('livechatQueueUser', id, fields);
+ },
+ removed(id) {
+ self.removed('livechatQueueUser', id);
+ },
+ });
+
+ this.ready();
+
+ this.onStop(() => {
+ // handleUsers.stop();
+ handleDepts.stop();
+ });
+});
diff --git a/packages/rocketchat-livechat/server/publications/livechatRooms.js b/app/livechat/server/publications/livechatRooms.js
similarity index 86%
rename from packages/rocketchat-livechat/server/publications/livechatRooms.js
rename to app/livechat/server/publications/livechatRooms.js
index 0a5cb4f5354a..37eaebf30f3f 100644
--- a/packages/rocketchat-livechat/server/publications/livechatRooms.js
+++ b/app/livechat/server/publications/livechatRooms.js
@@ -1,13 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { hasPermission } from '../../../authorization';
+import { Rooms } from '../../../models';
Meteor.publish('livechat:rooms', function(filter = {}, offset = 0, limit = 20) {
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:rooms' }));
}
- if (!RocketChat.authz.hasPermission(this.userId, 'view-livechat-rooms')) {
+ if (!hasPermission(this.userId, 'view-livechat-rooms')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:rooms' }));
}
@@ -50,7 +51,7 @@ Meteor.publish('livechat:rooms', function(filter = {}, offset = 0, limit = 20) {
const self = this;
- const handle = RocketChat.models.Rooms.findLivechat(query, offset, limit).observeChanges({
+ const handle = Rooms.findLivechat(query, offset, limit).observeChanges({
added(id, fields) {
self.added('livechatRoom', id, fields);
},
diff --git a/app/livechat/server/publications/livechatTriggers.js b/app/livechat/server/publications/livechatTriggers.js
new file mode 100644
index 000000000000..185dd444d2c2
--- /dev/null
+++ b/app/livechat/server/publications/livechatTriggers.js
@@ -0,0 +1,19 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { LivechatTrigger } from '../../../models';
+
+Meteor.publish('livechat:triggers', function(_id) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:triggers' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:triggers' }));
+ }
+
+ if (_id !== undefined) {
+ return LivechatTrigger.findById(_id);
+ } else {
+ return LivechatTrigger.find();
+ }
+});
diff --git a/packages/rocketchat-livechat/server/publications/livechatVisitors.js b/app/livechat/server/publications/livechatVisitors.js
similarity index 82%
rename from packages/rocketchat-livechat/server/publications/livechatVisitors.js
rename to app/livechat/server/publications/livechatVisitors.js
index b71b4888d55b..5521d96a32f2 100644
--- a/packages/rocketchat-livechat/server/publications/livechatVisitors.js
+++ b/app/livechat/server/publications/livechatVisitors.js
@@ -1,14 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import LivechatVisitors from '../models/LivechatVisitors';
+import { hasPermission } from '../../../authorization';
+import { LivechatVisitors } from '../../../models';
Meteor.publish('livechat:visitors', function(date) {
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitors' }));
}
- if (!RocketChat.authz.hasPermission(this.userId, 'view-livechat-manager')) {
+ if (!hasPermission(this.userId, 'view-livechat-manager')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitors' }));
}
diff --git a/app/livechat/server/publications/visitorHistory.js b/app/livechat/server/publications/visitorHistory.js
new file mode 100644
index 000000000000..2309a1697929
--- /dev/null
+++ b/app/livechat/server/publications/visitorHistory.js
@@ -0,0 +1,44 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Rooms, Subscriptions } from '../../../models';
+
+Meteor.publish('livechat:visitorHistory', function({ rid: roomId }) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' }));
+ }
+
+ const room = Rooms.findOneById(roomId);
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } });
+ if (!subscription) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' }));
+ }
+
+ const self = this;
+
+ if (room && room.v && room.v._id) {
+ const handle = Rooms.findByVisitorId(room.v._id).observeChanges({
+ added(id, fields) {
+ self.added('visitor_history', id, fields);
+ },
+ changed(id, fields) {
+ self.changed('visitor_history', id, fields);
+ },
+ removed(id) {
+ self.removed('visitor_history', id);
+ },
+ });
+
+ self.ready();
+
+ self.onStop(function() {
+ handle.stop();
+ });
+ } else {
+ self.ready();
+ }
+});
diff --git a/app/livechat/server/publications/visitorInfo.js b/app/livechat/server/publications/visitorInfo.js
new file mode 100644
index 000000000000..054eb358d476
--- /dev/null
+++ b/app/livechat/server/publications/visitorInfo.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Rooms, LivechatVisitors } from '../../../models';
+
+Meteor.publish('livechat:visitorInfo', function({ rid: roomId }) {
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorInfo' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorInfo' }));
+ }
+
+ const room = Rooms.findOneById(roomId);
+
+ if (room && room.v && room.v._id) {
+ return LivechatVisitors.findById(room.v._id);
+ } else {
+ return this.ready();
+ }
+});
diff --git a/app/livechat/server/publications/visitorPageVisited.js b/app/livechat/server/publications/visitorPageVisited.js
new file mode 100644
index 000000000000..ae875b6f8f3f
--- /dev/null
+++ b/app/livechat/server/publications/visitorPageVisited.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Rooms, Messages } from '../../../models';
+
+Meteor.publish('livechat:visitorPageVisited', function({ rid: roomId }) {
+
+ if (!this.userId) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' }));
+ }
+
+ if (!hasPermission(this.userId, 'view-l-room')) {
+ return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' }));
+ }
+
+ const self = this;
+ const room = Rooms.findOneById(roomId);
+
+ if (room) {
+ const handle = Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history').observeChanges({
+ added(id, fields) {
+ self.added('visitor_navigation_history', id, fields);
+ },
+ changed(id, fields) {
+ self.changed('visitor_navigation_history', id, fields);
+ },
+ removed(id) {
+ self.removed('visitor_navigation_history', id);
+ },
+ });
+
+ self.ready();
+
+ self.onStop(function() {
+ handle.stop();
+ });
+ } else {
+ self.ready();
+ }
+});
diff --git a/app/livechat/server/roomType.js b/app/livechat/server/roomType.js
new file mode 100644
index 000000000000..3bbe342fb349
--- /dev/null
+++ b/app/livechat/server/roomType.js
@@ -0,0 +1,36 @@
+import { Rooms, LivechatVisitors } from '../../models';
+import { roomTypes } from '../../utils';
+import LivechatRoomType from '../lib/LivechatRoomType';
+
+
+class LivechatRoomTypeServer extends LivechatRoomType {
+ getMsgSender(senderId) {
+ return LivechatVisitors.findOneById(senderId);
+ }
+
+ /**
+ * Returns details to use on notifications
+ *
+ * @param {object} room
+ * @param {object} user
+ * @param {string} notificationMessage
+ * @return {object} Notification details
+ */
+ getNotificationDetails(room, user, notificationMessage) {
+ const title = `[livechat] ${ this.roomName(room) }`;
+ const text = notificationMessage;
+
+ return { title, text };
+ }
+
+ canAccessUploadedFile({ rc_token, rc_rid } = {}) {
+ return rc_token && rc_rid && Rooms.findOneOpenByRoomIdAndVisitorToken(rc_rid, rc_token);
+ }
+
+ getReadReceiptsExtraData(message) {
+ const { token } = message;
+ return { token };
+ }
+}
+
+roomTypes.add(new LivechatRoomTypeServer());
diff --git a/app/livechat/server/sendMessageBySMS.js b/app/livechat/server/sendMessageBySMS.js
new file mode 100644
index 000000000000..551c2263cdef
--- /dev/null
+++ b/app/livechat/server/sendMessageBySMS.js
@@ -0,0 +1,47 @@
+import { callbacks } from '../../callbacks';
+import { settings } from '../../settings';
+import { SMS } from '../../sms';
+import { LivechatVisitors } from '../../models';
+
+callbacks.add('afterSaveMessage', function(message, room) {
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ return message;
+ }
+
+ if (!SMS.enabled) {
+ return message;
+ }
+
+ // only send the sms by SMS if it is a livechat room with SMS set to true
+ if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.sms && room.v && room.v.token)) {
+ return message;
+ }
+
+ // if the message has a token, it was sent from the visitor, so ignore it
+ if (message.token) {
+ return message;
+ }
+
+ // if the message has a type means it is a special message (like the closing comment), so skips
+ if (message.t) {
+ return message;
+ }
+
+ const SMSService = SMS.getService(settings.get('SMS_Service'));
+
+ if (!SMSService) {
+ return message;
+ }
+
+ const visitor = LivechatVisitors.getVisitorByToken(room.v.token);
+
+ if (!visitor || !visitor.phone || visitor.phone.length === 0) {
+ return message;
+ }
+
+ SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg);
+
+ return message;
+
+}, callbacks.priority.LOW, 'sendMessageBySms');
diff --git a/app/livechat/server/startup.js b/app/livechat/server/startup.js
new file mode 100644
index 000000000000..0c6bd346b87c
--- /dev/null
+++ b/app/livechat/server/startup.js
@@ -0,0 +1,45 @@
+import { Meteor } from 'meteor/meteor';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { roomTypes } from '../../utils';
+import { Rooms } from '../../models';
+import { hasPermission, addRoomAccessValidator } from '../../authorization';
+import { callbacks } from '../../callbacks';
+import { settings } from '../../settings';
+import { LivechatInquiry } from '../lib/LivechatInquiry';
+
+Meteor.startup(() => {
+ roomTypes.setRoomFind('l', (_id) => Rooms.findOneLivechatById(_id));
+
+ addRoomAccessValidator(function(room, user) {
+ return room && room.t === 'l' && user && hasPermission(user._id, 'view-livechat-rooms');
+ });
+
+ addRoomAccessValidator(function(room, user, extraData) {
+ if (!room && extraData && extraData.rid) {
+ room = Rooms.findOneById(extraData.rid);
+ }
+ return room && room.t === 'l' && extraData && extraData.visitorToken && room.v && room.v.token === extraData.visitorToken;
+ });
+
+ addRoomAccessValidator(function(room, user) {
+ if (settings.get('Livechat_Routing_Method') !== 'Guest_Pool') {
+ return;
+ }
+
+ if (!user || !room || room.t !== 'l') {
+ return;
+ }
+
+ const inquiry = LivechatInquiry.findOne({ agents: user._id, rid: room._id }, { fields: { status: 1 } });
+ return inquiry && inquiry.status === 'open';
+ });
+
+ callbacks.add('beforeLeaveRoom', function(user, room) {
+ if (room.t !== 'l') {
+ return user;
+ }
+ throw new Meteor.Error(TAPi18n.__('You_cant_leave_a_livechat_room_Please_use_the_close_button', {
+ lng: user.language || settings.get('Language') || 'en',
+ }));
+ }, callbacks.priority.LOW, 'cant-leave-room');
+});
diff --git a/packages/rocketchat-livechat/server/unclosedLivechats.js b/app/livechat/server/unclosedLivechats.js
similarity index 76%
rename from packages/rocketchat-livechat/server/unclosedLivechats.js
rename to app/livechat/server/unclosedLivechats.js
index 76d73427a61d..5c06a018f3e1 100644
--- a/packages/rocketchat-livechat/server/unclosedLivechats.js
+++ b/app/livechat/server/unclosedLivechats.js
@@ -1,6 +1,8 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../settings';
+import { Users } from '../../models';
import { UserPresenceMonitor } from 'meteor/konecty:user-presence';
+import { Livechat } from './lib/Livechat';
let agentsHandler;
let monitorAgents = false;
@@ -36,23 +38,23 @@ const onlineAgents = {
};
function runAgentLeaveAction(userId) {
- const action = RocketChat.settings.get('Livechat_agent_leave_action');
+ const action = settings.get('Livechat_agent_leave_action');
if (action === 'close') {
- return RocketChat.Livechat.closeOpenChats(userId, RocketChat.settings.get('Livechat_agent_leave_comment'));
+ return Livechat.closeOpenChats(userId, settings.get('Livechat_agent_leave_comment'));
} else if (action === 'forward') {
- return RocketChat.Livechat.forwardOpenChats(userId);
+ return Livechat.forwardOpenChats(userId);
}
}
-RocketChat.settings.get('Livechat_agent_leave_action_timeout', function(key, value) {
+settings.get('Livechat_agent_leave_action_timeout', function(key, value) {
actionTimeout = value * 1000;
});
-RocketChat.settings.get('Livechat_agent_leave_action', function(key, value) {
+settings.get('Livechat_agent_leave_action', function(key, value) {
monitorAgents = value;
if (value !== 'none') {
if (!agentsHandler) {
- agentsHandler = RocketChat.models.Users.findOnlineAgents().observeChanges({
+ agentsHandler = Users.findOnlineAgents().observeChanges({
added(id) {
onlineAgents.add(id);
},
diff --git a/app/livechat/server/visitorStatus.js b/app/livechat/server/visitorStatus.js
new file mode 100644
index 000000000000..18775e7da01b
--- /dev/null
+++ b/app/livechat/server/visitorStatus.js
@@ -0,0 +1,11 @@
+import { Meteor } from 'meteor/meteor';
+import { UserPresenceEvents } from 'meteor/konecty:user-presence';
+import { Livechat } from './lib/Livechat';
+
+Meteor.startup(() => {
+ UserPresenceEvents.on('setStatus', (session, status, metadata) => {
+ if (metadata && metadata.visitor) {
+ Livechat.notifyGuestStatusChanged(metadata.visitor, status);
+ }
+ });
+});
diff --git a/packages/rocketchat-livestream/.gitignore b/app/livestream/.gitignore
similarity index 100%
rename from packages/rocketchat-livestream/.gitignore
rename to app/livestream/.gitignore
diff --git a/packages/rocketchat-livestream/client/index.js b/app/livestream/client/index.js
similarity index 100%
rename from packages/rocketchat-livestream/client/index.js
rename to app/livestream/client/index.js
diff --git a/app/livestream/client/oauth.js b/app/livestream/client/oauth.js
new file mode 100644
index 000000000000..fa3ea4713e60
--- /dev/null
+++ b/app/livestream/client/oauth.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+export const close = (popup) => new Promise(function(resolve) {
+ const checkInterval = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(checkInterval);
+ resolve();
+ }
+ }, 300);
+});
+
+export const auth = async () => {
+ const oauthWindow = window.open(`${ settings.get('Site_Url') }/api/v1/livestream/oauth?userId=${ Meteor.userId() }`, 'youtube-integration-oauth', 'width=400,height=600');
+ return await close(oauthWindow);
+};
diff --git a/packages/rocketchat-livestream/client/styles/liveStreamTab.css b/app/livestream/client/styles/liveStreamTab.css
similarity index 100%
rename from packages/rocketchat-livestream/client/styles/liveStreamTab.css
rename to app/livestream/client/styles/liveStreamTab.css
diff --git a/app/livestream/client/tabBar.js b/app/livestream/client/tabBar.js
new file mode 100644
index 000000000000..640a7ecde7b3
--- /dev/null
+++ b/app/livestream/client/tabBar.js
@@ -0,0 +1,25 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { Session } from 'meteor/session';
+import { TabBar } from '../../ui-utils';
+import { Rooms } from '../../models';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ Tracker.autorun(function() {
+ TabBar.removeButton('livestream');
+ if (settings.get('Livestream_enabled')) {
+ const live = Rooms.findOne({ _id: Session.get('openedRoom'), 'streamingOptions.type': 'livestream', 'streamingOptions.id': { $exists :1 } }, { fields: { streamingOptions: 1 } });
+ TabBar.size = live ? 5 : 4;
+ return TabBar.addButton({
+ groups: ['channel', 'group'],
+ id: 'livestream',
+ i18nTitle: 'Livestream',
+ icon: 'podcast',
+ template: 'liveStreamTab',
+ order: live ? -1 : 15,
+ class: () => live && 'live',
+ });
+ }
+ });
+});
diff --git a/packages/rocketchat-livestream/client/views/broadcastView.html b/app/livestream/client/views/broadcastView.html
similarity index 100%
rename from packages/rocketchat-livestream/client/views/broadcastView.html
rename to app/livestream/client/views/broadcastView.html
diff --git a/packages/rocketchat-livestream/client/views/broadcastView.js b/app/livestream/client/views/broadcastView.js
similarity index 92%
rename from packages/rocketchat-livestream/client/views/broadcastView.js
rename to app/livestream/client/views/broadcastView.js
index e2feea2e05f8..4033c56856f7 100644
--- a/packages/rocketchat-livestream/client/views/broadcastView.js
+++ b/app/livestream/client/views/broadcastView.js
@@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
-import { RocketChat, handleError } from 'meteor/rocketchat:lib';
+import { handleError } from '../../../utils';
+import { settings } from '../../../settings';
const getMedia = () => navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
const createAndConnect = (url) => {
@@ -31,8 +32,8 @@ export const call = (...args) => new Promise(function(resolve, reject) {
const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));
-const waitForStreamStatus = async(id, status) => {
- const streamActive = new Promise(async(resolve) => {
+const waitForStreamStatus = async (id, status) => {
+ const streamActive = new Promise(async (resolve) => {
while (true) { // eslint-disable-line no-constant-condition
const currentStatus = await call('livestreamStreamStatus', { streamId: id });
if (currentStatus === status) {
@@ -43,8 +44,8 @@ const waitForStreamStatus = async(id, status) => {
});
await streamActive;
};
-const waitForBroadcastStatus = async(id, status) => {
- const broadcastActive = new Promise(async(resolve) => {
+const waitForBroadcastStatus = async (id, status) => {
+ const broadcastActive = new Promise(async (resolve) => {
while (true) { // eslint-disable-line no-constant-condition
const currentStatus = await call('getBroadcastStatus', { broadcastId: id });
if (currentStatus === status) {
@@ -66,7 +67,7 @@ Template.broadcastView.helpers({
});
Template.broadcastView.onCreated(async function() {
- const connection = createAndConnect(`${ RocketChat.settings.get('Broadcasting_media_server_url') }/${ this.data.id }`);
+ const connection = createAndConnect(`${ settings.get('Broadcasting_media_server_url') }/${ this.data.id }`);
this.mediaStream = new ReactiveVar(null);
this.mediaRecorder = new ReactiveVar(null);
this.connection = new ReactiveVar(connection);
diff --git a/packages/rocketchat-livestream/client/views/liveStreamTab.html b/app/livestream/client/views/liveStreamTab.html
similarity index 100%
rename from packages/rocketchat-livestream/client/views/liveStreamTab.html
rename to app/livestream/client/views/liveStreamTab.html
diff --git a/packages/rocketchat-livestream/client/views/liveStreamTab.js b/app/livestream/client/views/liveStreamTab.js
similarity index 92%
rename from packages/rocketchat-livestream/client/views/liveStreamTab.js
rename to app/livestream/client/views/liveStreamTab.js
index 647d80c877b8..0bb608f54cf6 100644
--- a/packages/rocketchat-livestream/client/views/liveStreamTab.js
+++ b/app/livestream/client/views/liveStreamTab.js
@@ -6,9 +6,13 @@ import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import toastr from 'toastr';
import { auth } from '../oauth.js';
-import { RocketChatAnnouncement, RocketChat, handleError } from 'meteor/rocketchat:lib';
-import { popout } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { RocketChatAnnouncement } from '../../../lib';
+import { popout } from '../../../ui-utils';
+import { t, handleError } from '../../../utils';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { hasAllPermission } from '../../../authorization';
+import { Users, Rooms } from '../../../models';
export const call = (...args) => new Promise(function(resolve, reject) {
Meteor.call(...args, function(err, result) {
@@ -46,7 +50,7 @@ function optionsFromUrl(url) {
Template.liveStreamTab.helpers({
broadcastEnabled() {
- return !!RocketChat.settings.get('Broadcasting_enabled');
+ return !!settings.get('Broadcasting_enabled');
},
streamingSource() {
return Template.instance().streamingOptions.get() ? Template.instance().streamingOptions.get().url : '';
@@ -64,7 +68,7 @@ Template.liveStreamTab.helpers({
return !!Template.instance().streamingOptions.get() && !!Template.instance().streamingOptions.get().url && Template.instance().streamingOptions.get().url !== '';
},
canEdit() {
- return RocketChat.authz.hasAllPermission('edit-room', this.rid);
+ return hasAllPermission('edit-room', this.rid);
},
editing() {
return Template.instance().editing.get() || Template.instance().streamingOptions.get() == null || (Template.instance().streamingOptions.get() != null && (Template.instance().streamingOptions.get().url == null || Template.instance().streamingOptions.get().url === ''));
@@ -100,7 +104,7 @@ Template.liveStreamTab.onCreated(function() {
this.popoutOpen = new ReactiveVar(popout.context != null);
this.autorun(() => {
- const room = RocketChat.models.Rooms.findOne(this.data.rid, { fields: { streamingOptions : 1 } });
+ const room = Rooms.findOne(this.data.rid, { fields: { streamingOptions : 1 } });
this.streamingOptions.set(room.streamingOptions);
});
});
@@ -121,8 +125,6 @@ Template.liveStreamTab.events({
e.preventDefault();
const clearedObject = {
- message: i.streamingOptions.get().message || '',
- isAudioOnly: i.streamingOptions.get().isAudioOnly || false,
};
Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', clearedObject, function(err) {
@@ -246,7 +248,7 @@ Template.liveStreamTab.events({
e.preventDefault();
e.currentTarget.classList.add('loading');
try {
- const user = RocketChat.models.Users.findOne({ _id: Meteor.userId() }, { fields: { 'settings.livestream': 1 } });
+ const user = Users.findOne({ _id: Meteor.userId() }, { fields: { 'settings.livestream': 1 } });
if (!user.settings || !user.settings.livestream) {
await auth();
}
@@ -269,7 +271,7 @@ Template.liveStreamTab.events({
},
});
-RocketChat.callbacks.add('openBroadcast', (rid) => {
+callbacks.add('openBroadcast', (rid) => {
const roomData = Session.get(`roomData${ rid }`);
if (!roomData) { return; }
popout.open({
diff --git a/packages/rocketchat-livestream/client/views/liveStreamView.html b/app/livestream/client/views/liveStreamView.html
similarity index 100%
rename from packages/rocketchat-livestream/client/views/liveStreamView.html
rename to app/livestream/client/views/liveStreamView.html
diff --git a/packages/rocketchat-livestream/client/views/liveStreamView.js b/app/livestream/client/views/liveStreamView.js
similarity index 100%
rename from packages/rocketchat-livestream/client/views/liveStreamView.js
rename to app/livestream/client/views/liveStreamView.js
diff --git a/packages/rocketchat-livestream/client/views/livestreamBroadcast.html b/app/livestream/client/views/livestreamBroadcast.html
similarity index 100%
rename from packages/rocketchat-livestream/client/views/livestreamBroadcast.html
rename to app/livestream/client/views/livestreamBroadcast.html
diff --git a/packages/rocketchat-livestream/client/views/livestreamBroadcast.js b/app/livestream/client/views/livestreamBroadcast.js
similarity index 100%
rename from packages/rocketchat-livestream/client/views/livestreamBroadcast.js
rename to app/livestream/client/views/livestreamBroadcast.js
diff --git a/packages/rocketchat-livestream/server/functions/livestream.js b/app/livestream/server/functions/livestream.js
similarity index 95%
rename from packages/rocketchat-livestream/server/functions/livestream.js
rename to app/livestream/server/functions/livestream.js
index 2b91962bf99b..a5e6f8ab5f7e 100644
--- a/packages/rocketchat-livestream/server/functions/livestream.js
+++ b/app/livestream/server/functions/livestream.js
@@ -11,7 +11,7 @@ const p = (fn) => new Promise(function(resolve, reject) {
});
});
-export const getBroadcastStatus = async({
+export const getBroadcastStatus = async ({
id,
access_token,
refresh_token,
@@ -32,7 +32,7 @@ export const getBroadcastStatus = async({
return result.items && result.items[0] && result.items[0].status.lifeCycleStatus;
};
-export const statusStreamLiveStream = async({
+export const statusStreamLiveStream = async ({
id,
access_token,
refresh_token,
@@ -102,7 +102,7 @@ export const setBroadcastStatus = ({
}, resolve));
};
-export const createLiveStream = async({
+export const createLiveStream = async ({
room,
access_token,
refresh_token,
diff --git a/app/livestream/server/index.js b/app/livestream/server/index.js
new file mode 100644
index 000000000000..9254662a155a
--- /dev/null
+++ b/app/livestream/server/index.js
@@ -0,0 +1,3 @@
+import './routes.js';
+import './methods.js';
+import './settings';
diff --git a/app/livestream/server/methods.js b/app/livestream/server/methods.js
new file mode 100644
index 000000000000..ae8e6e62b25a
--- /dev/null
+++ b/app/livestream/server/methods.js
@@ -0,0 +1,143 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { Rooms } from '../../models';
+import { createLiveStream, statusLiveStream, statusStreamLiveStream, getBroadcastStatus, setBroadcastStatus } from './functions/livestream';
+
+const selectLivestreamSettings = (user) => user && user.settings && user.settings.livestream;
+
+Meteor.methods({
+
+ async livestreamStreamStatus({ streamId }) {
+ if (!streamId) {
+ // TODO: change error
+ throw new Meteor.Error('error-not-allowed', 'Livestream ID not found', {
+ method: 'livestreamStreamStatus',
+ });
+ }
+ const livestreamSettings = selectLivestreamSettings(Meteor.user());
+
+ if (!livestreamSettings) {
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', {
+ method: 'livestreamStreamStatus',
+ });
+ }
+
+ const { access_token, refresh_token } = livestreamSettings;
+
+ return await statusStreamLiveStream({
+ id: streamId,
+ access_token,
+ refresh_token,
+ clientId: settings.get('Broadcasting_client_id'),
+ clientSecret: settings.get('Broadcasting_client_secret'),
+ });
+
+ },
+ async setLivestreamStatus({ broadcastId, status }) {
+ if (!broadcastId) {
+ // TODO: change error
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', {
+ method: 'livestreamStart',
+ });
+ }
+ const livestreamSettings = selectLivestreamSettings(Meteor.user());
+
+ if (!livestreamSettings) {
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', {
+ method: 'livestreamStart',
+ });
+ }
+
+ const { access_token, refresh_token } = livestreamSettings;
+
+ return await statusLiveStream({
+ id: broadcastId,
+ access_token,
+ refresh_token,
+ status,
+ clientId: settings.get('Broadcasting_client_id'),
+ clientSecret: settings.get('Broadcasting_client_secret'),
+ });
+
+ },
+ async livestreamGet({ rid }) {
+ const livestreamSettings = selectLivestreamSettings(Meteor.user());
+
+ if (!livestreamSettings) {
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', {
+ method: 'livestreamGet',
+ });
+ }
+
+ const room = Rooms.findOne({ _id: rid });
+
+ if (!room) {
+ // TODO: change error
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', {
+ method: 'livestreamGet',
+ });
+ }
+
+ const { access_token, refresh_token } = livestreamSettings;
+ return await createLiveStream({
+ room,
+ access_token,
+ refresh_token,
+ clientId: settings.get('Broadcasting_client_id'),
+ clientSecret: settings.get('Broadcasting_client_secret'),
+ });
+
+ },
+ async getBroadcastStatus({ broadcastId }) {
+ if (!broadcastId) {
+ // TODO: change error
+ throw new Meteor.Error('error-not-allowed', 'Broadcast ID not found', {
+ method: 'getBroadcastStatus',
+ });
+ }
+ const livestreamSettings = selectLivestreamSettings(Meteor.user());
+
+ if (!livestreamSettings) {
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', {
+ method: 'getBroadcastStatus',
+ });
+ }
+
+ const { access_token, refresh_token } = livestreamSettings;
+
+ return await getBroadcastStatus({
+ id: broadcastId,
+ access_token,
+ refresh_token,
+ clientId: settings.get('Broadcasting_client_id'),
+ clientSecret: settings.get('Broadcasting_client_secret'),
+ });
+ },
+ async setBroadcastStatus({ broadcastId, status }) {
+ if (!broadcastId) {
+ // TODO: change error
+ throw new Meteor.Error('error-not-allowed', 'Broadcast ID not found', {
+ method: 'setBroadcastStatus',
+ });
+ }
+ const livestreamSettings = selectLivestreamSettings(Meteor.user());
+
+ if (!livestreamSettings) {
+ throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', {
+ method: 'setBroadcastStatus',
+ });
+ }
+
+ const { access_token, refresh_token } = livestreamSettings;
+
+ return await setBroadcastStatus({
+ id: broadcastId,
+ access_token,
+ refresh_token,
+ status,
+ clientId: settings.get('Broadcasting_client_id'),
+ clientSecret: settings.get('Broadcasting_client_secret'),
+ });
+
+ },
+});
diff --git a/app/livestream/server/routes.js b/app/livestream/server/routes.js
new file mode 100644
index 000000000000..cc860cd019f8
--- /dev/null
+++ b/app/livestream/server/routes.js
@@ -0,0 +1,49 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { Users } from '../../models';
+import { API } from '../../api';
+import google from 'googleapis';
+const { OAuth2 } = google.auth;
+
+API.v1.addRoute('livestream/oauth', {
+ get: function functionName() {
+ const clientAuth = new OAuth2(settings.get('Broadcasting_client_id'), settings.get('Broadcasting_client_secret'), `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api'));
+ const { userId } = this.queryParams;
+ const url = clientAuth.generateAuthUrl({
+ access_type: 'offline',
+ scope: ['https://www.googleapis.com/auth/youtube'],
+ state: JSON.stringify({
+ userId,
+ }),
+ });
+
+ return {
+ statusCode: 302,
+ headers: {
+ Location: url,
+ }, body: 'Oauth redirect',
+ };
+ },
+});
+
+API.v1.addRoute('livestream/oauth/callback', {
+ get: function functionName() {
+ const { code, state } = this.queryParams;
+
+ const { userId } = JSON.parse(state);
+
+ const clientAuth = new OAuth2(settings.get('Broadcasting_client_id'), settings.get('Broadcasting_client_secret'), `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api'));
+
+ const ret = Meteor.wrapAsync(clientAuth.getToken.bind(clientAuth))(code);
+
+ Users.update({ _id: userId }, { $set: {
+ 'settings.livestream' : ret,
+ } });
+
+ return {
+ headers: {
+ 'content-type' : 'text/html',
+ }, body: '',
+ };
+ },
+});
diff --git a/app/livestream/server/settings.js b/app/livestream/server/settings.js
new file mode 100644
index 000000000000..149cd4180852
--- /dev/null
+++ b/app/livestream/server/settings.js
@@ -0,0 +1,25 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ settings.addGroup('LiveStream & Broadcasting', function() {
+
+ this.add('Livestream_enabled', false, {
+ type: 'boolean',
+ public: true,
+ alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues',
+ });
+
+ this.add('Broadcasting_enabled', false, {
+ type: 'boolean',
+ public: true,
+ alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues',
+ enableQuery: { _id: 'Livestream_enabled', value: true },
+ });
+
+ this.add('Broadcasting_client_id', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } });
+ this.add('Broadcasting_client_secret', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } });
+ this.add('Broadcasting_api_key', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } });
+ this.add('Broadcasting_media_server_url', '', { type: 'string', public: true, enableQuery: { _id: 'Broadcasting_enabled', value: true } });
+ });
+});
diff --git a/packages/rocketchat-logger/README.md b/app/logger/README.md
similarity index 100%
rename from packages/rocketchat-logger/README.md
rename to app/logger/README.md
diff --git a/packages/rocketchat-logger/client/ansispan.js b/app/logger/client/ansispan.js
similarity index 100%
rename from packages/rocketchat-logger/client/ansispan.js
rename to app/logger/client/ansispan.js
diff --git a/packages/rocketchat-logger/client/index.js b/app/logger/client/index.js
similarity index 100%
rename from packages/rocketchat-logger/client/index.js
rename to app/logger/client/index.js
diff --git a/app/logger/client/logger.js b/app/logger/client/logger.js
new file mode 100644
index 000000000000..92556c3dc874
--- /dev/null
+++ b/app/logger/client/logger.js
@@ -0,0 +1,88 @@
+import { Template } from 'meteor/templating';
+import _ from 'underscore';
+
+import { getConfig } from '../../ui-utils/client/config';
+
+Template.log = !!(getConfig('debug') || getConfig('debug-template'));
+
+if (Template.log) {
+
+ Template.logMatch = /.*/;
+
+ Template.enableLogs = function(log) {
+ Template.logMatch = /.*/;
+ if (log === false) {
+ return Template.log = false;
+ } else {
+ Template.log = true;
+ if (log instanceof RegExp) {
+ return Template.logMatch = log;
+ }
+ }
+ };
+
+ const wrapHelpersAndEvents = function(original, prefix, color) {
+ return function(dict) {
+
+ const template = this;
+ const fn1 = function(name, fn) {
+ if (fn instanceof Function) {
+ return dict[name] = function(...args) {
+
+ const result = fn.apply(this, args);
+ if (Template.log === true) {
+ const completeName = `${ prefix }:${ template.viewName.replace('Template.', '') }.${ name }`;
+ if (Template.logMatch.test(completeName)) {
+ console.log(`%c${ completeName }`, `color: ${ color }`, {
+ args,
+ scope: this,
+ result,
+ });
+ }
+ }
+ return result;
+ };
+ }
+ };
+ _.each(name, (fn, name) => {
+ fn1(name, fn);
+ });
+ return original.call(template, dict);
+ };
+ };
+
+ Template.prototype.helpers = wrapHelpersAndEvents(Template.prototype.helpers, 'helper', 'blue');
+
+ Template.prototype.events = wrapHelpersAndEvents(Template.prototype.events, 'event', 'green');
+
+ const wrapLifeCycle = function(original, prefix, color) {
+ return function(fn) {
+ const template = this;
+ if (fn instanceof Function) {
+ const wrap = function(...args) {
+ const result = fn.apply(this, args);
+ if (Template.log === true) {
+ const completeName = `${ prefix }:${ template.viewName.replace('Template.', '') }.${ name }`;
+ if (Template.logMatch.test(completeName)) {
+ console.log(`%c${ completeName }`, `color: ${ color }; font-weight: bold`, {
+ args,
+ scope: this,
+ result,
+ });
+ }
+ }
+ return result;
+ };
+ return original.call(template, wrap);
+ } else {
+ return original.call(template, fn);
+ }
+ };
+ };
+
+ Template.prototype.onCreated = wrapLifeCycle(Template.prototype.onCreated, 'onCreated', 'blue');
+
+ Template.prototype.onRendered = wrapLifeCycle(Template.prototype.onRendered, 'onRendered', 'green');
+
+ Template.prototype.onDestroyed = wrapLifeCycle(Template.prototype.onDestroyed, 'onDestroyed', 'red');
+}
diff --git a/app/logger/client/viewLogs.js b/app/logger/client/viewLogs.js
new file mode 100644
index 000000000000..8185bf6a6f0f
--- /dev/null
+++ b/app/logger/client/viewLogs.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { Mongo } from 'meteor/mongo';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { AdminBox } from '../../ui-utils';
+import { hasAllPermission } from '../../authorization';
+import { t } from '../../utils';
+
+export const stdout = new Mongo.Collection('stdout');
+
+Meteor.startup(function() {
+ AdminBox.addOption({
+ href: 'admin-view-logs',
+ i18nLabel: 'View_Logs',
+ icon: 'post',
+ permissionGranted() {
+ return hasAllPermission('view-logs');
+ },
+ });
+});
+
+FlowRouter.route('/admin/view-logs', {
+ name: 'admin-view-logs',
+ action() {
+ return BlazeLayout.render('main', {
+ center: 'pageSettingsContainer',
+ pageTitle: t('View_Logs'),
+ pageTemplate: 'viewLogs',
+ noScroll: true,
+ });
+ },
+});
diff --git a/packages/rocketchat-logger/client/views/viewLogs.html b/app/logger/client/views/viewLogs.html
similarity index 100%
rename from packages/rocketchat-logger/client/views/viewLogs.html
rename to app/logger/client/views/viewLogs.html
diff --git a/app/logger/client/views/viewLogs.js b/app/logger/client/views/viewLogs.js
new file mode 100644
index 000000000000..70df611076ec
--- /dev/null
+++ b/app/logger/client/views/viewLogs.js
@@ -0,0 +1,134 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { Tracker } from 'meteor/tracker';
+import { ansispan } from '../ansispan';
+import { stdout } from '../viewLogs';
+import { readMessage } from '../../../ui-utils';
+import { hasAllPermission } from '../../../authorization';
+import _ from 'underscore';
+import moment from 'moment';
+import { SideNav } from '../../../ui-utils/client';
+
+Template.viewLogs.onCreated(function() {
+ this.subscribe('stdout');
+ return this.atBottom = true;
+});
+
+Template.viewLogs.helpers({
+ hasPermission() {
+ return hasAllPermission('view-logs');
+ },
+ logs() {
+ return stdout.find({}, {
+ sort: {
+ ts: 1,
+ },
+ });
+ },
+ ansispan(string) {
+ string = ansispan(string.replace(/\s/g, ' ').replace(/(\\n|\n)/g, '
'));
+ string = string.replace(/(.\d{8}-\d\d:\d\d:\d\d\.\d\d\d\(?.{0,2}\)?)/, '
$1 ');
+ return string;
+ },
+ formatTS(date) {
+ return moment(date).format('YMMDD-HH:mm:ss.SSS(ZZ)');
+ },
+});
+
+Template.viewLogs.events({
+ 'click .new-logs'() {
+ Template.instance().atBottom = true;
+ return Template.instance().sendToBottomIfNecessary();
+ },
+});
+
+Template.viewLogs.onRendered(function() {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+ const wrapper = this.find('.terminal');
+ const wrapperUl = this.find('.terminal');
+ const newLogs = this.find('.new-logs');
+ const template = this;
+ template.isAtBottom = function(scrollThreshold) {
+ if (scrollThreshold == null) {
+ scrollThreshold = 0;
+ }
+ if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) {
+ newLogs.className = 'new-logs not';
+ return true;
+ }
+ return false;
+ };
+ template.sendToBottom = function() {
+ wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight;
+ return newLogs.className = 'new-logs not';
+ };
+ template.checkIfScrollIsAtBottom = function() {
+ template.atBottom = template.isAtBottom(100);
+ readMessage.enable();
+ return readMessage.read();
+ };
+ template.sendToBottomIfNecessary = function() {
+ if (template.atBottom === true && template.isAtBottom() !== true) {
+ return template.sendToBottom();
+ } else if (template.atBottom === false) {
+ return newLogs.className = 'new-logs';
+ }
+ };
+ template.sendToBottomIfNecessaryDebounced = _.debounce(template.sendToBottomIfNecessary, 10);
+ template.sendToBottomIfNecessary();
+ if (window.MutationObserver == null) {
+ wrapperUl.addEventListener('DOMSubtreeModified', function() {
+ return template.sendToBottomIfNecessaryDebounced();
+ });
+ } else {
+ const observer = new MutationObserver(function(mutations) {
+ return mutations.forEach(function() {
+ return template.sendToBottomIfNecessaryDebounced();
+ });
+ });
+ observer.observe(wrapperUl, {
+ childList: true,
+ });
+ }
+ template.onWindowResize = function() {
+ return Meteor.defer(function() {
+ return template.sendToBottomIfNecessaryDebounced();
+ });
+ };
+ window.addEventListener('resize', template.onWindowResize);
+ wrapper.addEventListener('mousewheel', function() {
+ template.atBottom = false;
+ return Meteor.defer(function() {
+ return template.checkIfScrollIsAtBottom();
+ });
+ });
+ wrapper.addEventListener('wheel', function() {
+ template.atBottom = false;
+ return Meteor.defer(function() {
+ return template.checkIfScrollIsAtBottom();
+ });
+ });
+ wrapper.addEventListener('touchstart', function() {
+ return template.atBottom = false;
+ });
+ wrapper.addEventListener('touchend', function() {
+ Meteor.defer(function() {
+ return template.checkIfScrollIsAtBottom();
+ });
+ Meteor.setTimeout(function() {
+ return template.checkIfScrollIsAtBottom();
+ }, 1000);
+ return Meteor.setTimeout(function() {
+ return template.checkIfScrollIsAtBottom();
+ }, 2000);
+ });
+ return wrapper.addEventListener('scroll', function() {
+ template.atBottom = false;
+ return Meteor.defer(function() {
+ return template.checkIfScrollIsAtBottom();
+ });
+ });
+});
diff --git a/app/logger/index.js b/app/logger/index.js
new file mode 100644
index 000000000000..a67eca871efb
--- /dev/null
+++ b/app/logger/index.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+if (Meteor.isClient) {
+ module.exports = require('./client/index.js');
+}
+if (Meteor.isServer) {
+ module.exports = require('./server/index.js');
+}
diff --git a/packages/rocketchat-logger/server/index.js b/app/logger/server/index.js
similarity index 100%
rename from packages/rocketchat-logger/server/index.js
rename to app/logger/server/index.js
diff --git a/app/logger/server/server.js b/app/logger/server/server.js
new file mode 100644
index 000000000000..d7750a2d6064
--- /dev/null
+++ b/app/logger/server/server.js
@@ -0,0 +1,383 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { EJSON } from 'meteor/ejson';
+import { Log } from 'meteor/logging';
+import { EventEmitter } from 'events';
+import { settings } from '../../settings';
+import { hasPermission } from '../../authorization';
+import _ from 'underscore';
+import s from 'underscore.string';
+
+let Logger;
+
+const LoggerManager = new class extends EventEmitter {
+ constructor() {
+ super();
+ this.enabled = false;
+ this.loggers = {};
+ this.queue = [];
+ this.showPackage = false;
+ this.showFileAndLine = false;
+ this.logLevel = 0;
+ }
+ register(logger) {
+ if (!logger instanceof Logger) {
+ return;
+ }
+ this.loggers[logger.name] = logger;
+ this.emit('register', logger);
+ }
+ addToQueue(logger, args) {
+ this.queue.push({
+ logger, args,
+ });
+ }
+ dispatchQueue() {
+ _.each(this.queue, (item) => item.logger._log.apply(item.logger, item.args));
+ this.clearQueue();
+ }
+ clearQueue() {
+ this.queue = [];
+ }
+
+ disable() {
+ this.enabled = false;
+ }
+
+ enable(dispatchQueue = false) {
+ this.enabled = true;
+ return (dispatchQueue === true) ? this.dispatchQueue() : this.clearQueue();
+ }
+};
+
+const defaultTypes = {
+ debug: {
+ name: 'debug',
+ color: 'blue',
+ level: 2,
+ },
+ log: {
+ name: 'info',
+ color: 'blue',
+ level: 1,
+ },
+ info: {
+ name: 'info',
+ color: 'blue',
+ level: 1,
+ },
+ success: {
+ name: 'info',
+ color: 'green',
+ level: 1,
+ },
+ warn: {
+ name: 'warn',
+ color: 'magenta',
+ level: 1,
+ },
+ error: {
+ name: 'error',
+ color: 'red',
+ level: 0,
+ },
+};
+
+class _Logger {
+ constructor(name, config = {}) {
+ const self = this;
+ this.name = name;
+
+ this.config = Object.assign({}, config);
+ if (LoggerManager.loggers && LoggerManager.loggers[this.name] != null) {
+ LoggerManager.loggers[this.name].warn('Duplicated instance');
+ return LoggerManager.loggers[this.name];
+ }
+ _.each(defaultTypes, (typeConfig, type) => {
+ this[type] = function(...args) {
+ return self._log.call(self, {
+ section: this.__section,
+ type,
+ level: typeConfig.level,
+ method: typeConfig.name,
+ arguments: args,
+ });
+ };
+
+ self[`${ type }_box`] = function(...args) {
+ return self._log.call(self, {
+ section: this.__section,
+ type,
+ box: true,
+ level: typeConfig.level,
+ method: typeConfig.name,
+ arguments: args,
+ });
+ };
+ });
+ if (this.config.methods) {
+ _.each(this.config.methods, (typeConfig, method) => {
+ if (this[method] != null) {
+ self.warn(`Method ${ method } already exists`);
+ }
+ if (defaultTypes[typeConfig.type] == null) {
+ self.warn(`Method type ${ typeConfig.type } does not exist`);
+ }
+ this[method] = function(...args) {
+ return self._log.call(self, {
+ section: this.__section,
+ type: typeConfig.type,
+ level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level,
+ method,
+ arguments: args,
+ });
+ };
+ this[`${ method }_box`] = function(...args) {
+ return self._log.call(self, {
+ section: this.__section,
+ type: typeConfig.type,
+ box: true,
+ level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level,
+ method,
+ arguments: args,
+ });
+ };
+ });
+ }
+ if (this.config.sections) {
+ _.each(this.config.sections, (name, section) => {
+ this[section] = {};
+ _.each(defaultTypes, (typeConfig, type) => {
+ self[section][type] = (...args) => this[type].apply({ __section: name }, args);
+ self[section][`${ type }_box`] = (...args) => this[`${ type }_box`].apply({ __section: name }, args);
+ });
+ _.each(this.config.methods, (typeConfig, method) => {
+ self[section][method] = (...args) => self[method].apply({ __section: name }, args);
+ self[section][`${ method }_box`] = (...args) => self[`${ method }_box`].apply({ __section: name }, args);
+ });
+ });
+ }
+
+ LoggerManager.register(this);
+ }
+ getPrefix(options) {
+ let prefix = `${ this.name } ➔ ${ options.method }`;
+ if (options.section) {
+ prefix = `${ this.name } ➔ ${ options.section }.${ options.method }`;
+ }
+ const details = this._getCallerDetails();
+ const detailParts = [];
+ if (details.package && (LoggerManager.showPackage === true || options.type === 'error')) {
+ detailParts.push(details.package);
+ }
+ if (LoggerManager.showFileAndLine === true || options.type === 'error') {
+ if ((details.file != null) && (details.line != null)) {
+ detailParts.push(`${ details.file }:${ details.line }`);
+ } else {
+ if (details.file != null) {
+ detailParts.push(details.file);
+ }
+ if (details.line != null) {
+ detailParts.push(details.line);
+ }
+ }
+ }
+ if (defaultTypes[options.type]) {
+ // format the message to a colored message
+ prefix = prefix[defaultTypes[options.type].color];
+ }
+ if (detailParts.length > 0) {
+ prefix = `${ detailParts.join(' ') } ${ prefix }`;
+ }
+ return prefix;
+ }
+ _getCallerDetails() {
+ const getStack = () => {
+ // We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a
+ // core-parsed stack) since it's impossible to compose it with the use of
+ // Error.prepareStackTrace used on the server for source maps.
+ const { stack } = new Error();
+ return stack;
+ };
+ const stack = getStack();
+ if (!stack) {
+ return {};
+ }
+ const lines = stack.split('\n').splice(1);
+ // looking for the first line outside the logging package (or an
+ // eval if we find that first)
+ let line = lines[0];
+ for (let index = 0, len = lines.length; index < len, index++; line = lines[index]) {
+ if (line.match(/^\s*at eval \(eval/)) {
+ return { file: 'eval' };
+ }
+
+ if (!line.match(/packages\/rocketchat_logger(?:\/|\.js)/)) {
+ break;
+ }
+ }
+
+ const details = {};
+ // The format for FF is 'functionName@filePath:lineNumber'
+ // The format for V8 is 'functionName (packages/logging/logging.js:81)' or
+ // 'packages/logging/logging.js:81'
+ const match = /(?:[@(]| at )([^(]+?):([0-9:]+)(?:\)|$)/.exec(line);
+ if (!match) {
+ return details;
+ }
+ details.line = match[2].split(':')[0];
+ // Possible format: https://foo.bar.com/scripts/file.js?random=foobar
+ // XXX: if you can write the following in better way, please do it
+ // XXX: what about evals?
+ details.file = match[1].split('/').slice(-1)[0].split('?')[0];
+ const packageMatch = match[1].match(/packages\/([^\.\/]+)(?:\/|\.)/);
+ if (packageMatch) {
+ details.package = packageMatch[1];
+ }
+ return details;
+ }
+ makeABox(message, title) {
+ if (!_.isArray(message)) {
+ message = message.split('\n');
+ }
+ let len = 0;
+
+ len = Math.max.apply(null, message.map((line) => line.length));
+
+ const topLine = `+--${ s.pad('', len, '-') }--+`;
+ const separator = `| ${ s.pad('', len, '') } |`;
+ let lines = [];
+
+ lines.push(topLine);
+ if (title) {
+ lines.push(`| ${ s.lrpad(title, len) } |`);
+ lines.push(topLine);
+ }
+ lines.push(separator);
+
+ lines = [...lines, ...message.map((line) => `| ${ s.rpad(line, len) } |`)];
+
+ lines.push(separator);
+ lines.push(topLine);
+ return lines;
+ }
+
+ _log(options, ...args) {
+ if (LoggerManager.enabled === false) {
+ LoggerManager.addToQueue(this, [options, ...args]);
+ return;
+ }
+ if (options.level == null) {
+ options.level = 1;
+ }
+
+ if (LoggerManager.logLevel < options.level) {
+ return;
+ }
+
+ const prefix = this.getPrefix(options);
+
+ if (options.box === true && _.isString(options.arguments[0])) {
+ let color = undefined;
+ if (defaultTypes[options.type]) {
+ color = defaultTypes[options.type].color;
+ }
+
+ const box = this.makeABox(options.arguments[0], options.arguments[1]);
+ let subPrefix = '➔';
+ if (color) {
+ subPrefix = subPrefix[color];
+ }
+
+ console.log(subPrefix, prefix);
+ box.forEach((line) => {
+ console.log(subPrefix, color ? line[color] : line);
+ });
+
+ } else {
+ options.arguments.unshift(prefix);
+ console.log.apply(console, options.arguments);
+ }
+ }
+}
+
+Logger = _Logger;
+const processString = function(string, date) {
+ let obj;
+ try {
+ if (string[0] === '{') {
+ obj = EJSON.parse(string);
+ } else {
+ obj = {
+ message: string,
+ time: date,
+ level: 'info',
+ };
+ }
+ return Log.format(obj, { color: true });
+ } catch (error) {
+ return string;
+ }
+};
+
+const SystemLogger = new Logger('System', {
+ methods: {
+ startup: {
+ type: 'success',
+ level: 0,
+ },
+ },
+});
+
+
+const StdOut = new class extends EventEmitter {
+ constructor() {
+ super();
+ const { write } = process.stdout;
+ this.queue = [];
+ process.stdout.write = (...args) => {
+ write.apply(process.stdout, args);
+ const date = new Date;
+ const string = processString(args[0], date);
+ const item = {
+ id: Random.id(),
+ string,
+ ts: date,
+ };
+ this.queue.push(item);
+
+ if (typeof settings !== 'undefined') {
+ const limit = settings.get('Log_View_Limit');
+ if (limit && this.queue.length > limit) {
+ this.queue.shift();
+ }
+ }
+ this.emit('write', string, item);
+ };
+ }
+};
+
+
+Meteor.publish('stdout', function() {
+ if (!this.userId || hasPermission(this.userId, 'view-logs') !== true) {
+ return this.ready();
+ }
+
+ StdOut.queue.forEach((item) => {
+ this.added('stdout', item.id, {
+ string: item.string,
+ ts: item.ts,
+ });
+ });
+
+ this.ready();
+ StdOut.on('write', (string, item) => {
+ this.added('stdout', item.id, {
+ string: item.string,
+ ts: item.ts,
+ });
+ });
+});
+
+
+export { SystemLogger, StdOut, LoggerManager, processString, Logger };
diff --git a/packages/rocketchat-mail-messages/client/index.js b/app/mail-messages/client/index.js
similarity index 100%
rename from packages/rocketchat-mail-messages/client/index.js
rename to app/mail-messages/client/index.js
diff --git a/app/mail-messages/client/router.js b/app/mail-messages/client/router.js
new file mode 100644
index 000000000000..9e706f11be74
--- /dev/null
+++ b/app/mail-messages/client/router.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+
+FlowRouter.route('/admin/mailer', {
+ name: 'admin-mailer',
+ action() {
+ return BlazeLayout.render('main', {
+ center: 'mailer',
+ });
+ },
+});
+
+FlowRouter.route('/mailer/unsubscribe/:_id/:createdAt', {
+ name: 'mailer-unsubscribe',
+ action(params) {
+ Meteor.call('Mailer:unsubscribe', params._id, params.createdAt);
+ return BlazeLayout.render('mailerUnsubscribe');
+ },
+});
diff --git a/app/mail-messages/client/startup.js b/app/mail-messages/client/startup.js
new file mode 100644
index 000000000000..35f55d75cbbe
--- /dev/null
+++ b/app/mail-messages/client/startup.js
@@ -0,0 +1,11 @@
+import { AdminBox } from '../../ui-utils';
+import { hasAllPermission } from '../../authorization';
+
+AdminBox.addOption({
+ href: 'admin-mailer',
+ i18nLabel: 'Mailer',
+ icon: 'mail',
+ permissionGranted() {
+ return hasAllPermission('access-mailer');
+ },
+});
diff --git a/packages/rocketchat-mail-messages/client/views/mailer.html b/app/mail-messages/client/views/mailer.html
similarity index 100%
rename from packages/rocketchat-mail-messages/client/views/mailer.html
rename to app/mail-messages/client/views/mailer.html
diff --git a/app/mail-messages/client/views/mailer.js b/app/mail-messages/client/views/mailer.js
new file mode 100644
index 000000000000..e21205cc319e
--- /dev/null
+++ b/app/mail-messages/client/views/mailer.js
@@ -0,0 +1,46 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { Tracker } from 'meteor/tracker';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../../settings';
+import { handleError } from '../../../utils';
+import toastr from 'toastr';
+import { SideNav } from '../../../ui-utils/client';
+
+Template.mailer.helpers({
+ fromEmail() {
+ return settings.get('From_Email');
+ },
+});
+
+Template.mailer.events({
+ 'click .send'(e, t) {
+ e.preventDefault();
+ const from = $(t.find('[name=from]')).val();
+ const subject = $(t.find('[name=subject]')).val();
+ const body = $(t.find('[name=body]')).val();
+ const dryrun = $(t.find('[name=dryrun]:checked')).val();
+ const query = $(t.find('[name=query]')).val();
+ if (!from) {
+ toastr.error(TAPi18n.__('error-invalid-from-address'));
+ return;
+ }
+ if (body.indexOf('[unsubscribe]') === -1) {
+ toastr.error(TAPi18n.__('error-missing-unsubscribe-link'));
+ return;
+ }
+ return Meteor.call('Mailer.sendMail', from, subject, body, dryrun, query, function(err) {
+ if (err) {
+ return handleError(err);
+ }
+ return toastr.success(TAPi18n.__('The_emails_are_being_sent'));
+ });
+ },
+});
+
+Template.mailer.onRendered(() => {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+});
diff --git a/packages/rocketchat-mail-messages/client/views/mailerUnsubscribe.html b/app/mail-messages/client/views/mailerUnsubscribe.html
similarity index 100%
rename from packages/rocketchat-mail-messages/client/views/mailerUnsubscribe.html
rename to app/mail-messages/client/views/mailerUnsubscribe.html
diff --git a/packages/rocketchat-mail-messages/client/views/mailerUnsubscribe.js b/app/mail-messages/client/views/mailerUnsubscribe.js
similarity index 100%
rename from packages/rocketchat-mail-messages/client/views/mailerUnsubscribe.js
rename to app/mail-messages/client/views/mailerUnsubscribe.js
diff --git a/app/mail-messages/server/functions/sendMail.js b/app/mail-messages/server/functions/sendMail.js
new file mode 100644
index 000000000000..5e1db39d6d0f
--- /dev/null
+++ b/app/mail-messages/server/functions/sendMail.js
@@ -0,0 +1,64 @@
+import { Meteor } from 'meteor/meteor';
+import { EJSON } from 'meteor/ejson';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { placeholders } from '../../../utils';
+import s from 'underscore.string';
+import * as Mailer from '../../../mailer';
+
+export const sendMail = function(from, subject, body, dryrun, query) {
+ Mailer.checkAddressFormatAndThrow(from, 'Mailer.sendMail');
+ if (body.indexOf('[unsubscribe]') === -1) {
+ throw new Meteor.Error('error-missing-unsubscribe-link', 'You must provide the [unsubscribe] link.', {
+ function: 'Mailer.sendMail',
+ });
+ }
+
+ let userQuery = { 'mailer.unsubscribed': { $exists: 0 } };
+ if (query) {
+ userQuery = { $and: [userQuery, EJSON.parse(query)] };
+ }
+
+ if (dryrun) {
+ return Meteor.users.find({
+ 'emails.address': from,
+ }).forEach((user) => {
+ const email = `${ user.name } <${ user.emails[0].address }>`;
+ const html = placeholders.replace(body, {
+ unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', {
+ _id: user._id,
+ createdAt: user.createdAt.getTime(),
+ })),
+ name: user.name,
+ email,
+ });
+
+ console.log(`Sending email to ${ email }`);
+ return Mailer.send({
+ to: email,
+ from,
+ subject,
+ html,
+ });
+ });
+ }
+
+ return Meteor.users.find(userQuery).forEach(function(user) {
+ const email = `${ user.name } <${ user.emails[0].address }>`;
+
+ const html = placeholders.replace(body, {
+ unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', {
+ _id: user._id,
+ createdAt: user.createdAt.getTime(),
+ })),
+ name: s.escapeHTML(user.name),
+ email: s.escapeHTML(email),
+ });
+ console.log(`Sending email to ${ email }`);
+ return Mailer.send({
+ to: email,
+ from,
+ subject,
+ html,
+ });
+ });
+};
diff --git a/app/mail-messages/server/functions/unsubscribe.js b/app/mail-messages/server/functions/unsubscribe.js
new file mode 100644
index 000000000000..9d892acb0c50
--- /dev/null
+++ b/app/mail-messages/server/functions/unsubscribe.js
@@ -0,0 +1,8 @@
+import { Users } from '../../../models';
+
+export const unsubscribe = function(_id, createdAt) {
+ if (_id && createdAt) {
+ return Users.rocketMailUnsubscribe(_id, createdAt) === 1;
+ }
+ return false;
+};
diff --git a/app/mail-messages/server/index.js b/app/mail-messages/server/index.js
new file mode 100644
index 000000000000..a05bc57cc61e
--- /dev/null
+++ b/app/mail-messages/server/index.js
@@ -0,0 +1,8 @@
+import './startup';
+import './methods/sendMail';
+import './methods/unsubscribe';
+import { Mailer } from './lib/Mailer';
+
+export {
+ Mailer,
+};
diff --git a/packages/rocketchat-mail-messages/server/lib/Mailer.js b/app/mail-messages/server/lib/Mailer.js
similarity index 100%
rename from packages/rocketchat-mail-messages/server/lib/Mailer.js
rename to app/mail-messages/server/lib/Mailer.js
diff --git a/app/mail-messages/server/methods/sendMail.js b/app/mail-messages/server/methods/sendMail.js
new file mode 100644
index 000000000000..5dcc9b729aeb
--- /dev/null
+++ b/app/mail-messages/server/methods/sendMail.js
@@ -0,0 +1,28 @@
+import { Meteor } from 'meteor/meteor';
+import { Mailer } from '../lib/Mailer';
+import { hasRole } from '../../../authorization';
+
+Meteor.methods({
+ 'Mailer.sendMail'(from, subject, body, dryrun, query) {
+ const userId = Meteor.userId();
+ if (!userId) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'Mailer.sendMail',
+ });
+ }
+ if (hasRole(userId, 'admin') !== true) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', {
+ method: 'Mailer.sendMail',
+ });
+ }
+ return Mailer.sendMail(from, subject, body, dryrun, query);
+ },
+});
+
+
+// Limit setting username once per minute
+// DDPRateLimiter.addRule
+// type: 'method'
+// name: 'Mailer.sendMail'
+// connectionId: -> return true
+// , 1, 60000
diff --git a/packages/rocketchat-mail-messages/server/methods/unsubscribe.js b/app/mail-messages/server/methods/unsubscribe.js
similarity index 100%
rename from packages/rocketchat-mail-messages/server/methods/unsubscribe.js
rename to app/mail-messages/server/methods/unsubscribe.js
diff --git a/app/mail-messages/server/startup.js b/app/mail-messages/server/startup.js
new file mode 100644
index 000000000000..ed0993fcad50
--- /dev/null
+++ b/app/mail-messages/server/startup.js
@@ -0,0 +1,11 @@
+import { Meteor } from 'meteor/meteor';
+import { Permissions } from '../../models';
+
+Meteor.startup(function() {
+ return Permissions.upsert('access-mailer', {
+ $setOnInsert: {
+ _id: 'access-mailer',
+ roles: ['admin'],
+ },
+ });
+});
diff --git a/app/mailer/index.js b/app/mailer/index.js
new file mode 100644
index 000000000000..7c4b0096a2da
--- /dev/null
+++ b/app/mailer/index.js
@@ -0,0 +1 @@
+export * from './server/api';
diff --git a/app/mailer/server/api.js b/app/mailer/server/api.js
new file mode 100644
index 000000000000..c8044be64766
--- /dev/null
+++ b/app/mailer/server/api.js
@@ -0,0 +1,114 @@
+import { Meteor } from 'meteor/meteor';
+import { Email } from 'meteor/email';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../settings';
+import _ from 'underscore';
+import s from 'underscore.string';
+import juice from 'juice';
+let contentHeader;
+let contentFooter;
+
+let body;
+let Settings = {
+ get: () => {},
+};
+
+// define server language for email translations
+// @TODO: change TAPi18n.__ function to use the server language by default
+let lng = 'en';
+settings.get('Language', (key, value) => {
+ lng = value || 'en';
+});
+
+export const replacekey = (str, key, value = '') => str.replace(new RegExp(`(\\[${ key }\\]|__${ key }__)`, 'igm'), value);
+export const translate = (str) => str.replace(/\{ ?([^\} ]+)(( ([^\}]+))+)? ?\}/gmi, (match, key) => TAPi18n.__(key, { lng }));
+export const replace = function replace(str, data = {}) {
+ if (!str) {
+ return '';
+ }
+ const options = {
+ Site_Name: Settings.get('Site_Name'),
+ Site_URL: Settings.get('Site_Url'),
+ Site_URL_Slash: Settings.get('Site_Url').replace(/\/?$/, '/'),
+ ...(data.name && {
+ fname: s.strLeft(data.name, ' '),
+ lname: s.strRightBack(data.name, ' '),
+ }),
+ ...data,
+ };
+ return Object.entries(options).reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str));
+};
+
+export const replaceEscaped = (str, data = {}) => replace(str, {
+ Site_Name: s.escapeHTML(settings.get('Site_Name')),
+ Site_Url: s.escapeHTML(settings.get('Site_Url')),
+ ...Object.entries(data).reduce((ret, [key, value]) => {
+ ret[key] = s.escapeHTML(value);
+ return ret;
+ }, {}),
+});
+export const wrap = (html, data = {}) => replaceEscaped(body.replace('{{body}}', html), data);
+export const inlinecss = (html) => juice.inlineContent(html, Settings.get('email_style'));
+export const getTemplate = (template, fn, escape = true) => {
+ let html = '';
+ Settings.get(template, (key, value) => {
+ html = value || '';
+ fn(escape ? inlinecss(html) : html);
+ });
+ Settings.get('email_style', () => {
+ fn(escape ? inlinecss(html) : html);
+ });
+};
+export const getTemplateWrapped = (template, fn) => {
+ let html = '';
+ const wrapInlineCSS = _.debounce(() => fn(wrap(inlinecss(html))), 100);
+
+ Settings.get('Email_Header', () => html && wrapInlineCSS());
+ Settings.get('Email_Footer', () => html && wrapInlineCSS());
+ Settings.get('email_style', () => html && wrapInlineCSS());
+ Settings.get(template, (key, value) => {
+ html = value || '';
+ return html && wrapInlineCSS();
+ });
+};
+export const setSettings = (s) => {
+ Settings = s;
+
+ getTemplate('Email_Header', (value) => {
+ contentHeader = replace(value || '');
+ body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`);
+ }, false);
+
+ getTemplate('Email_Footer', (value) => {
+ contentFooter = replace(value || '');
+ body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`);
+ }, false);
+
+ body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`);
+};
+
+export const rfcMailPatternWithName = /^(?:.*<)?([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(?:>?)$/;
+
+export const checkAddressFormat = (from) => rfcMailPatternWithName.test(from);
+
+export const sendNoWrap = ({ to, from, subject, html, headers }) => {
+ if (!checkAddressFormat(to)) {
+ return;
+ }
+ Meteor.defer(() => Email.send({ to, from, subject, html, headers }));
+};
+
+export const send = ({ to, from, subject, html, data, headers }) => sendNoWrap({ to, from, subject: replace(subject, data), html: wrap(html, data), headers });
+
+export const checkAddressFormatAndThrow = (from, func) => {
+ if (checkAddressFormat(from)) {
+ return true;
+ }
+ throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', {
+ function: func,
+ });
+};
+
+export const getHeader = () => contentHeader;
+
+export const getFooter = () => contentFooter;
diff --git a/packages/rocketchat-mapview/client/index.js b/app/mapview/client/index.js
similarity index 100%
rename from packages/rocketchat-mapview/client/index.js
rename to app/mapview/client/index.js
diff --git a/packages/rocketchat-mapview/client/mapview.js b/app/mapview/client/mapview.js
similarity index 81%
rename from packages/rocketchat-mapview/client/mapview.js
rename to app/mapview/client/mapview.js
index bd6efc7962bf..a1695c1aaac6 100644
--- a/packages/rocketchat-mapview/client/mapview.js
+++ b/app/mapview/client/mapview.js
@@ -1,5 +1,6 @@
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
/*
* MapView is a named function that will replace geolocation in messages with a Google Static Map
* @param {Object} message - The message object
@@ -8,7 +9,7 @@ import { RocketChat } from 'meteor/rocketchat:lib';
function MapView(message) {
// get MapView settings
- const mv_googlekey = RocketChat.settings.get('MapView_GMapsAPIKey');
+ const mv_googlekey = settings.get('MapView_GMapsAPIKey');
if (message.location) {
@@ -26,4 +27,4 @@ function MapView(message) {
return message;
}
-RocketChat.callbacks.add('renderMessage', MapView, RocketChat.callbacks.priority.HIGH, 'mapview');
+callbacks.add('renderMessage', MapView, callbacks.priority.HIGH, 'mapview');
diff --git a/packages/rocketchat-mapview/server/index.js b/app/mapview/server/index.js
similarity index 100%
rename from packages/rocketchat-mapview/server/index.js
rename to app/mapview/server/index.js
diff --git a/app/mapview/server/settings.js b/app/mapview/server/settings.js
new file mode 100644
index 000000000000..0fb4ea239ac5
--- /dev/null
+++ b/app/mapview/server/settings.js
@@ -0,0 +1,7 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ settings.add('MapView_Enabled', false, { type: 'boolean', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_Enabled', i18nDescription: 'MapView_Enabled_Description' });
+ return settings.add('MapView_GMapsAPIKey', '', { type: 'string', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_GMapsAPIKey', i18nDescription: 'MapView_GMapsAPIKey_Description' });
+});
diff --git a/app/markdown/client/index.js b/app/markdown/client/index.js
new file mode 100644
index 000000000000..33d93bded040
--- /dev/null
+++ b/app/markdown/client/index.js
@@ -0,0 +1 @@
+export { Markdown } from '../lib/markdown';
diff --git a/app/markdown/lib/markdown.js b/app/markdown/lib/markdown.js
new file mode 100644
index 000000000000..5881a5f1bec5
--- /dev/null
+++ b/app/markdown/lib/markdown.js
@@ -0,0 +1,101 @@
+/*
+ * Markdown is a named function that will parse markdown syntax
+ * @param {Object} message - The message object
+ */
+import s from 'underscore.string';
+import { Meteor } from 'meteor/meteor';
+import { Blaze } from 'meteor/blaze';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { marked } from './parser/marked/marked.js';
+import { original } from './parser/original/original.js';
+
+import { code } from './parser/original/code.js';
+
+const parsers = {
+ original,
+ marked,
+};
+
+class MarkdownClass {
+ parse(text) {
+ const message = {
+ html: s.escapeHTML(text),
+ };
+ return this.mountTokensBack(this.parseMessageNotEscaped(message)).html;
+ }
+
+ parseNotEscaped(text) {
+ const message = {
+ html: text,
+ };
+ return this.mountTokensBack(this.parseMessageNotEscaped(message)).html;
+ }
+
+ parseMessageNotEscaped(message) {
+ const parser = settings.get('Markdown_Parser');
+
+ if (parser === 'disabled') {
+ return message;
+ }
+
+ if (typeof parsers[parser] === 'function') {
+ return parsers[parser](message);
+ }
+ return parsers.original(message);
+ }
+
+ mountTokensBackRecursively(message, tokenList, useHtml = true) {
+ const missingTokens = [];
+
+ if (tokenList.length > 0) {
+ for (const { token, text, noHtml } of tokenList) {
+ if (message.html.indexOf(token) >= 0) {
+ message.html = message.html.replace(token, () => (useHtml ? text : noHtml)); // Uses lambda so doesn't need to escape $
+ } else {
+ missingTokens.push({ token, text, noHtml });
+ }
+ }
+ }
+
+ // If there are tokens that were missing from the string, but the last iteration replaced at least one token, then go again
+ // this is done because one of the tokens may have been hidden by another one
+ if (missingTokens.length > 0 && missingTokens.length < tokenList.length) {
+ this.mountTokensBackRecursively(message, missingTokens, useHtml);
+ }
+ }
+
+ mountTokensBack(message, useHtml = true) {
+ if (message.tokens) {
+ this.mountTokensBackRecursively(message, message.tokens, useHtml);
+ }
+
+ return message;
+ }
+
+ code(...args) {
+ return code(...args);
+ }
+}
+
+export const Markdown = new MarkdownClass;
+
+// renderMessage already did html escape
+const MarkdownMessage = (message) => {
+ if (s.trim(message != null ? message.html : undefined)) {
+ message = Markdown.parseMessageNotEscaped(message);
+ }
+
+ return message;
+};
+
+callbacks.add('renderMessage', MarkdownMessage, callbacks.priority.HIGH, 'markdown');
+
+if (Meteor.isClient) {
+ Blaze.registerHelper('RocketChatMarkdown', (text) => Markdown.parse(text));
+ Blaze.registerHelper('RocketChatMarkdownUnescape', (text) => Markdown.parseNotEscaped(text));
+ Blaze.registerHelper('RocketChatMarkdownInline', (text) => {
+ const output = Markdown.parse(text);
+ return output.replace(/^
/, '').replace(/<\/p>$/, '');
+ });
+}
diff --git a/app/markdown/lib/parser/marked/marked.js b/app/markdown/lib/parser/marked/marked.js
new file mode 100644
index 000000000000..08577708a076
--- /dev/null
+++ b/app/markdown/lib/parser/marked/marked.js
@@ -0,0 +1,114 @@
+import { settings } from '../../../../settings';
+import { Random } from 'meteor/random';
+import _ from 'underscore';
+import s from 'underscore.string';
+import hljs from 'highlight.js';
+import _marked from 'marked';
+
+const renderer = new _marked.Renderer();
+
+let msg = null;
+
+renderer.code = function(code, lang, escaped) {
+ if (this.options.highlight) {
+ const out = this.options.highlight(code, lang);
+ if (out != null && out !== code) {
+ escaped = true;
+ code = out;
+ }
+ }
+
+ let text = null;
+
+ if (!lang) {
+ text = `
${ (escaped ? code : s.escapeHTML(code, true)) }
`;
+ } else {
+ text = `
${ (escaped ? code : s.escapeHTML(code, true)) }
`;
+ }
+
+ if (_.isString(msg)) {
+ return text;
+ }
+
+ const token = `=!=${ Random.id() }=!=`;
+ msg.tokens.push({
+ highlight: true,
+ token,
+ text,
+ });
+
+ return token;
+};
+
+renderer.codespan = function(text) {
+ text = `
${ text }
`;
+ if (_.isString(msg)) {
+ return text;
+ }
+
+ const token = `=!=${ Random.id() }=!=`;
+ msg.tokens.push({
+ token,
+ text,
+ });
+
+ return token;
+};
+
+renderer.blockquote = function(quote) {
+ return `
${ quote } `;
+};
+
+const linkRenderer = renderer.link;
+renderer.link = function(href, title, text) {
+ const html = linkRenderer.call(renderer, href, title, text);
+ return html.replace(/^
{
+ msg = message;
+
+ if (!msg.tokens) {
+ msg.tokens = [];
+ }
+
+ if (gfm == null) { gfm = settings.get('Markdown_Marked_GFM'); }
+ if (tables == null) { tables = settings.get('Markdown_Marked_Tables'); }
+ if (breaks == null) { breaks = settings.get('Markdown_Marked_Breaks'); }
+ if (pedantic == null) { pedantic = settings.get('Markdown_Marked_Pedantic'); }
+ if (smartLists == null) { smartLists = settings.get('Markdown_Marked_SmartLists'); }
+ if (smartypants == null) { smartypants = settings.get('Markdown_Marked_Smartypants'); }
+
+ msg.html = _marked(s.unescapeHTML(msg.html), {
+ gfm,
+ tables,
+ breaks,
+ pedantic,
+ smartLists,
+ smartypants,
+ renderer,
+ sanitize: true,
+ highlight,
+ });
+
+ return msg;
+};
diff --git a/packages/rocketchat-markdown/lib/parser/original/code.js b/app/markdown/lib/parser/original/code.js
similarity index 100%
rename from packages/rocketchat-markdown/lib/parser/original/code.js
rename to app/markdown/lib/parser/original/code.js
diff --git a/app/markdown/lib/parser/original/markdown.js b/app/markdown/lib/parser/original/markdown.js
new file mode 100644
index 000000000000..7ac8f4f20647
--- /dev/null
+++ b/app/markdown/lib/parser/original/markdown.js
@@ -0,0 +1,95 @@
+/*
+ * Markdown is a named function that will parse markdown syntax
+ * @param {String} msg - The message html
+ */
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { settings } from '../../../../settings';
+import s from 'underscore.string';
+
+const parseNotEscaped = function(msg, message) {
+ if (message && message.tokens == null) {
+ message.tokens = [];
+ }
+
+ const addAsToken = function(html) {
+ const token = `=!=${ Random.id() }=!=`;
+ message.tokens.push({
+ token,
+ text: html,
+ });
+
+ return token;
+ };
+
+ const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|');
+
+ if (settings.get('Markdown_Headers')) {
+ // Support # Text for h1
+ msg = msg.replace(/^# (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$1 ');
+
+ // Support # Text for h2
+ msg = msg.replace(/^## (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$1 ');
+
+ // Support # Text for h3
+ msg = msg.replace(/^### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$1 ');
+
+ // Support # Text for h4
+ msg = msg.replace(/^#### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$1 ');
+ }
+
+ // Support *text* to make bold
+ msg = msg.replace(/(|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1* $2 * $3');
+
+ // Support _text_ to make italics
+ msg = msg.replace(/(^|>|[ >*~`])\_{1,2}([^\_\r\n]+)\_{1,2}([<*~`]|\B|\b|$)/gm, '$1_ $2 _ $3');
+
+ // Support ~text~ to strike through text
+ msg = msg.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1~ $2 ~ $3');
+
+ // Support for block quote
+ // >>>
+ // Text
+ // <<<
+ msg = msg.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '>>> $1<<< ');
+
+ // Support >Text for quote
+ msg = msg.replace(/^>(.*)$/gm, '> $1 ');
+
+ // Remove white-space around blockquote (prevent ). Because blockquote is block element.
+ msg = msg.replace(/\s*/gm, '');
+ msg = msg.replace(/<\/blockquote>\s*/gm, ' ');
+
+ // Remove new-line between blockquotes.
+ msg = msg.replace(/<\/blockquote>\n {
+ const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank';
+ return addAsToken(`
`);
+ });
+
+ // Support [Text](http://link)
+ msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), (match, title, url) => {
+ const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank';
+ title = title.replace(/&/g, '&');
+
+ let escapedUrl = s.escapeHTML(url);
+ escapedUrl = escapedUrl.replace(/&/g, '&');
+
+ return addAsToken(`${ s.escapeHTML(title) } `);
+ });
+
+ // Support
+ msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => {
+ const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank';
+ return addAsToken(`${ s.escapeHTML(title) } `);
+ });
+
+ return msg;
+};
+
+export const markdown = function(message) {
+ message.html = parseNotEscaped(message.html, message);
+ return message;
+};
diff --git a/packages/rocketchat-markdown/lib/parser/original/original.js b/app/markdown/lib/parser/original/original.js
similarity index 100%
rename from packages/rocketchat-markdown/lib/parser/original/original.js
rename to app/markdown/lib/parser/original/original.js
diff --git a/app/markdown/server/index.js b/app/markdown/server/index.js
new file mode 100644
index 000000000000..f4d1855ab52a
--- /dev/null
+++ b/app/markdown/server/index.js
@@ -0,0 +1,2 @@
+import './settings';
+export { Markdown } from '../lib/markdown';
diff --git a/app/markdown/server/settings.js b/app/markdown/server/settings.js
new file mode 100644
index 000000000000..14d16c7e7746
--- /dev/null
+++ b/app/markdown/server/settings.js
@@ -0,0 +1,88 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(() => {
+ settings.add('Markdown_Parser', 'original', {
+ type: 'select',
+ values: [{
+ key: 'disabled',
+ i18nLabel: 'Disabled',
+ }, {
+ key: 'original',
+ i18nLabel: 'Original',
+ }, {
+ key: 'marked',
+ i18nLabel: 'Marked',
+ }],
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ });
+
+ const enableQueryOriginal = { _id: 'Markdown_Parser', value: 'original' };
+ settings.add('Markdown_Headers', false, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryOriginal,
+ });
+ settings.add('Markdown_SupportSchemesForLink', 'http,https', {
+ type: 'string',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ i18nDescription: 'Markdown_SupportSchemesForLink_Description',
+ enableQuery: enableQueryOriginal,
+ });
+
+ const enableQueryMarked = { _id: 'Markdown_Parser', value: 'marked' };
+ settings.add('Markdown_Marked_GFM', true, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryMarked,
+ });
+ settings.add('Markdown_Marked_Tables', true, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryMarked,
+ });
+ settings.add('Markdown_Marked_Breaks', true, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryMarked,
+ });
+ settings.add('Markdown_Marked_Pedantic', false, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: [{
+ _id: 'Markdown_Parser',
+ value: 'marked',
+ }, {
+ _id: 'Markdown_Marked_GFM',
+ value: false,
+ }],
+ });
+ settings.add('Markdown_Marked_SmartLists', true, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryMarked,
+ });
+ settings.add('Markdown_Marked_Smartypants', true, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'Markdown',
+ public: true,
+ enableQuery: enableQueryMarked,
+ });
+});
diff --git a/app/markdown/tests/client.mocks.js b/app/markdown/tests/client.mocks.js
new file mode 100644
index 000000000000..497763873795
--- /dev/null
+++ b/app/markdown/tests/client.mocks.js
@@ -0,0 +1,60 @@
+import mock from 'mock-require';
+import _ from 'underscore';
+import s from 'underscore.string';
+_.mixin(s.exports());
+
+mock('meteor/meteor', {
+ Meteor: {
+ absoluteUrl() {
+ return 'http://localhost:3000/';
+ },
+ },
+});
+
+mock('meteor/blaze', {
+ Blaze: {},
+});
+
+mock('../../settings', {
+ settings: {
+ get(setting) {
+ switch (setting) {
+ case 'Markdown_SupportSchemesForLink':
+ return 'http,https';
+ case 'Markdown_Parser':
+ return 'original';
+ case 'Markdown_Headers':
+ // case 'Markdown_Marked_GFM':
+ // case 'Markdown_Marked_Tables':
+ // case 'Markdown_Marked_Breaks':
+ // case 'Markdown_Marked_Pedantic':
+ // case 'Markdown_Marked_SmartLists':
+ // case 'Markdown_Marked_Smartypants':
+ return true;
+ default:
+ throw new Error(`Missing setting mock ${ setting }`);
+ }
+ },
+ },
+});
+
+mock('../../callbacks', {
+ callbacks: {
+ add() {
+
+ },
+ priority: {
+ HIGH: 1,
+ },
+ },
+});
+
+mock('meteor/random', {
+ Random: {
+ id() {
+ return Math.random();
+ },
+ },
+});
+
+global.s = s;
diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js
new file mode 100644
index 000000000000..f6f5a2495c3f
--- /dev/null
+++ b/app/markdown/tests/client.tests.js
@@ -0,0 +1,291 @@
+/* eslint-env mocha */
+import 'babel-polyfill';
+import assert from 'assert';
+import s from 'underscore.string';
+import './client.mocks.js';
+import { original } from '../lib/parser/original/original';
+import { Markdown } from '../lib/markdown';
+
+const wrapper = (text, tag) => `${ tag } ${ text }${ tag } `;
+const boldWrapper = (text) => wrapper(`${ text } `, '*');
+const italicWrapper = (text) => wrapper(`${ text } `, '_');
+const strikeWrapper = (text) => wrapper(`${ text } `, '~');
+const headerWrapper = (text, level) => `${ text } `;
+const quoteWrapper = (text) => `> ${ text } `;
+const linkWrapped = (link, title) => `${ s.escapeHTML(title) } `;
+const inlinecodeWrapper = (text) => wrapper(`${ text }
`, '`');
+const codeWrapper = (text, lang) => `\`\`\` ${ text } \`\`\`
`;
+
+const bold = {
+ '*Hello*': boldWrapper('Hello'),
+ '**Hello**': boldWrapper('Hello'),
+ '**Hello*': boldWrapper('Hello'),
+ '*Hello**': boldWrapper('Hello'),
+ Hello: 'Hello',
+ '*Hello': '*Hello',
+ 'Hello*': 'Hello*',
+ 'He*llo': 'He*llo',
+ '***Hello***': `*${ boldWrapper('Hello') }*`,
+ '***Hello**': `*${ boldWrapper('Hello') }`,
+ '*Hello* this is dog': `${ boldWrapper('Hello') } this is dog`,
+ 'Rocket cat says *Hello*': `Rocket cat says ${ boldWrapper('Hello') }`,
+ 'He said *Hello* to her': `He said ${ boldWrapper('Hello') } to her`,
+ '**Hello** this is dog': `${ boldWrapper('Hello') } this is dog`,
+ 'Rocket cat says **Hello**': `Rocket cat says ${ boldWrapper('Hello') }`,
+ 'He said **Hello** to her': `He said ${ boldWrapper('Hello') } to her`,
+ 'He was a**nn**oyed': `He was a${ boldWrapper('nn') }oyed`,
+ 'There are two o in f*oo*tball': `There are two o in f${ boldWrapper('oo') }tball`,
+};
+
+const italic = {
+ _Hello_: italicWrapper('Hello'),
+ __Hello__: italicWrapper('Hello'),
+ __Hello_: italicWrapper('Hello'),
+ _Hello__: italicWrapper('Hello'),
+ Hello: 'Hello',
+ _Hello: '_Hello',
+ Hello_: 'Hello_',
+ He_llo: 'He_llo',
+ ___Hello___: '___Hello___',
+ ___Hello__: '___Hello__',
+ '_Hello_ this is dog': `${ italicWrapper('Hello') } this is dog`,
+ 'Rocket cat says _Hello_': `Rocket cat says ${ italicWrapper('Hello') }`,
+ 'He said _Hello_ to her': `He said ${ italicWrapper('Hello') } to her`,
+ '__Hello__ this is dog': `${ italicWrapper('Hello') } this is dog`,
+ 'Rocket cat says __Hello__': `Rocket cat says ${ italicWrapper('Hello') }`,
+ 'He said __Hello__ to her': `He said ${ italicWrapper('Hello') } to her`,
+};
+
+const strike = {
+ '~Hello~': strikeWrapper('Hello'),
+ '~~Hello~~': strikeWrapper('Hello'),
+ '~~Hello~': strikeWrapper('Hello'),
+ '~Hello~~': strikeWrapper('Hello'),
+ Hello: 'Hello',
+ '~Hello': '~Hello',
+ 'Hello~': 'Hello~',
+ 'He~llo': 'He~llo',
+ '~~~Hello~~~': '~~~Hello~~~',
+ '~~~Hello~~': '~~~Hello~~',
+ '~Hello~ this is dog': `${ strikeWrapper('Hello') } this is dog`,
+ 'Rocket cat says ~Hello~': `Rocket cat says ${ strikeWrapper('Hello') }`,
+ 'He said ~Hello~ to her': `He said ${ strikeWrapper('Hello') } to her`,
+ '~~Hello~~ this is dog': `${ strikeWrapper('Hello') } this is dog`,
+ 'Rocket cat says ~~Hello~~': `Rocket cat says ${ strikeWrapper('Hello') }`,
+ 'He said ~~Hello~~ to her': `He said ${ strikeWrapper('Hello') } to her`,
+};
+
+const headersLevel1 = {
+ '# Hello': headerWrapper('Hello', 1),
+ '# Rocket.Cat': headerWrapper('Rocket.Cat', 1),
+ '# Hi': headerWrapper('Hi', 1),
+ '# Hello this is dog': headerWrapper('Hello this is dog', 1),
+ '# Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 1),
+ '# He said Hello to her': headerWrapper('He said Hello to her', 1),
+ '#Hello': '#Hello',
+ '#Hello#': '#Hello#',
+ 'He#llo': 'He#llo',
+};
+
+const headersLevel2 = {
+ '## Hello': headerWrapper('Hello', 2),
+ '## Rocket.Cat': headerWrapper('Rocket.Cat', 2),
+ '## Hi': headerWrapper('Hi', 2),
+ '## Hello this is dog': headerWrapper('Hello this is dog', 2),
+ '## Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 2),
+ '## He said Hello to her': headerWrapper('He said Hello to her', 2),
+ '##Hello': '##Hello',
+ '##Hello##': '##Hello##',
+ 'He##llo': 'He##llo',
+};
+
+const headersLevel3 = {
+ '### Hello': headerWrapper('Hello', 3),
+ '### Rocket.Cat': headerWrapper('Rocket.Cat', 3),
+ '### Hi': headerWrapper('Hi', 3),
+ '### Hello this is dog': headerWrapper('Hello this is dog', 3),
+ '### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 3),
+ '### He said Hello to her': headerWrapper('He said Hello to her', 3),
+ '###Hello': '###Hello',
+ '###Hello###': '###Hello###',
+ 'He###llo': 'He###llo',
+};
+
+const headersLevel4 = {
+ '#### Hello': headerWrapper('Hello', 4),
+ '#### Rocket.Cat': headerWrapper('Rocket.Cat', 4),
+ '#### Hi': headerWrapper('Hi', 4),
+ '#### Hello this is dog': headerWrapper('Hello this is dog', 4),
+ '#### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 4),
+ '#### He said Hello to her': headerWrapper('He said Hello to her', 4),
+ '####Hello': '####Hello',
+ '####Hello####': '####Hello####',
+ 'He####llo': 'He####llo',
+};
+
+const quote = {
+ '>Hello': s.escapeHTML('>Hello'),
+ '>Rocket.Cat': s.escapeHTML('>Rocket.Cat'),
+ '>Hi': s.escapeHTML('>Hi'),
+ '> Hello this is dog': s.escapeHTML('> Hello this is dog'),
+ '> Rocket cat says Hello': s.escapeHTML('> Rocket cat says Hello'),
+ '> He said Hello to her': s.escapeHTML('> He said Hello to her'),
+ '> He said Hello to her ': s.escapeHTML('> He said Hello to her '),
+ '<Hello': s.escapeHTML('<Hello'),
+ '<Rocket.Cat>': s.escapeHTML('<Rocket.Cat>'),
+ ' >Hi': s.escapeHTML(' >Hi'),
+ 'Hello > this is dog': s.escapeHTML('Hello > this is dog'),
+ 'Roc>ket cat says Hello': s.escapeHTML('Roc>ket cat says Hello'),
+ 'He said Hello to her>': s.escapeHTML('He said Hello to her>'),
+ '>Hello': quoteWrapper('Hello'),
+ '>Rocket.Cat': quoteWrapper('Rocket.Cat'),
+ '>Hi': quoteWrapper('Hi'),
+ '> Hello this is dog': quoteWrapper(' Hello this is dog'),
+ '> Rocket cat says Hello': quoteWrapper(' Rocket cat says Hello'),
+ '> He said Hello to her': quoteWrapper(' He said Hello to her'),
+ '': s.escapeHTML(''),
+ ' >Hi': s.escapeHTML(' >Hi'),
+ 'Hello > this is dog': s.escapeHTML('Hello > this is dog'),
+ 'Roc>ket cat says Hello': s.escapeHTML('Roc>ket cat says Hello'),
+ 'He said Hello to her>': s.escapeHTML('He said Hello to her>'),
+};
+
+const link = {
+ '<http://link|Text>': s.escapeHTML('<http://link|Text>'),
+ '<https://open.rocket.chat/|Open Site For Rocket.Chat>': s.escapeHTML('<https://open.rocket.chat/|Open Site For Rocket.Chat>'),
+ '<https://open.rocket.chat/ | Open Site For Rocket.Chat>': s.escapeHTML('<https://open.rocket.chat/ | Open Site For Rocket.Chat>'),
+ '<https://rocket.chat/|Rocket.Chat Site>': s.escapeHTML('<https://rocket.chat/|Rocket.Chat Site>'),
+ '<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': s.escapeHTML('<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>'),
+ '<http://linkText>': s.escapeHTML('<http://linkText>'),
+ '<https:open.rocket.chat/ | Open Site For Rocket.Chat>': s.escapeHTML('<https:open.rocket.chat/ | Open Site For Rocket.Chat>'),
+ 'https://open.rocket.chat/|Open Site For Rocket.Chat': s.escapeHTML('https://open.rocket.chat/|Open Site For Rocket.Chat'),
+ '<www.open.rocket.chat/|Open Site For Rocket.Chat>': s.escapeHTML('<www.open.rocket.chat/|Open Site For Rocket.Chat>'),
+ '<htps://rocket.chat/|Rocket.Chat Site>': s.escapeHTML('<htps://rocket.chat/|Rocket.Chat Site>'),
+ '<ttps://rocket.chat/|Rocket.Chat Site>': s.escapeHTML('<ttps://rocket.chat/|Rocket.Chat Site>'),
+ '<tps://rocket.chat/|Rocket.Chat Site>': s.escapeHTML('<tps://rocket.chat/|Rocket.Chat Site>'),
+ '<open.rocket.chat/|Open Site For Rocket.Chat>': s.escapeHTML('<open.rocket.chat/|Open Site For Rocket.Chat>'),
+ '<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': s.escapeHTML('<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>'),
+
+ '': linkWrapped('http://link', 'Text'),
+ '': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
+ '': linkWrapped('https://open.rocket.chat/ ', ' Open Site For Rocket.Chat'),
+ '': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
+ '': linkWrapped('https://rocket.chat/docs/developer-guides/testing/#testing', 'Testing Entry on Rocket.Chat Docs Site'),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+ '': s.escapeHTML(''),
+
+ '[Text](http://link)': linkWrapped('http://link', 'Text'),
+ '[Open Site For Rocket.Chat](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
+ '[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': linkWrapped('https://open.rocket.chat/ ', ' Open Site For Rocket.Chat'),
+ '[Rocket.Chat Site](https://rocket.chat/)': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
+ '[Testing Entry on Rocket.Chat Docs Site](https://rocket.chat/docs/developer-guides/testing/#testing)': linkWrapped('https://rocket.chat/docs/developer-guides/testing/#testing', 'Testing Entry on Rocket.Chat Docs Site'),
+ '[](http://linkText)': '[](http://linkText)',
+ '[text]': '[text]',
+ '[Open Site For Rocket.Chat](https:open.rocket.chat/)': '[Open Site For Rocket.Chat](https:open.rocket.chat/)',
+ '[Open Site For Rocket.Chat](www.open.rocket.chat/)': '[Open Site For Rocket.Chat](www.open.rocket.chat/)',
+ '[Rocket.Chat Site](htps://rocket.chat/)': '[Rocket.Chat Site](htps://rocket.chat/)',
+ '[Rocket.Chat Site](ttps://rocket.chat/)': '[Rocket.Chat Site](ttps://rocket.chat/)',
+ '[Rocket.Chat Site](tps://rocket.chat/)': '[Rocket.Chat Site](tps://rocket.chat/)',
+ '[Open Site For Rocket.Chat](open.rocket.chat/)': '[Open Site For Rocket.Chat](open.rocket.chat/)',
+ '[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)': '[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)',
+ '[Text](http://link?param1=1¶m2=2)': linkWrapped('http://link?param1=1¶m2=2', 'Text'),
+};
+
+const inlinecode = {
+ '`code`': inlinecodeWrapper('code'),
+ '`code` begin': `${ inlinecodeWrapper('code') } begin`,
+ 'End `code`': `End ${ inlinecodeWrapper('code') }`,
+ 'Middle `code` middle': `Middle ${ inlinecodeWrapper('code') } middle`,
+ '`code`begin': `${ inlinecodeWrapper('code') }begin`,
+ 'End`code`': `End${ inlinecodeWrapper('code') }`,
+ 'Middle`code`middle': `Middle${ inlinecodeWrapper('code') }middle`,
+};
+
+const code = {
+ '```code```': codeWrapper('code ', 'clean'),
+ '```code': codeWrapper('code \n', 'stylus'),
+ '```code\n': codeWrapper('code \n', 'stylus'),
+ '```\ncode\n```': codeWrapper('code \n', 'stylus'),
+ '```code\n```': codeWrapper('code \n', 'stylus'),
+ '```\ncode```': codeWrapper('code ', 'clean'),
+ '```javascript\nvar a = \'log\';\nconsole.log(a);```': codeWrapper('var a = \'log\' ;\nconsole .log(a);', 'javascript'),
+ '```*code*```': codeWrapper('*code *', 'armasm'),
+ '```**code**```': codeWrapper('**code **', 'armasm'),
+ '```_code_```': codeWrapper('_code_ ', 'sqf'),
+ '```__code__```': codeWrapper('__code__ ', 'markdown'),
+};
+
+const nested = {
+ '> some quote\n`window.location.reload();`': `${ quoteWrapper(' some quote') }${ inlinecodeWrapper('window.location.reload();') }`,
+};
+
+const defaultObjectTest = (result, object, objectKey) => assert.equal(result.html, object[objectKey]);
+
+const testObject = (object, parser = original, test = defaultObjectTest) => {
+ Object.keys(object).forEach((objectKey) => {
+ describe(objectKey, () => {
+ const message = {
+ html: s.escapeHTML(objectKey),
+ };
+ const result = Markdown.mountTokensBack(parser(message));
+ it(`should be equal to ${ object[objectKey] }`, () => {
+ test(result, object, objectKey);
+ });
+ });
+ });
+};
+
+describe('Original', function() {
+ describe('Bold', () => testObject(bold));
+
+ describe('Italic', () => testObject(italic));
+
+ describe('Strike', () => testObject(strike));
+
+ describe('Headers', () => {
+ describe('Level 1', () => testObject(headersLevel1));
+
+ describe('Level 2', () => testObject(headersLevel2));
+
+ describe('Level 3', () => testObject(headersLevel3));
+
+ describe('Level 4', () => testObject(headersLevel4));
+ });
+
+ describe('Quote', () => testObject(quote));
+
+ describe('Link', () => testObject(link));
+
+ describe('Inline Code', () => testObject(inlinecode));
+
+ describe('Code', () => testObject(code));
+
+ describe('Nested', () => testObject(nested));
+});
+
+// describe.only('Marked', function() {
+// describe('Bold', () => testObject(bold, marked));
+
+// describe('Italic', () => testObject(italic, marked));
+
+// describe('Strike', () => testObject(strike, marked));
+
+// describe('Headers', () => {
+// describe('Level 1', () => testObject(headersLevel1, marked));
+
+// describe('Level 2', () => testObject(headersLevel2, marked));
+
+// describe('Level 3', () => testObject(headersLevel3, marked));
+
+// describe('Level 4', () => testObject(headersLevel4, marked));
+// });
+
+// describe('Quote', () => testObject(quote, marked));
+// });
diff --git a/app/mentions-flextab/client/actionButton.js b/app/mentions-flextab/client/actionButton.js
new file mode 100644
index 000000000000..63eca8abccb6
--- /dev/null
+++ b/app/mentions-flextab/client/actionButton.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { MessageAction, RoomHistoryManager } from '../../ui-utils';
+import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+
+Meteor.startup(function() {
+ MessageAction.addButton({
+ id: 'jump-to-message',
+ icon: 'jump',
+ label: 'Jump_to_message',
+ context: ['mentions', 'threads'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ if (window.matchMedia('(max-width: 500px)').matches) {
+ Template.instance().tabBar.close();
+ }
+ RoomHistoryManager.getSurroundingMessages(message, 50);
+ },
+ order: 100,
+ group: 'menu',
+ });
+});
diff --git a/packages/rocketchat-mentions-flextab/client/index.js b/app/mentions-flextab/client/index.js
similarity index 100%
rename from packages/rocketchat-mentions-flextab/client/index.js
rename to app/mentions-flextab/client/index.js
diff --git a/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js b/app/mentions-flextab/client/lib/MentionedMessage.js
similarity index 100%
rename from packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js
rename to app/mentions-flextab/client/lib/MentionedMessage.js
diff --git a/app/mentions-flextab/client/tabBar.js b/app/mentions-flextab/client/tabBar.js
new file mode 100644
index 000000000000..07bd5de84803
--- /dev/null
+++ b/app/mentions-flextab/client/tabBar.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { TabBar } from '../../ui-utils';
+
+Meteor.startup(function() {
+ return TabBar.addButton({
+ groups: ['channel', 'group'],
+ id: 'mentions',
+ i18nTitle: 'Mentions',
+ icon: 'at',
+ template: 'mentionsFlexTab',
+ order: 3,
+ });
+});
diff --git a/app/mentions-flextab/client/views/mentionsFlexTab.html b/app/mentions-flextab/client/views/mentionsFlexTab.html
new file mode 100644
index 000000000000..bf8413811dff
--- /dev/null
+++ b/app/mentions-flextab/client/views/mentionsFlexTab.html
@@ -0,0 +1,21 @@
+
+ {{#if Template.subscriptionsReady}}
+ {{#unless hasMessages}}
+
+ {{/unless}}
+ {{/if}}
+
+
+ {{# with messageContext}}
+ {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
+ {{/with}}
+
+ {{#if hasMore}}
+
+ {{> loading}}
+
+ {{/if}}
+
+
diff --git a/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js b/app/mentions-flextab/client/views/mentionsFlexTab.js
similarity index 80%
rename from packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js
rename to app/mentions-flextab/client/views/mentionsFlexTab.js
index 0cd0b4c8fe17..24268b57ac94 100644
--- a/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js
+++ b/app/mentions-flextab/client/views/mentionsFlexTab.js
@@ -2,25 +2,13 @@ import _ from 'underscore';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { MentionedMessage } from '../lib/MentionedMessage';
-
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
Template.mentionsFlexTab.helpers({
hasMessages() {
- return MentionedMessage.find({
- rid: this.rid,
- }, {
- sort: {
- ts: -1,
- },
- }).count() > 0;
+ return Template.instance().cursor.count() > 0;
},
messages() {
- return MentionedMessage.find({
- rid: this.rid,
- }, {
- sort: {
- ts: -1,
- },
- });
+ return Template.instance().cursor;
},
message() {
return _.extend(this, { customClass: 'mentions', actionContext: 'mentions' });
@@ -28,9 +16,17 @@ Template.mentionsFlexTab.helpers({
hasMore() {
return Template.instance().hasMore.get();
},
+ messageContext,
});
Template.mentionsFlexTab.onCreated(function() {
+ this.cursor = MentionedMessage.find({
+ rid: this.data.rid,
+ }, {
+ sort: {
+ ts: -1,
+ },
+ });
this.hasMore = new ReactiveVar(true);
this.limit = new ReactiveVar(50);
return this.autorun(() => {
diff --git a/packages/rocketchat-mentions-flextab/server/index.js b/app/mentions-flextab/server/index.js
similarity index 100%
rename from packages/rocketchat-mentions-flextab/server/index.js
rename to app/mentions-flextab/server/index.js
diff --git a/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js b/app/mentions-flextab/server/publications/mentionedMessages.js
similarity index 78%
rename from packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js
rename to app/mentions-flextab/server/publications/mentionedMessages.js
index c26d68c4b598..fcf047545e7b 100644
--- a/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js
+++ b/app/mentions-flextab/server/publications/mentionedMessages.js
@@ -1,19 +1,19 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Users, Messages } from '../../../models';
Meteor.publish('mentionedMessages', function(rid, limit = 50) {
if (!this.userId) {
return this.ready();
}
const publication = this;
- const user = RocketChat.models.Users.findOneById(this.userId);
+ const user = Users.findOneById(this.userId);
if (!user) {
return this.ready();
}
if (!Meteor.call('canAccessRoom', rid, this.userId)) {
return this.ready();
}
- const cursorHandle = RocketChat.models.Messages.findVisibleByMentionAndRoomId(user.username, rid, {
+ const cursorHandle = Messages.findVisibleByMentionAndRoomId(user.username, rid, {
sort: {
ts: -1,
},
diff --git a/app/mentions/client/client.js b/app/mentions/client/client.js
new file mode 100644
index 000000000000..7030dcdf18a4
--- /dev/null
+++ b/app/mentions/client/client.js
@@ -0,0 +1,27 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { callbacks } from '../../callbacks';
+import { settings } from '../../settings';
+import { Users } from '../../models/client';
+import { MentionsParser } from '../lib/MentionsParser';
+
+let me;
+let useRealName;
+let pattern;
+
+Meteor.startup(() => Tracker.autorun(() => {
+ const uid = Meteor.userId();
+ me = uid && (Users.findOne(uid, { fields: { username: 1 } }) || {}).username;
+ pattern = settings.get('UTF8_Names_Validation');
+ useRealName = settings.get('UI_Use_Real_Name');
+}));
+
+
+const instance = new MentionsParser({
+ pattern: () => pattern,
+ useRealName: () => useRealName,
+ me: () => me,
+});
+
+callbacks.add('renderMessage', (message) => instance.parse(message), callbacks.priority.MEDIUM, 'mentions-message');
+callbacks.add('renderMentions', (message) => instance.parse(message), callbacks.priority.MEDIUM, 'mentions-mentions');
diff --git a/app/mentions/client/index.js b/app/mentions/client/index.js
new file mode 100644
index 000000000000..c9353c892c3c
--- /dev/null
+++ b/app/mentions/client/index.js
@@ -0,0 +1,2 @@
+import './client';
+import './mentionLink.css';
diff --git a/app/mentions/client/mentionLink.css b/app/mentions/client/mentionLink.css
new file mode 100644
index 000000000000..70c1be80838d
--- /dev/null
+++ b/app/mentions/client/mentionLink.css
@@ -0,0 +1,37 @@
+.message .mention-link,
+.mention-link {
+ padding: 0 6px 2px;
+
+ transition: opacity 0.3s, background-color 0.3s, color 0.3s;
+
+ color: var(--mention-link-text-color);
+
+ border-radius: var(--mention-link-radius);
+
+ background-color: var(--mention-link-background);
+
+ font-weight: 700;
+
+ &:hover {
+ opacity: 0.6;
+ color: var(--mention-link-text-color);
+ }
+
+ &--me {
+ color: var(--mention-link-me-text-color);
+ background-color: var(--mention-link-me-background);
+
+ &:hover {
+ color: var(--mention-link-me-text-color);
+ }
+ }
+
+ &--group {
+ color: var(--mention-link-group-text-color);
+ background-color: var(--mention-link-group-background);
+
+ &:hover {
+ color: var(--mention-link-group-text-color);
+ }
+ }
+}
diff --git a/app/mentions/lib/MentionsParser.js b/app/mentions/lib/MentionsParser.js
new file mode 100644
index 000000000000..0921f66e2b3b
--- /dev/null
+++ b/app/mentions/lib/MentionsParser.js
@@ -0,0 +1,109 @@
+import s from 'underscore.string';
+
+export class MentionsParser {
+ constructor({ pattern, useRealName, me }) {
+ this.pattern = pattern;
+ this.useRealName = useRealName;
+ this.me = me;
+ }
+
+ set me(m) {
+ this._me = m;
+ }
+
+ get me() {
+ return typeof this._me === 'function' ? this._me() : this._me;
+ }
+
+ set pattern(p) {
+ this._pattern = p;
+ }
+
+ get pattern() {
+ return typeof this._pattern === 'function' ? this._pattern() : this._pattern;
+ }
+
+ set useRealName(s) {
+ this._useRealName = s;
+ }
+
+ get useRealName() {
+ return typeof this._useRealName === 'function' ? this._useRealName() : this._useRealName;
+ }
+
+ get userMentionRegex() {
+ return new RegExp(`(^|\\s|| ?)@(${ this.pattern }(@(${ this.pattern }))?)`, 'gm');
+ }
+
+ get channelMentionRegex() {
+ return new RegExp(`(^|\\s|
)#(${ this.pattern }(@(${ this.pattern }))?)`, 'gm');
+ }
+
+ replaceUsers = (msg, { mentions, temp }, me) => msg
+ .replace(this.userMentionRegex, (match, prefix, mention) => {
+ const classNames = ['mention-link'];
+
+ if (mention === 'all') {
+ classNames.push('mention-link--all');
+ classNames.push('mention-link--group');
+ } else if (mention === 'here') {
+ classNames.push('mention-link--here');
+ classNames.push('mention-link--group');
+ } else if (mention === me) {
+ classNames.push('mention-link--me');
+ classNames.push('mention-link--user');
+ } else {
+ classNames.push('mention-link--user');
+ }
+
+ const className = classNames.join(' ');
+
+ if (mention === 'all' || mention === 'here') {
+ return `${ prefix }${ mention } `;
+ }
+
+ const label = temp ?
+ mention && s.escapeHTML(mention) :
+ (mentions || [])
+ .filter(({ username }) => username === mention)
+ .map(({ name, username }) => (this.useRealName ? name : username))
+ .map((label) => label && s.escapeHTML(label))[0];
+
+ if (!label) {
+ return match;
+ }
+
+ return `${ prefix }${ label } `;
+ })
+
+ replaceChannels = (msg, { temp, channels }) => msg
+ .replace(/'/g, '\'')
+ .replace(this.channelMentionRegex, (match, prefix, mention) => {
+ if (!temp && !(channels && channels.find((c) => c.name === mention))) {
+ return match;
+ }
+
+ const channel = channels && channels.find(({ name }) => name === mention);
+ const reference = channel ? channel._id : mention;
+ return `${ prefix }${ `#${ mention }` } `;
+ })
+
+ getUserMentions(str) {
+ return (str.match(this.userMentionRegex) || []).map((match) => match.trim());
+ }
+
+ getChannelMentions(str) {
+ return (str.match(this.channelMentionRegex) || []).map((match) => match.trim());
+ }
+
+ parse(message) {
+ let msg = (message && message.html) || '';
+ if (!msg.trim()) {
+ return message;
+ }
+ msg = this.replaceUsers(msg, message, this.me);
+ msg = this.replaceChannels(msg, message, this.me);
+ message.html = msg;
+ return message;
+ }
+}
diff --git a/app/mentions/server/Mentions.js b/app/mentions/server/Mentions.js
new file mode 100644
index 000000000000..0d03dfdcc01c
--- /dev/null
+++ b/app/mentions/server/Mentions.js
@@ -0,0 +1,76 @@
+/*
+* Mentions is a named function that will process Mentions
+* @param {Object} message - The message object
+*/
+import { MentionsParser } from '../lib/MentionsParser';
+
+export default class MentionsServer extends MentionsParser {
+ constructor(args) {
+ super(args);
+ this.messageMaxAll = args.messageMaxAll;
+ this.getChannel = args.getChannel;
+ this.getChannels = args.getChannels;
+ this.getUsers = args.getUsers;
+ this.getUser = args.getUser;
+ this.getTotalChannelMembers = args.getTotalChannelMembers;
+ this.onMaxRoomMembersExceeded = args.onMaxRoomMembersExceeded || (() => {});
+ }
+ set getUsers(m) {
+ this._getUsers = m;
+ }
+ get getUsers() {
+ return typeof this._getUsers === 'function' ? this._getUsers : () => this._getUsers;
+ }
+ set getChannels(m) {
+ this._getChannels = m;
+ }
+ get getChannels() {
+ return typeof this._getChannels === 'function' ? this._getChannels : () => this._getChannels;
+ }
+ set getChannel(m) {
+ this._getChannel = m;
+ }
+ get getChannel() {
+ return typeof this._getChannel === 'function' ? this._getChannel : () => this._getChannel;
+ }
+ set messageMaxAll(m) {
+ this._messageMaxAll = m;
+ }
+ get messageMaxAll() {
+ return typeof this._messageMaxAll === 'function' ? this._messageMaxAll() : this._messageMaxAll;
+ }
+ getUsersByMentions({ msg, rid, u: sender }) {
+ let mentions = this.getUserMentions(msg);
+ const mentionsAll = [];
+ const userMentions = [];
+
+ mentions.forEach((m) => {
+ const mention = m.trim().substr(1);
+ if (mention !== 'all' && mention !== 'here') {
+ return userMentions.push(mention);
+ }
+ if (this.messageMaxAll > 0 && this.getTotalChannelMembers(rid) > this.messageMaxAll) {
+ return this.onMaxRoomMembersExceeded({ sender, rid });
+ }
+ mentionsAll.push({
+ _id: mention,
+ username: mention,
+ });
+ });
+ mentions = userMentions.length ? this.getUsers(userMentions) : [];
+ return [...mentionsAll, ...mentions];
+ }
+ getChannelbyMentions({ msg }) {
+ const channels = this.getChannelMentions(msg);
+ return this.getChannels(channels.map((c) => c.trim().substr(1)));
+ }
+ execute(message) {
+ const mentionsAll = this.getUsersByMentions(message);
+ const channels = this.getChannelbyMentions(message);
+
+ message.mentions = mentionsAll;
+ message.channels = channels;
+
+ return message;
+ }
+}
diff --git a/packages/rocketchat-mentions/server/index.js b/app/mentions/server/index.js
similarity index 100%
rename from packages/rocketchat-mentions/server/index.js
rename to app/mentions/server/index.js
diff --git a/app/mentions/server/methods/getUserMentionsByChannel.js b/app/mentions/server/methods/getUserMentionsByChannel.js
new file mode 100644
index 000000000000..d648ed811d05
--- /dev/null
+++ b/app/mentions/server/methods/getUserMentionsByChannel.js
@@ -0,0 +1,23 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { Rooms, Users, Messages } from '../../../models';
+
+Meteor.methods({
+ getUserMentionsByChannel({ roomId, options }) {
+ check(roomId, String);
+
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserMentionsByChannel' });
+ }
+
+ const room = Rooms.findOneById(roomId);
+
+ if (!room) {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUserMentionsByChannel' });
+ }
+
+ const user = Users.findOneById(Meteor.userId());
+
+ return Messages.findVisibleByMentionAndRoomId(user.username, roomId, options).fetch();
+ },
+});
diff --git a/app/mentions/server/server.js b/app/mentions/server/server.js
new file mode 100644
index 000000000000..3012b300379a
--- /dev/null
+++ b/app/mentions/server/server.js
@@ -0,0 +1,38 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { Notifications } from '../../notifications';
+import { Users, Subscriptions, Rooms } from '../../models';
+import _ from 'underscore';
+import MentionsServer from './Mentions';
+
+const mention = new MentionsServer({
+ pattern: () => settings.get('UTF8_Names_Validation'),
+ messageMaxAll: () => settings.get('Message_MaxAll'),
+ getUsers: (usernames) => Meteor.users.find({ username: { $in: _.unique(usernames) } }, { fields: { _id: true, username: true, name: 1 } }).fetch(),
+ getUser: (userId) => Users.findOneById(userId),
+ getTotalChannelMembers: (rid) => Subscriptions.findByRoomId(rid).count(),
+ getChannels: (channels) => Rooms.find({ name: { $in: _.unique(channels) }, t: { $in: ['c', 'p'] } }, { fields: { _id: 1, name: 1 } }).fetch(),
+ onMaxRoomMembersExceeded({ sender, rid }) {
+ // Get the language of the user for the error notification.
+ const { language } = this.getUser(sender._id);
+ const msg = TAPi18n.__('Group_mentions_disabled_x_members', { total: this.messageMaxAll }, language);
+
+ Notifications.notifyUser(sender._id, 'message', {
+ _id: Random.id(),
+ rid,
+ ts: new Date,
+ msg,
+ groupable: false,
+ });
+
+ // Also throw to stop propagation of 'sendMessage'.
+ throw new Meteor.Error('error-action-not-allowed', msg, {
+ method: 'filterATAllTag',
+ action: msg,
+ });
+ },
+});
+callbacks.add('beforeSaveMessage', (message) => mention.execute(message), callbacks.priority.HIGH, 'mentions');
diff --git a/app/mentions/tests/client.tests.js b/app/mentions/tests/client.tests.js
new file mode 100644
index 000000000000..d3af918771ad
--- /dev/null
+++ b/app/mentions/tests/client.tests.js
@@ -0,0 +1,370 @@
+/* eslint-env mocha */
+import 'babel-polyfill';
+import assert from 'assert';
+import { MentionsParser } from '../lib/MentionsParser';
+
+let mentionsParser;
+beforeEach(function functionName() {
+ mentionsParser = new MentionsParser({
+ pattern: '[0-9a-zA-Z-_.]+',
+ me: () => 'me',
+ });
+});
+
+describe('Mention', function() {
+ describe('get pattern', () => {
+ const regexp = '[0-9a-zA-Z-_.]+';
+ beforeEach(() => mentionsParser.pattern = () => regexp);
+
+ describe('by function', function functionName() {
+ it(`should be equal to ${ regexp }`, () => {
+ assert.equal(regexp, mentionsParser.pattern);
+ });
+ });
+
+ describe('by const', function functionName() {
+ it(`should be equal to ${ regexp }`, () => {
+ assert.equal(regexp, mentionsParser.pattern);
+ });
+ });
+ });
+
+ describe('get useRealName', () => {
+ beforeEach(() => mentionsParser.useRealName = () => true);
+
+ describe('by function', function functionName() {
+ it('should be true', () => {
+ assert.equal(true, mentionsParser.useRealName);
+ });
+ });
+
+ describe('by const', function functionName() {
+ it('should be true', () => {
+ assert.equal(true, mentionsParser.useRealName);
+ });
+ });
+ });
+
+ describe('get me', () => {
+ const me = 'me';
+
+ describe('by function', function functionName() {
+ beforeEach(() => mentionsParser.me = () => me);
+
+ it(`should be equal to ${ me }`, () => {
+ assert.equal(me, mentionsParser.me);
+ });
+ });
+
+ describe('by const', function functionName() {
+ beforeEach(() => mentionsParser.me = me);
+
+ it(`should be equal to ${ me }`, () => {
+ assert.equal(me, mentionsParser.me);
+ });
+ });
+ });
+
+ describe('getUserMentions', function functionName() {
+ describe('for simple text, no mentions', () => {
+ const result = [];
+ [
+ '#rocket.cat',
+ 'hello rocket.cat how are you?',
+ ]
+ .forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions(text));
+ });
+ });
+ });
+
+ describe('for one user', () => {
+ const result = ['@rocket.cat'];
+ [
+ '@rocket.cat',
+ ' @rocket.cat ',
+ 'hello @rocket.cat',
+ // 'hello,@rocket.cat', // this test case is ignored since is not compatible with the message box behavior
+ '@rocket.cat, hello',
+ '@rocket.cat,hello',
+ 'hello @rocket.cat how are you?',
+ ]
+ .forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions(text));
+ });
+ });
+
+ it.skip('should return without the "." from "@rocket.cat."', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat.'));
+ });
+
+ it.skip('should return without the "_" from "@rocket.cat_"', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat_'));
+ });
+
+ it.skip('should return without the "-" from "@rocket.cat-"', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat-'));
+ });
+ });
+
+ describe('for two users', () => {
+ const result = ['@rocket.cat', '@all'];
+ [
+ '@rocket.cat @all',
+ ' @rocket.cat @all ',
+ 'hello @rocket.cat and @all',
+ '@rocket.cat, hello @all',
+ 'hello @rocket.cat and @all how are you?',
+ ]
+ .forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions(text));
+ });
+ });
+ });
+ });
+
+ describe('getChannelMentions', function functionName() {
+ describe('for simple text, no mentions', () => {
+ const result = [];
+ [
+ '@rocket.cat',
+ 'hello rocket.cat how are you?',
+ ]
+ .forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getChannelMentions(text));
+ });
+ });
+ });
+
+ describe('for one channel', () => {
+ const result = ['#general'];
+ [
+ '#general',
+ ' #general ',
+ 'hello #general',
+ '#general, hello',
+ 'hello #general, how are you?',
+ ].forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getChannelMentions(text));
+ });
+ });
+
+ it.skip('should return without the "." from "#general."', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('#general.'));
+ });
+
+ it.skip('should return without the "_" from "#general_"', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('#general_'));
+ });
+
+ it.skip('should return without the "-" from "#general."', () => {
+ assert.deepEqual(result, mentionsParser.getUserMentions('#general-'));
+ });
+ });
+
+ describe('for two channels', () => {
+ const result = ['#general', '#other'];
+ [
+ '#general #other',
+ ' #general #other',
+ 'hello #general and #other',
+ '#general, hello #other',
+ 'hello #general #other, how are you?',
+ ].forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getChannelMentions(text));
+ });
+ });
+ });
+
+ describe('for url with fragments', () => {
+ const result = [];
+ [
+ 'http://localhost/#general',
+ ].forEach((text) => {
+ it(`should return nothing from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getChannelMentions(text));
+ });
+ });
+ });
+
+ describe('for messages with url and channels', () => {
+ const result = ['#general'];
+ [
+ 'http://localhost/#general #general',
+ ].forEach((text) => {
+ it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => {
+ assert.deepEqual(result, mentionsParser.getChannelMentions(text));
+ });
+ });
+ });
+ });
+});
+
+const message = {
+ mentions: [{ username: 'rocket.cat', name: 'Rocket.Cat' }, { username: 'admin', name: 'Admin' }, { username: 'me', name: 'Me' }, { username: 'specialchars', name: ' ' }],
+ channels: [{ name: 'general', _id: '42' }, { name: 'rocket.cat', _id: '169' }],
+};
+
+describe('replace methods', function() {
+ describe('replaceUsers', () => {
+ it('should render for @all', () => {
+ const result = mentionsParser.replaceUsers('@all', message, 'me');
+ assert.equal(result, 'all ');
+ });
+
+ const str2 = 'rocket.cat';
+
+ it(`should render for "@${ str2 }"`, () => {
+ const result = mentionsParser.replaceUsers(`@${ str2 }`, message, 'me');
+ assert.equal(result, `${ str2 } `);
+ });
+
+ it(`should render for "hello ${ str2 }"`, () => {
+ const result = mentionsParser.replaceUsers(`hello @${ str2 }`, message, 'me');
+ assert.equal(result, `hello ${ str2 } `);
+ });
+
+ it('should render for unknow/private user "hello @unknow"', () => {
+ const result = mentionsParser.replaceUsers('hello @unknow', message, 'me');
+ assert.equal(result, 'hello @unknow');
+ });
+
+ it('should render for me', () => {
+ const result = mentionsParser.replaceUsers('hello @me', message, 'me');
+ assert.equal(result, 'hello me ');
+ });
+ });
+
+ describe('replaceUsers (RealNames)', () => {
+ beforeEach(() => {
+ mentionsParser.useRealName = () => true;
+ });
+
+ it('should render for @all', () => {
+ const result = mentionsParser.replaceUsers('@all', message, 'me');
+ assert.equal(result, 'all ');
+ });
+
+ const str2 = 'rocket.cat';
+ const str2Name = 'Rocket.Cat';
+
+ it(`should render for "@${ str2 }"`, () => {
+ const result = mentionsParser.replaceUsers(`@${ str2 }`, message, 'me');
+ assert.equal(result, `${ str2Name } `);
+ });
+
+ it(`should render for "hello @${ str2 }"`, () => {
+ const result = mentionsParser.replaceUsers(`hello @${ str2 }`, message, 'me');
+ assert.equal(result, `hello ${ str2Name } `);
+ });
+
+ const specialchars = 'specialchars';
+ const specialcharsName = '<img onerror=alert(hello)>';
+
+ it(`should escape special characters in "hello @${ specialchars }"`, () => {
+ const result = mentionsParser.replaceUsers(`hello @${ specialchars }`, message, 'me');
+ assert.equal(result, `hello ${ specialcharsName } `);
+ });
+
+ it(`should render for "hello @${ str2 } "`, () => {
+ const result = mentionsParser.replaceUsers(`hello @${ str2 } `, message, 'me');
+ assert.equal(result, `hello${ str2Name } `);
+ });
+
+ it('should render for unknow/private user "hello @unknow"', () => {
+ const result = mentionsParser.replaceUsers('hello @unknow', message, 'me');
+ assert.equal(result, 'hello @unknow');
+ });
+
+ it('should render for me', () => {
+ const result = mentionsParser.replaceUsers('hello @me', message, 'me');
+ assert.equal(result, 'hello Me ');
+ });
+ });
+
+ describe('replaceChannels', () => {
+ it('should render for #general', () => {
+ const result = mentionsParser.replaceChannels('#general', message);
+ assert.equal('#general ', result);
+ });
+
+ const str2 = '#rocket.cat';
+
+ it(`should render for ${ str2 }`, () => {
+ const result = mentionsParser.replaceChannels(str2, message);
+ assert.equal(result, `${ str2 } `);
+ });
+
+ it(`should render for "hello ${ str2 }"`, () => {
+ const result = mentionsParser.replaceChannels(`hello ${ str2 }`, message);
+ assert.equal(result, `hello ${ str2 } `);
+ });
+
+ it('should render for unknow/private channel "hello #unknow"', () => {
+ const result = mentionsParser.replaceChannels('hello #unknow', message);
+ assert.equal(result, 'hello #unknow');
+ });
+ });
+
+ describe('parse all', () => {
+ it('should render for #general', () => {
+ message.html = '#general';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '#general ');
+ });
+
+ it('should render for "#general and @rocket.cat', () => {
+ message.html = '#general and @rocket.cat';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '#general and rocket.cat ');
+ });
+
+ it('should render for "', () => {
+ message.html = '';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '');
+ });
+
+ it('should render for "simple text', () => {
+ message.html = 'simple text';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, 'simple text');
+ });
+ });
+
+ describe('parse all (RealNames)', () => {
+ beforeEach(() => {
+ mentionsParser.useRealName = () => true;
+ });
+
+ it('should render for #general', () => {
+ message.html = '#general';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '#general ');
+ });
+
+ it('should render for "#general and @rocket.cat', () => {
+ message.html = '#general and @rocket.cat';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '#general and Rocket.Cat ');
+ });
+
+ it('should render for "', () => {
+ message.html = '';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, '');
+ });
+
+ it('should render for "simple text', () => {
+ message.html = 'simple text';
+ const result = mentionsParser.parse(message, 'me');
+ assert.equal(result.html, 'simple text');
+ });
+ });
+});
diff --git a/packages/rocketchat-mentions/tests/server.tests.js b/app/mentions/tests/server.tests.js
similarity index 100%
rename from packages/rocketchat-mentions/tests/server.tests.js
rename to app/mentions/tests/server.tests.js
diff --git a/packages/rocketchat-message-action/client/index.js b/app/message-action/client/index.js
similarity index 100%
rename from packages/rocketchat-message-action/client/index.js
rename to app/message-action/client/index.js
diff --git a/packages/rocketchat-message-action/client/messageAction.html b/app/message-action/client/messageAction.html
similarity index 81%
rename from packages/rocketchat-message-action/client/messageAction.html
rename to app/message-action/client/messageAction.html
index 872c52d58bb3..8d6c73566772 100644
--- a/packages/rocketchat-message-action/client/messageAction.html
+++ b/app/message-action/client/messageAction.html
@@ -9,7 +9,7 @@
{{/if}}
{{#if msg_in_chat_window}}
-
+
{{/if}}
@@ -21,7 +21,7 @@
{{/if}}
{{#if msg_in_chat_window}}
-
+
{{text}}
{{/if}}
diff --git a/app/message-action/client/messageAction.js b/app/message-action/client/messageAction.js
new file mode 100644
index 000000000000..025c06db224a
--- /dev/null
+++ b/app/message-action/client/messageAction.js
@@ -0,0 +1,13 @@
+import { Template } from 'meteor/templating';
+
+Template.messageAction.helpers({
+ isButton() {
+ return this.type === 'button';
+ },
+ areButtonsHorizontal() {
+ return Template.parentData(1).button_alignment === 'horizontal';
+ },
+ jsActionButtonClassname(processingType) {
+ return `js-actionButton-${ processingType || 'sendMessage' }`;
+ },
+});
diff --git a/packages/rocketchat-message-action/client/stylesheets/messageAction.css b/app/message-action/client/stylesheets/messageAction.css
similarity index 100%
rename from packages/rocketchat-message-action/client/stylesheets/messageAction.css
rename to app/message-action/client/stylesheets/messageAction.css
diff --git a/app/message-action/index.js b/app/message-action/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/message-action/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/app/message-attachments/client/index.js b/app/message-attachments/client/index.js
new file mode 100644
index 000000000000..f3cd8f6e320f
--- /dev/null
+++ b/app/message-attachments/client/index.js
@@ -0,0 +1,10 @@
+import './messageAttachment.html';
+import './messageAttachment';
+import './renderField.html';
+import { registerFieldTemplate } from './renderField';
+
+
+
+export {
+ registerFieldTemplate,
+};
diff --git a/packages/rocketchat-message-attachments/client/messageAttachment.html b/app/message-attachments/client/messageAttachment.html
similarity index 76%
rename from packages/rocketchat-message-attachments/client/messageAttachment.html
rename to app/message-attachments/client/messageAttachment.html
index 99a8a5633055..cb22f01e8036 100644
--- a/packages/rocketchat-message-attachments/client/messageAttachment.html
+++ b/app/message-attachments/client/messageAttachment.html
@@ -6,31 +6,34 @@
{{else}}
{{pretext}}
{{/if}}
-
+
+
{{#if author_name}}
{{#if author_link}}
{{#if author_icon}}
-
+
{{/if}}
-
{{author_name}}
+
{{author_name}}
{{#if ts}}
{{#if message_link}}
{{time}}
{{else}}
-
- {{time}}
-
+ {{#unless time}}
+
+ {{time}}
+
+ {{/unless}}
{{/if}}
{{/if}}
{{else}}
{{#if author_icon}}
-
+
{{/if}}
{{author_name}}
{{#if ts}}
@@ -39,9 +42,11 @@
{{time}}
{{else}}
-
- {{time}}
-
+ {{#unless time}}
+
+ {{time}}
+
+ {{/unless}}
{{/if}}
{{/if}}
@@ -50,9 +55,9 @@
{{#if title}}
{{#if title_link}}
-
{{#if isFile}} {{_ "Attachment_File_Uploaded"}}: {{/if}}{{title}}
+
{{title}}
{{#if title_link_download}}
-
+
{{> icon icon="download"}}
{{/if}}
{{else}}
{{title}}
@@ -69,7 +74,7 @@
{{#if thumb_url}}
-
+
{{/if}}
@@ -86,7 +91,7 @@
{{#if loadImage}}
- {{> lazyloadImage src=image_url preview=image_preview height=(getImageHeight image_dimensions.height) class="gallery-item" title=title description=description}}
+ {{> lazyloadImage src=(getURL image_url) preview=image_preview height=(getImageHeight image_dimensions.height) class="gallery-item" title=title description=description}}
{{#if labels}}
{{#each labels}}
@@ -112,7 +117,7 @@
{{#unless mediaCollapsed}}
-
+
Your browser does not support the audio element.
@@ -123,7 +128,7 @@
{{#unless mediaCollapsed}}
-
+
Your browser does not support the video element.
@@ -141,11 +146,8 @@
{{#if fields}}
{{#unless collapsed}}
- {{#each fields}}
-
-
{{title}}
- {{{RocketChatMarkdown value}}}
-
+ {{#each field in fields}}
+ {{> renderField field=field}}
{{/each}}
{{/unless}}
diff --git a/app/message-attachments/client/messageAttachment.js b/app/message-attachments/client/messageAttachment.js
new file mode 100644
index 000000000000..148db49fde2e
--- /dev/null
+++ b/app/message-attachments/client/messageAttachment.js
@@ -0,0 +1,82 @@
+import { Meteor } from 'meteor/meteor';
+import { DateFormat } from '../../lib';
+import { Template } from 'meteor/templating';
+import { getUserPreference, getURL } from '../../utils/client';
+import { Users } from '../../models';
+import { renderMessageBody } from '../../ui-utils';
+
+const colors = {
+ good: '#35AC19',
+ warning: '#FCB316',
+ danger: '#D30230',
+};
+
+Template.messageAttachment.helpers({
+ parsedText() {
+ return renderMessageBody({
+ msg: this.text,
+ });
+ },
+ markdownInPretext() {
+ return this.mrkdwn_in && this.mrkdwn_in.includes('pretext');
+ },
+ parsedPretext() {
+ return renderMessageBody({
+ msg: this.pretext,
+ });
+ },
+ loadImage() {
+ if (this.downloadImages !== true) {
+ const user = Users.findOne({ _id: Meteor.userId() }, { fields: { 'settings.autoImageLoad' : 1 } });
+ if (getUserPreference(user, 'autoImageLoad') === false) {
+ return false;
+ }
+ if (Meteor.Device.isPhone() && getUserPreference(user, 'saveMobileBandwidth') !== true) {
+ return false;
+ }
+ }
+ return true;
+ },
+ getImageHeight(height = 200) {
+ return height;
+ },
+ color() {
+ return colors[this.color] || this.color;
+ },
+ collapsed() {
+ if (this.collapsed != null) {
+ return this.collapsed;
+ }
+ return false;
+ },
+ mediaCollapsed() {
+ if (this.collapsed != null) {
+ return this.collapsed;
+ } else {
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ }
+ },
+ time() {
+ const messageDate = new Date(this.ts);
+ const today = new Date();
+ if (messageDate.toDateString() === today.toDateString()) {
+ return DateFormat.formatTime(this.ts);
+ }
+ return DateFormat.formatDateAndTime(this.ts);
+ },
+ injectIndex(data, previousIndex, index) {
+ data.index = `${ previousIndex }.attachments.${ index }`;
+ },
+
+ isFile() {
+ return this.type === 'file';
+ },
+ isPDF() {
+ if (this.type === 'file' && this.title_link.endsWith('.pdf') && Template.parentData().file) {
+ this.fileId = Template.parentData().file._id;
+ return true;
+ }
+ return false;
+ },
+ getURL,
+});
diff --git a/app/message-attachments/client/renderField.html b/app/message-attachments/client/renderField.html
new file mode 100644
index 000000000000..360d8b976134
--- /dev/null
+++ b/app/message-attachments/client/renderField.html
@@ -0,0 +1,20 @@
+
+ {{#if field.type}}
+
+
+ {{{specializedRendering field=field message=../..}}}
+
+ {{else}}
+ {{#if field.short}}
+
+
{{field.title}}
+ {{{RocketChatMarkdown field.value}}}
+
+ {{else}}
+
+
{{field.title}}
+ {{{RocketChatMarkdown field.value}}}
+
+ {{/if}}
+ {{/if}}
+
diff --git a/app/message-attachments/client/renderField.js b/app/message-attachments/client/renderField.js
new file mode 100644
index 000000000000..28bc9fd0adab
--- /dev/null
+++ b/app/message-attachments/client/renderField.js
@@ -0,0 +1,56 @@
+import { Template } from 'meteor/templating';
+import { Blaze } from 'meteor/blaze';
+
+const renderers = {};
+
+/**
+ * The field templates will be rendered non-reactive for all messages by the messages-list (@see rocketchat-nrr)
+ * Thus, we cannot provide helpers or events to the template, but we need to register this interactivity at the parent
+ * template which is the room. The event will be bubbled by the Blaze-framework
+ * @param fieldType
+ * @param templateName
+ * @param helpers
+ * @param events
+ */
+export function registerFieldTemplate(fieldType, templateName, events) {
+ renderers[fieldType] = templateName;
+
+ // propagate helpers and events to the room template, changing the selectors
+ // loop at events. For each event (like 'click .accept'), copy the function to a function of the room events.
+ // While doing that, add the fieldType as class selector to the events function in order to avoid naming clashes
+ if (events != null) {
+ const uniqueEvents = {};
+ // rename the event handlers so they are unique in the "parent" template to which the events bubble
+ for (const property in events) {
+ if (events.hasOwnProperty(property)) {
+ const event = property.substr(0, property.indexOf(' '));
+ const selector = property.substr(property.indexOf(' ') + 1);
+ Object.defineProperty(uniqueEvents,
+ `${ event } .${ fieldType } ${ selector }`,
+ {
+ value: events[property],
+ enumerable: true, // assign as a own property
+ });
+ }
+ }
+ Template.room.events(uniqueEvents);
+ }
+}
+
+// onRendered is not being executed (no idea why). Consequently, we cannot use Blaze.renderWithData(), since we don't
+// have access to the DOM outside onRendered. Therefore, we can only translate the content of the field to HTML and
+// embed it non-reactively.
+// This in turn means that onRendered of the field template will not be processed either.
+// I guess it may have someting to do with rocketchat-nrr
+Template.renderField.helpers({
+ specializedRendering({ hash: { field, message } }) {
+ let html = '';
+ if (field.type && renderers[field.type]) {
+ html = Blaze.toHTMLWithData(Template[renderers[field.type]], { field, message });
+ } else {
+ // consider the value already formatted as html
+ html = field.value;
+ }
+ return `
${ html }
`;
+ },
+});
diff --git a/packages/rocketchat-message-attachments/client/stylesheets/messageAttachments.css b/app/message-attachments/client/stylesheets/messageAttachments.css
similarity index 94%
rename from packages/rocketchat-message-attachments/client/stylesheets/messageAttachments.css
rename to app/message-attachments/client/stylesheets/messageAttachments.css
index 976f9c6756f2..b71694263442 100644
--- a/packages/rocketchat-message-attachments/client/stylesheets/messageAttachments.css
+++ b/app/message-attachments/client/stylesheets/messageAttachments.css
@@ -65,13 +65,12 @@ html.rtl .attachment {
}
& .attachment-title {
+
+ color: #1d74f5;
+
font-size: 1.02rem;
font-weight: 500;
line-height: 1.5rem;
-
- & > a {
- font-weight: 500;
- }
}
& .attachment-text {
@@ -90,6 +89,8 @@ html.rtl .attachment {
display: flex;
margin-top: 4px;
+
+ align-items: center;
flex-wrap: wrap;
& .attachment-field {
@@ -138,11 +139,7 @@ html.rtl .attachment {
}
& .attachment-download-icon {
- margin-left: 5px;
- padding: 2px 5px;
-
- border-width: 1px;
- border-radius: 5px;
+ padding: 0 5px;
}
& .attachment-canvas {
diff --git a/app/message-attachments/index.js b/app/message-attachments/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/message-attachments/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/app/message-mark-as-unread/client/actionButton.js b/app/message-mark-as-unread/client/actionButton.js
new file mode 100644
index 000000000000..620e97abec65
--- /dev/null
+++ b/app/message-mark-as-unread/client/actionButton.js
@@ -0,0 +1,36 @@
+import { Meteor } from 'meteor/meteor';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { RoomManager, MessageAction } from '../../ui-utils';
+import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+import { handleError } from '../../utils';
+import { ChatSubscription } from '../../models';
+
+Meteor.startup(() => {
+ MessageAction.addButton({
+ id: 'mark-message-as-unread',
+ icon: 'flag',
+ label: 'Mark_unread',
+ context: ['message', 'message-mobile'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ return Meteor.call('unreadMessages', message, function(error) {
+ if (error) {
+ return handleError(error);
+ }
+ const subscription = ChatSubscription.findOne({
+ rid: message.rid,
+ });
+ if (subscription == null) {
+ return;
+ }
+ RoomManager.close(subscription.t + subscription.name);
+ return FlowRouter.go('home');
+ });
+ },
+ condition(message) {
+ return Meteor.userId() && message.u._id !== Meteor.userId();
+ },
+ order: 10,
+ group: 'menu',
+ });
+});
diff --git a/packages/rocketchat-message-mark-as-unread/client/index.js b/app/message-mark-as-unread/client/index.js
similarity index 100%
rename from packages/rocketchat-message-mark-as-unread/client/index.js
rename to app/message-mark-as-unread/client/index.js
diff --git a/packages/rocketchat-message-mark-as-unread/server/index.js b/app/message-mark-as-unread/server/index.js
similarity index 100%
rename from packages/rocketchat-message-mark-as-unread/server/index.js
rename to app/message-mark-as-unread/server/index.js
diff --git a/app/message-mark-as-unread/server/logger.js b/app/message-mark-as-unread/server/logger.js
new file mode 100644
index 000000000000..1327ecda6e8c
--- /dev/null
+++ b/app/message-mark-as-unread/server/logger.js
@@ -0,0 +1,9 @@
+import { Logger } from '../../logger';
+
+const logger = new Logger('MessageMarkAsUnread', {
+ sections: {
+ connection: 'Connection',
+ events: 'Events',
+ },
+});
+export default logger;
diff --git a/app/message-mark-as-unread/server/unreadMessages.js b/app/message-mark-as-unread/server/unreadMessages.js
new file mode 100644
index 000000000000..f5f0421f2bae
--- /dev/null
+++ b/app/message-mark-as-unread/server/unreadMessages.js
@@ -0,0 +1,48 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages, Subscriptions } from '../../models';
+import logger from './logger';
+
+Meteor.methods({
+ unreadMessages(firstUnreadMessage, room) {
+ const userId = Meteor.userId();
+ if (!userId) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'unreadMessages',
+ });
+ }
+
+ if (room) {
+ const lastMessage = Messages.findVisibleByRoomId(room, { limit: 1, sort: { ts: -1 } }).fetch()[0];
+
+ if (lastMessage == null) {
+ throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
+ method: 'unreadMessages',
+ action: 'Unread_messages',
+ });
+ }
+
+ return Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts);
+ }
+
+ const originalMessage = Messages.findOneById(firstUnreadMessage._id, {
+ fields: {
+ u: 1,
+ rid: 1,
+ file: 1,
+ ts: 1,
+ },
+ });
+ if (originalMessage == null || userId === originalMessage.u._id) {
+ throw new Meteor.Error('error-action-not-allowed', 'Not allowed', {
+ method: 'unreadMessages',
+ action: 'Unread_messages',
+ });
+ }
+ const lastSeen = Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, userId).ls;
+ if (firstUnreadMessage.ts >= lastSeen) {
+ return logger.connection.debug('Provided message is already marked as unread');
+ }
+ logger.connection.debug(`Updating unread message of ${ originalMessage.ts } as the first unread`);
+ return Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts);
+ },
+});
diff --git a/app/message-pin/client/actionButton.js b/app/message-pin/client/actionButton.js
new file mode 100644
index 000000000000..9f17bd36f890
--- /dev/null
+++ b/app/message-pin/client/actionButton.js
@@ -0,0 +1,105 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { RoomHistoryManager, MessageAction } from '../../ui-utils';
+import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+import { handleError } from '../../utils';
+import { settings } from '../../settings';
+import { Subscriptions } from '../../models';
+import { hasAtLeastOnePermission } from '../../authorization';
+import toastr from 'toastr';
+
+Meteor.startup(function() {
+ MessageAction.addButton({
+ id: 'pin-message',
+ icon: 'pin',
+ label: 'Pin',
+ context: ['pinned', 'message', 'message-mobile'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ message.pinned = true;
+ Meteor.call('pinMessage', message, function(error) {
+ if (error) {
+ return handleError(error);
+ }
+ });
+ },
+ condition(message) {
+ if (!settings.get('Message_AllowPinning') || message.pinned || !Subscriptions.findOne({ rid: message.rid }, { fields: { _id: 1 } })) {
+ return false;
+ }
+
+ return hasAtLeastOnePermission('pin-message', message.rid);
+ },
+ order: 7,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'unpin-message',
+ icon: 'pin',
+ label: 'Unpin',
+ context: ['pinned', 'message', 'message-mobile'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ message.pinned = false;
+ Meteor.call('unpinMessage', message, function(error) {
+ if (error) {
+ return handleError(error);
+ }
+ });
+ },
+ condition(message) {
+ if (!settings.get('Message_AllowPinning') || !message.pinned || !Subscriptions.findOne({ rid: message.rid }, { fields: { _id: 1 } })) {
+ return false;
+ }
+
+ return hasAtLeastOnePermission('pin-message', message.rid);
+ },
+ order: 8,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'jump-to-pin-message',
+ icon: 'jump',
+ label: 'Jump_to_message',
+ context: ['pinned'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ if (window.matchMedia('(max-width: 500px)').matches) {
+ Template.instance().tabBar.close();
+ }
+ return RoomHistoryManager.getSurroundingMessages(message, 50);
+ },
+ condition(message) {
+ if (!Subscriptions.findOne({ rid: message.rid }, { fields: { _id: 1 } })) {
+ return false;
+ }
+ return true;
+ },
+ order: 100,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'permalink-pinned',
+ icon: 'permalink',
+ label: 'Get_link',
+ classes: 'clipboard',
+ context: ['pinned'],
+ async action(event) {
+ const { msg: message } = messageArgs(this);
+ $(event.currentTarget).attr('data-clipboard-text', await MessageAction.getPermaLink(message._id));
+ toastr.success(TAPi18n.__('Copied'));
+ },
+ condition(message) {
+ if (!Subscriptions.findOne({ rid: message.rid }, { fields: { _id: 1 } })) {
+ return false;
+ }
+ return true;
+ },
+ order: 101,
+ group: 'menu',
+ });
+});
diff --git a/packages/rocketchat-message-pin/client/index.js b/app/message-pin/client/index.js
similarity index 100%
rename from packages/rocketchat-message-pin/client/index.js
rename to app/message-pin/client/index.js
diff --git a/packages/rocketchat-message-pin/client/lib/PinnedMessage.js b/app/message-pin/client/lib/PinnedMessage.js
similarity index 100%
rename from packages/rocketchat-message-pin/client/lib/PinnedMessage.js
rename to app/message-pin/client/lib/PinnedMessage.js
diff --git a/app/message-pin/client/messageType.js b/app/message-pin/client/messageType.js
new file mode 100644
index 000000000000..a178d84f46ea
--- /dev/null
+++ b/app/message-pin/client/messageType.js
@@ -0,0 +1,10 @@
+import { Meteor } from 'meteor/meteor';
+import { MessageTypes } from '../../ui-utils';
+
+Meteor.startup(function() {
+ MessageTypes.registerType({
+ id: 'message_pinned',
+ system: true,
+ message: 'Pinned_a_message',
+ });
+});
diff --git a/app/message-pin/client/pinMessage.js b/app/message-pin/client/pinMessage.js
new file mode 100644
index 000000000000..4fe5ead345df
--- /dev/null
+++ b/app/message-pin/client/pinMessage.js
@@ -0,0 +1,42 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { ChatMessage, Subscriptions } from '../../models';
+
+Meteor.methods({
+ pinMessage(message) {
+ if (!Meteor.userId()) {
+ return false;
+ }
+ if (!settings.get('Message_AllowPinning')) {
+ return false;
+ }
+ if (Subscriptions.findOne({ rid: message.rid }) == null) {
+ return false;
+ }
+ return ChatMessage.update({
+ _id: message._id,
+ }, {
+ $set: {
+ pinned: true,
+ },
+ });
+ },
+ unpinMessage(message) {
+ if (!Meteor.userId()) {
+ return false;
+ }
+ if (!settings.get('Message_AllowPinning')) {
+ return false;
+ }
+ if (Subscriptions.findOne({ rid: message.rid }) == null) {
+ return false;
+ }
+ return ChatMessage.update({
+ _id: message._id,
+ }, {
+ $set: {
+ pinned: false,
+ },
+ });
+ },
+});
diff --git a/app/message-pin/client/tabBar.js b/app/message-pin/client/tabBar.js
new file mode 100644
index 000000000000..11c121f4f155
--- /dev/null
+++ b/app/message-pin/client/tabBar.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { settings } from '../../settings';
+import { TabBar } from '../../ui-utils';
+
+Meteor.startup(function() {
+ return Tracker.autorun(function() {
+ if (settings.get('Message_AllowPinning')) {
+ TabBar.addButton({
+ groups: ['channel', 'group', 'direct'],
+ id: 'pinned-messages',
+ i18nTitle: 'Pinned_Messages',
+ icon: 'pin',
+ template: 'pinnedMessages',
+ order: 10,
+ });
+ } else {
+ TabBar.removeButton('pinned-messages');
+ }
+ });
+});
diff --git a/app/message-pin/client/views/pinnedMessages.html b/app/message-pin/client/views/pinnedMessages.html
new file mode 100644
index 000000000000..7ad911de4ab0
--- /dev/null
+++ b/app/message-pin/client/views/pinnedMessages.html
@@ -0,0 +1,22 @@
+
+ {{#if Template.subscriptionsReady}}
+ {{#unless hasMessages}}
+
+ {{/unless}}
+ {{/if}}
+
+
+ {{# with messageContext}}
+ {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
+ {{/with}}
+
+
+ {{#if hasMore}}
+
+ {{> loading}}
+
+ {{/if}}
+
+
diff --git a/app/message-pin/client/views/pinnedMessages.js b/app/message-pin/client/views/pinnedMessages.js
new file mode 100644
index 000000000000..dc4bc2ee11c1
--- /dev/null
+++ b/app/message-pin/client/views/pinnedMessages.js
@@ -0,0 +1,52 @@
+import _ from 'underscore';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { PinnedMessage } from '../lib/PinnedMessage';
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
+
+Template.pinnedMessages.helpers({
+ hasMessages() {
+ return Template.instance().cursor.count() > 0;
+ },
+ messages() {
+ return Template.instance().cursor;
+ },
+ message() {
+ return _.extend(this, { customClass: 'pinned', actionContext: 'pinned' });
+ },
+ hasMore() {
+ return Template.instance().hasMore.get();
+ },
+ messageContext,
+});
+
+Template.pinnedMessages.onCreated(function() {
+ this.rid = this.data.rid;
+
+ this.cursor = PinnedMessage.find({
+ rid: this.data.rid,
+ }, {
+ sort: {
+ ts: -1,
+ },
+ });
+
+ this.hasMore = new ReactiveVar(true);
+ this.limit = new ReactiveVar(50);
+ return this.autorun(() => {
+ const data = Template.currentData();
+ return this.subscribe('pinnedMessages', data.rid, this.limit.get(), () => {
+ if (this.cursor.count() < this.limit.get()) {
+ return this.hasMore.set(false);
+ }
+ });
+ });
+});
+
+Template.pinnedMessages.events({
+ 'scroll .js-list': _.throttle(function(e, instance) {
+ if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) {
+ return instance.limit.set(instance.limit.get() + 50);
+ }
+ }, 200),
+});
diff --git a/packages/rocketchat-message-pin/client/views/stylesheets/messagepin.css b/app/message-pin/client/views/stylesheets/messagepin.css
similarity index 100%
rename from packages/rocketchat-message-pin/client/views/stylesheets/messagepin.css
rename to app/message-pin/client/views/stylesheets/messagepin.css
diff --git a/app/message-pin/server/index.js b/app/message-pin/server/index.js
new file mode 100644
index 000000000000..e4160692c732
--- /dev/null
+++ b/app/message-pin/server/index.js
@@ -0,0 +1,4 @@
+import './settings';
+import './pinMessage';
+import './publications/pinnedMessages';
+import './startup/indexes';
diff --git a/app/message-pin/server/pinMessage.js b/app/message-pin/server/pinMessage.js
new file mode 100644
index 000000000000..19b59cb6fe6d
--- /dev/null
+++ b/app/message-pin/server/pinMessage.js
@@ -0,0 +1,163 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { isTheLastMessage } from '../../lib';
+import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
+import { hasPermission } from '../../authorization';
+import { Subscriptions, Messages, Users, Rooms } from '../../models';
+
+const recursiveRemove = (msg, deep = 1) => {
+ if (!msg) {
+ return;
+ }
+
+ if (deep > settings.get('Message_QuoteChainLimit')) {
+ delete msg.attachments;
+ return msg;
+ }
+
+ msg.attachments = Array.isArray(msg.attachments) ? msg.attachments.map(
+ (nestedMsg) => recursiveRemove(nestedMsg, deep + 1)
+ ) : null;
+
+ return msg;
+};
+
+const shouldAdd = (attachments, attachment) => !attachments.some(({ message_link }) => message_link && message_link === attachment.message_link);
+
+Meteor.methods({
+ pinMessage(message, pinnedAt) {
+ const userId = Meteor.userId();
+ if (!userId) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'pinMessage',
+ });
+ }
+
+ if (!settings.get('Message_AllowPinning')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Message pinning not allowed', {
+ method: 'pinMessage',
+ action: 'Message_pinning',
+ });
+ }
+
+ if (!hasPermission(Meteor.userId(), 'pin-message', message.rid)) {
+ throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
+ if (!subscription) {
+ return false;
+ }
+
+ let originalMessage = Messages.findOneById(message._id);
+ if (originalMessage == null || originalMessage._id == null) {
+ throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', {
+ method: 'pinMessage',
+ action: 'Message_pinning',
+ });
+ }
+
+ const me = Users.findOneById(userId);
+
+ // If we keep history of edits, insert a new message to store history information
+ if (settings.get('Message_KeepHistory')) {
+ Messages.cloneAndSaveAsHistoryById(message._id, me);
+ }
+ const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
+
+ originalMessage.pinned = true;
+ originalMessage.pinnedAt = pinnedAt || Date.now;
+ originalMessage.pinnedBy = {
+ _id: userId,
+ username: me.username,
+ };
+
+ originalMessage = callbacks.run('beforeSaveMessage', originalMessage);
+
+ Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned);
+ if (isTheLastMessage(room, message)) {
+ Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned);
+ }
+
+ const attachments = [];
+
+ if (Array.isArray(originalMessage.attachments)) {
+ originalMessage.attachments.forEach((attachment) => {
+ if (!attachment.message_link || shouldAdd(attachments, attachment)) {
+ attachments.push(attachment);
+ }
+ });
+ }
+
+ return Messages.createWithTypeRoomIdMessageAndUser(
+ 'message_pinned',
+ originalMessage.rid,
+ '',
+ me,
+ {
+ attachments: [
+ {
+ text: originalMessage.msg,
+ author_name: originalMessage.u.username,
+ author_icon: getUserAvatarURL(originalMessage.u.username),
+ ts: originalMessage.ts,
+ attachments: recursiveRemove(attachments),
+ },
+ ],
+ }
+ );
+ },
+ unpinMessage(message) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'unpinMessage',
+ });
+ }
+
+ if (!settings.get('Message_AllowPinning')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Message pinning not allowed', {
+ method: 'unpinMessage',
+ action: 'Message_pinning',
+ });
+ }
+
+ if (!hasPermission(Meteor.userId(), 'pin-message', message.rid)) {
+ throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
+ if (!subscription) {
+ return false;
+ }
+
+ let originalMessage = Messages.findOneById(message._id);
+
+ if (originalMessage == null || originalMessage._id == null) {
+ throw new Meteor.Error('error-invalid-message', 'Message you are unpinning was not found', {
+ method: 'unpinMessage',
+ action: 'Message_pinning',
+ });
+ }
+
+ const me = Users.findOneById(Meteor.userId());
+
+ // If we keep history of edits, insert a new message to store history information
+ if (settings.get('Message_KeepHistory')) {
+ Messages.cloneAndSaveAsHistoryById(originalMessage._id, me);
+ }
+
+ originalMessage.pinned = false;
+ originalMessage.pinnedBy = {
+ _id: Meteor.userId(),
+ username: me.username,
+ };
+ originalMessage = callbacks.run('beforeSaveMessage', originalMessage);
+ const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
+ if (isTheLastMessage(room, message)) {
+ Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned);
+ }
+
+ return Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned);
+ },
+});
diff --git a/app/message-pin/server/publications/pinnedMessages.js b/app/message-pin/server/publications/pinnedMessages.js
new file mode 100644
index 000000000000..8d41d8fe750d
--- /dev/null
+++ b/app/message-pin/server/publications/pinnedMessages.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { Users, Messages } from '../../../models';
+
+Meteor.publish('pinnedMessages', function(rid, limit = 50) {
+ if (!this.userId) {
+ return this.ready();
+ }
+ const publication = this;
+
+ const user = Users.findOneById(this.userId);
+ if (!user) {
+ return this.ready();
+ }
+ if (!Meteor.call('canAccessRoom', rid, this.userId)) {
+ return this.ready();
+ }
+ const cursorHandle = Messages.findPinnedByRoom(rid, { sort: { ts: -1 }, limit }).observeChanges({
+ added(_id, record) {
+ return publication.added('rocketchat_pinned_message', _id, record);
+ },
+ changed(_id, record) {
+ return publication.changed('rocketchat_pinned_message', _id, record);
+ },
+ removed(_id) {
+ return publication.removed('rocketchat_pinned_message', _id);
+ },
+ });
+ this.ready();
+ return this.onStop(function() {
+ return cursorHandle.stop();
+ });
+});
diff --git a/app/message-pin/server/settings.js b/app/message-pin/server/settings.js
new file mode 100644
index 000000000000..e5ee9ac276ad
--- /dev/null
+++ b/app/message-pin/server/settings.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { Permissions } from '../../models';
+
+Meteor.startup(function() {
+ settings.add('Message_AllowPinning', true, {
+ type: 'boolean',
+ group: 'Message',
+ public: true,
+ });
+ return Permissions.upsert('pin-message', {
+ $setOnInsert: {
+ roles: ['owner', 'moderator', 'admin'],
+ },
+ });
+});
diff --git a/app/message-pin/server/startup/indexes.js b/app/message-pin/server/startup/indexes.js
new file mode 100644
index 000000000000..96e78ad44d22
--- /dev/null
+++ b/app/message-pin/server/startup/indexes.js
@@ -0,0 +1,12 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages } from '../../../models';
+
+Meteor.startup(function() {
+ return Meteor.defer(function() {
+ return Messages.tryEnsureIndex({
+ 'pinnedBy._id': 1,
+ }, {
+ sparse: 1,
+ });
+ });
+});
diff --git a/app/message-snippet/client/actionButton.js b/app/message-snippet/client/actionButton.js
new file mode 100644
index 000000000000..342ff2b2854d
--- /dev/null
+++ b/app/message-snippet/client/actionButton.js
@@ -0,0 +1,69 @@
+// import { Meteor } from 'meteor/meteor';
+// import { MessageAction, modal } from '../../ui-utils';
+// import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+// import { t, handleError } from '../../utils';
+// import { settings } from '../../settings';
+// import { Subscriptions } from '../../models';
+// import { hasAtLeastOnePermission } from '../../authorization';
+//
+// Meteor.startup(function() {
+// MessageAction.addButton({
+// id: 'snippeted-message',
+// icon: 'code',
+// label: 'Snippet',
+// context: [
+// 'snippeted',
+// 'message',
+// 'message-mobile',
+// ],
+// order: 10,
+// group: 'menu',
+// action() {
+// const { msg: message } = messageArgs(this);
+//
+// modal.open({
+// title: 'Create a Snippet',
+// text: 'The name of your snippet (with file extension):',
+// type: 'input',
+// showCancelButton: true,
+// closeOnConfirm: false,
+// inputPlaceholder: 'Snippet name',
+// }, function(filename) {
+// if (filename === false) {
+// return false;
+// }
+// if (filename === '') {
+// modal.showInputError('You need to write something!');
+// return false;
+// }
+// message.snippeted = true;
+// Meteor.call('snippetMessage', message, filename, function(error) {
+// if (error) {
+// return handleError(error);
+// }
+// modal.open({
+// title: t('Nice'),
+// text: `Snippet '${ filename }' created.`,
+// type: 'success',
+// timer: 2000,
+// });
+// });
+// });
+//
+// },
+// condition(message) {
+// if (Subscriptions.findOne({ rid: message.rid, 'u._id': Meteor.userId() }) === undefined) {
+// return false;
+// }
+//
+// if (message.snippeted || ((settings.get('Message_AllowSnippeting') === undefined) ||
+// (settings.get('Message_AllowSnippeting') === null) ||
+// (settings.get('Message_AllowSnippeting')) === false)) {
+// return false;
+// }
+//
+// return hasAtLeastOnePermission('snippet-message', message.rid);
+// },
+// });
+//
+// });
diff --git a/packages/rocketchat-message-snippet/client/index.js b/app/message-snippet/client/index.js
similarity index 100%
rename from packages/rocketchat-message-snippet/client/index.js
rename to app/message-snippet/client/index.js
diff --git a/packages/rocketchat-message-snippet/client/lib/collections.js b/app/message-snippet/client/lib/collections.js
similarity index 100%
rename from packages/rocketchat-message-snippet/client/lib/collections.js
rename to app/message-snippet/client/lib/collections.js
diff --git a/app/message-snippet/client/messageType.js b/app/message-snippet/client/messageType.js
new file mode 100644
index 000000000000..f96968f1e548
--- /dev/null
+++ b/app/message-snippet/client/messageType.js
@@ -0,0 +1,15 @@
+import { Meteor } from 'meteor/meteor';
+import { MessageTypes } from '../../ui-utils';
+import s from 'underscore.string';
+
+Meteor.startup(function() {
+ MessageTypes.registerType({
+ id: 'message_snippeted',
+ system: true,
+ message: 'Snippeted_a_message',
+ data(message) {
+ const snippetLink = `
${ s.escapeHTML(message.snippetName) } `;
+ return { snippetLink };
+ },
+ });
+});
diff --git a/packages/rocketchat-message-snippet/client/page/snippetPage.html b/app/message-snippet/client/page/snippetPage.html
similarity index 100%
rename from packages/rocketchat-message-snippet/client/page/snippetPage.html
rename to app/message-snippet/client/page/snippetPage.html
diff --git a/packages/rocketchat-message-snippet/client/page/snippetPage.js b/app/message-snippet/client/page/snippetPage.js
similarity index 80%
rename from packages/rocketchat-message-snippet/client/page/snippetPage.js
rename to app/message-snippet/client/page/snippetPage.js
index a985b453be8f..05a5f96dffa0 100644
--- a/packages/rocketchat-message-snippet/client/page/snippetPage.js
+++ b/app/message-snippet/client/page/snippetPage.js
@@ -1,8 +1,9 @@
import { Meteor } from 'meteor/meteor';
-import { DateFormat } from 'meteor/rocketchat:lib';
+import { DateFormat } from '../../../lib';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../settings';
+import { Markdown } from '../../../markdown/client';
import { SnippetedMessages } from '../lib/collections';
import moment from 'moment';
@@ -16,13 +17,13 @@ Template.snippetPage.helpers({
return null;
}
message.html = message.msg;
- const markdown = RocketChat.Markdown.parse(message);
+ const markdown = Markdown.parse(message);
return markdown.tokens[0].text;
},
date() {
const snippet = SnippetedMessages.findOne({ _id: FlowRouter.getParam('snippetId') });
if (snippet !== undefined) {
- return moment(snippet.ts).format(RocketChat.settings.get('Message_DateFormat'));
+ return moment(snippet.ts).format(settings.get('Message_DateFormat'));
}
},
time() {
diff --git a/packages/rocketchat-message-snippet/client/page/stylesheets/snippetPage.css b/app/message-snippet/client/page/stylesheets/snippetPage.css
similarity index 100%
rename from packages/rocketchat-message-snippet/client/page/stylesheets/snippetPage.css
rename to app/message-snippet/client/page/stylesheets/snippetPage.css
diff --git a/app/message-snippet/client/router.js b/app/message-snippet/client/router.js
new file mode 100644
index 000000000000..b982e9e9eb3b
--- /dev/null
+++ b/app/message-snippet/client/router.js
@@ -0,0 +1,18 @@
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { TabBar } from '../../ui-utils';
+
+FlowRouter.route('/snippet/:snippetId/:snippetName', {
+ name: 'snippetView',
+ action() {
+ BlazeLayout.render('main', { center: 'snippetPage', flexTabBar: null });
+ },
+ triggersEnter: [function() {
+ TabBar.hide();
+ }],
+ triggersExit: [
+ function() {
+ TabBar.show();
+ },
+ ],
+});
diff --git a/app/message-snippet/client/snippetMessage.js b/app/message-snippet/client/snippetMessage.js
new file mode 100644
index 000000000000..c8c75a16c7fd
--- /dev/null
+++ b/app/message-snippet/client/snippetMessage.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { ChatMessage, Subscriptions } from '../../models';
+
+Meteor.methods({
+ snippetMessage(message) {
+ if (typeof Meteor.userId() === 'undefined' || Meteor.userId() === null) {
+ return false;
+ }
+ if ((typeof settings.get('Message_AllowSnippeting') === 'undefined') ||
+ (settings.get('Message_AllowSnippeting') === null) ||
+ (settings.get('Message_AllowSnippeting') === false)) {
+ return false;
+ }
+
+ const subscription = Subscriptions.findOne({ rid: message.rid, 'u._id': Meteor.userId() });
+
+ if (subscription === undefined) {
+ return false;
+ }
+ ChatMessage.update({
+ _id: message._id,
+ }, {
+ $set: {
+ snippeted: true,
+ },
+ });
+ },
+});
diff --git a/app/message-snippet/client/tabBar/tabBar.js b/app/message-snippet/client/tabBar/tabBar.js
new file mode 100644
index 000000000000..4a5c46062743
--- /dev/null
+++ b/app/message-snippet/client/tabBar/tabBar.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { settings } from '../../../settings';
+import { TabBar } from '../../../ui-utils';
+
+Meteor.startup(function() {
+ Tracker.autorun(function() {
+ if (settings.get('Message_AllowSnippeting')) {
+ TabBar.addButton({
+ groups: ['channel', 'group', 'direct'],
+ id: 'snippeted-messages',
+ i18nTitle: 'snippet-message',
+ icon: 'code',
+ template: 'snippetedMessages',
+ order: 20,
+ });
+ } else {
+ TabBar.removeButton('snippeted-messages');
+ }
+ });
+});
diff --git a/app/message-snippet/client/tabBar/views/snippetedMessages.html b/app/message-snippet/client/tabBar/views/snippetedMessages.html
new file mode 100644
index 000000000000..539dabf6e0f7
--- /dev/null
+++ b/app/message-snippet/client/tabBar/views/snippetedMessages.html
@@ -0,0 +1,24 @@
+
+
+
+
+ {{# with messageContext}}
+ {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
+ {{/with}}
+
+ {{#if hasMore}}
+
+ {{> loading}}
+
+ {{/if}}
+
+
diff --git a/app/message-snippet/client/tabBar/views/snippetedMessages.js b/app/message-snippet/client/tabBar/views/snippetedMessages.js
new file mode 100644
index 000000000000..660c08769f96
--- /dev/null
+++ b/app/message-snippet/client/tabBar/views/snippetedMessages.js
@@ -0,0 +1,36 @@
+import _ from 'underscore';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { SnippetedMessages } from '../../lib/collections';
+import { messageContext } from '../../../../ui-utils/client/lib/messageContext';
+
+Template.snippetedMessages.helpers({
+ hasMessages() {
+ return Template.instance().cursor.count() > 0;
+ },
+ messages() {
+ return Template.instance().cursor;
+ },
+ message() {
+ return _.extend(this, { customClass: 'snippeted', actionContext: 'snippeted' });
+ },
+ hasMore() {
+ return Template.instance().hasMore.get();
+ },
+ messageContext,
+});
+
+Template.snippetedMessages.onCreated(function() {
+ this.rid = this.data.rid;
+ this.cursor = SnippetedMessages.find({ snippeted:true, rid: this.data.rid }, { sort: { ts: -1 } });
+ this.hasMore = new ReactiveVar(true);
+ this.limit = new ReactiveVar(50);
+ this.autorun(() => {
+ const data = Template.currentData();
+ this.subscribe('snippetedMessages', data.rid, this.limit.get(), function() {
+ if (this.cursor.count() < this.limit.get()) {
+ return this.hasMore.set(false);
+ }
+ });
+ });
+});
diff --git a/app/message-snippet/server/index.js b/app/message-snippet/server/index.js
new file mode 100644
index 000000000000..4e46076a914f
--- /dev/null
+++ b/app/message-snippet/server/index.js
@@ -0,0 +1,5 @@
+import './startup/settings';
+import './methods/snippetMessage';
+import './requests';
+import './publications/snippetedMessagesByRoom';
+import './publications/snippetedMessage';
diff --git a/app/message-snippet/server/methods/snippetMessage.js b/app/message-snippet/server/methods/snippetMessage.js
new file mode 100644
index 000000000000..351aed9da5f3
--- /dev/null
+++ b/app/message-snippet/server/methods/snippetMessage.js
@@ -0,0 +1,53 @@
+import { Meteor } from 'meteor/meteor';
+import { Subscriptions, Messages, Users, Rooms } from '../../../models';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { isTheLastMessage } from '../../../lib';
+
+Meteor.methods({
+ snippetMessage(message, filename) {
+ if (Meteor.userId() == null) {
+ // noinspection JSUnresolvedFunction
+ throw new Meteor.Error('error-invalid-user', 'Invalid user',
+ { method: 'snippetMessage' });
+ }
+
+ const room = Rooms.findOne({ _id: message.rid });
+
+ if ((typeof room === 'undefined') || (room === null)) {
+ return false;
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
+ if (!subscription) {
+ return false;
+ }
+
+ const me = Users.findOneById(Meteor.userId());
+
+ // If we keep history of edits, insert a new message to store history information
+ if (settings.get('Message_KeepHistory')) {
+ Messages.cloneAndSaveAsHistoryById(message._id, me);
+ }
+
+ message.snippeted = true;
+ message.snippetedAt = Date.now;
+ message.snippetedBy = {
+ _id: Meteor.userId(),
+ username: me.username,
+ };
+
+ message = callbacks.run('beforeSaveMessage', message);
+
+ // Create the SnippetMessage
+ Messages.setSnippetedByIdAndUserId(message, filename, message.snippetedBy,
+ message.snippeted, Date.now, filename);
+ if (isTheLastMessage(room, message)) {
+ Rooms.setLastMessageSnippeted(room._id, message, filename, message.snippetedBy,
+ message.snippeted, Date.now, filename);
+ }
+
+ Messages.createWithTypeRoomIdMessageAndUser(
+ 'message_snippeted', message.rid, '', me, { snippetId: message._id, snippetName: filename });
+ },
+});
diff --git a/app/message-snippet/server/publications/snippetedMessage.js b/app/message-snippet/server/publications/snippetedMessage.js
new file mode 100644
index 000000000000..6a5fe838cd99
--- /dev/null
+++ b/app/message-snippet/server/publications/snippetedMessage.js
@@ -0,0 +1,54 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages, Users, Rooms } from '../../../models';
+
+Meteor.publish('snippetedMessage', function(_id) {
+ if (typeof this.userId === 'undefined' || this.userId === null) {
+ return this.ready();
+ }
+
+ const snippet = Messages.findOne({ _id, snippeted: true });
+ const user = Users.findOneById(this.userId);
+ const roomSnippetQuery = {
+ _id: snippet.rid,
+ usernames: {
+ $in: [
+ user.username,
+ ],
+ },
+ };
+
+ if (!Meteor.call('canAccessRoom', snippet.rid, this.userId)) {
+ return this.ready();
+ }
+
+ if (Rooms.findOne(roomSnippetQuery) === undefined) {
+ return this.ready();
+ }
+
+ const publication = this;
+
+
+ if (typeof user === 'undefined' || user === null) {
+ return this.ready();
+ }
+
+ const cursor = Messages.find(
+ { _id }
+ ).observeChanges({
+ added(_id, record) {
+ publication.added('rocketchat_snippeted_message', _id, record);
+ },
+ changed(_id, record) {
+ publication.changed('rocketchat_snippeted_message', _id, record);
+ },
+ removed(_id) {
+ publication.removed('rocketchat_snippeted_message', _id);
+ },
+ });
+
+ this.ready();
+
+ this.onStop = function() {
+ cursor.stop();
+ };
+});
diff --git a/packages/rocketchat-message-snippet/server/publications/snippetedMessagesByRoom.js b/app/message-snippet/server/publications/snippetedMessagesByRoom.js
similarity index 81%
rename from packages/rocketchat-message-snippet/server/publications/snippetedMessagesByRoom.js
rename to app/message-snippet/server/publications/snippetedMessagesByRoom.js
index b7b6c5a769d3..9dd0073c0720 100644
--- a/packages/rocketchat-message-snippet/server/publications/snippetedMessagesByRoom.js
+++ b/app/message-snippet/server/publications/snippetedMessagesByRoom.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Users, Messages } from '../../../models';
Meteor.publish('snippetedMessages', function(rid, limit = 50) {
if (typeof this.userId === 'undefined' || this.userId === null) {
@@ -8,7 +8,7 @@ Meteor.publish('snippetedMessages', function(rid, limit = 50) {
const publication = this;
- const user = RocketChat.models.Users.findOneById(this.userId);
+ const user = Users.findOneById(this.userId);
if (typeof user === 'undefined' || user === null) {
return this.ready();
@@ -18,7 +18,7 @@ Meteor.publish('snippetedMessages', function(rid, limit = 50) {
return this.ready();
}
- const cursorHandle = RocketChat.models.Messages.findSnippetedByRoom(
+ const cursorHandle = Messages.findSnippetedByRoom(
rid,
{
sort: { ts: -1 },
diff --git a/app/message-snippet/server/requests.js b/app/message-snippet/server/requests.js
new file mode 100644
index 000000000000..2e4048178494
--- /dev/null
+++ b/app/message-snippet/server/requests.js
@@ -0,0 +1,65 @@
+import { WebApp } from 'meteor/webapp';
+import { Cookies } from 'meteor/ostrio:cookies';
+import { Users, Rooms, Messages } from '../../models';
+
+WebApp.connectHandlers.use('/snippet/download', function(req, res) {
+ let rawCookies;
+ let token;
+ let uid;
+ const cookie = new Cookies();
+
+ if (req.headers && req.headers.cookie !== null) {
+ rawCookies = req.headers.cookie;
+ }
+
+ if (rawCookies !== null) {
+ uid = cookie.get('rc_uid', rawCookies);
+ }
+
+ if (rawCookies !== null) {
+ token = cookie.get('rc_token', rawCookies);
+ }
+
+ if (uid === null) {
+ uid = req.query.rc_uid;
+ token = req.query.rc_token;
+ }
+
+ const user = Users.findOneByIdAndLoginToken(uid, token);
+
+ if (!(uid && token && user)) {
+ res.writeHead(403);
+ res.end();
+ return false;
+ }
+ const match = /^\/([^\/]+)\/(.*)/.exec(req.url);
+
+ if (match[1]) {
+ const snippet = Messages.findOne(
+ {
+ _id: match[1],
+ snippeted: true,
+ }
+ );
+ const room = Rooms.findOne({ _id: snippet.rid, usernames: { $in: [user.username] } });
+ if (room === undefined) {
+ res.writeHead(403);
+ res.end();
+ return false;
+ }
+
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(snippet.snippetName) }`);
+ res.setHeader('Content-Type', 'application/octet-stream');
+
+ // Removing the ``` contained in the msg.
+ const snippetContent = snippet.msg.substr(3, snippet.msg.length - 6);
+ res.setHeader('Content-Length', snippetContent.length);
+ res.write(snippetContent);
+ res.end();
+ return;
+ }
+
+ res.writeHead(404);
+ res.end();
+ return;
+});
diff --git a/app/message-snippet/server/startup/settings.js b/app/message-snippet/server/startup/settings.js
new file mode 100644
index 000000000000..1293ed4f166c
--- /dev/null
+++ b/app/message-snippet/server/startup/settings.js
@@ -0,0 +1,17 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../../settings';
+import { Permissions } from '../../../models';
+
+Meteor.startup(function() {
+ settings.add('Message_AllowSnippeting', false, {
+ type: 'boolean',
+ public: true,
+ group: 'Message',
+ });
+ Permissions.upsert('snippet-message', {
+ $setOnInsert: {
+ roles: ['owner', 'moderator', 'admin'],
+ },
+ });
+});
+
diff --git a/app/message-star/client/actionButton.js b/app/message-star/client/actionButton.js
new file mode 100644
index 000000000000..c528cd55810f
--- /dev/null
+++ b/app/message-star/client/actionButton.js
@@ -0,0 +1,103 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { handleError } from '../../utils';
+import { Subscriptions } from '../../models';
+import { settings } from '../../settings';
+import { RoomHistoryManager, MessageAction } from '../../ui-utils';
+import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+import toastr from 'toastr';
+Meteor.startup(function() {
+ MessageAction.addButton({
+ id: 'star-message',
+ icon: 'star',
+ label: 'Star',
+ context: ['starred', 'message', 'message-mobile'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ message.starred = Meteor.userId();
+ Meteor.call('starMessage', message, function(error) {
+ if (error) {
+ return handleError(error);
+ }
+ });
+ },
+ condition(message) {
+ if (Subscriptions.findOne({ rid: message.rid }) == null && settings.get('Message_AllowStarring')) {
+ return false;
+ }
+
+ return !message.starred || !message.starred.find((star) => star._id === Meteor.userId());
+ },
+ order: 9,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'unstar-message',
+ icon: 'star',
+ label: 'Unstar_Message',
+ context: ['starred', 'message', 'message-mobile'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ message.starred = false;
+ Meteor.call('starMessage', message, function(error) {
+ if (error) {
+ handleError(error);
+ }
+ });
+ },
+ condition(message) {
+ if (Subscriptions.findOne({ rid: message.rid }) == null && settings.get('Message_AllowStarring')) {
+ return false;
+ }
+
+ return message.starred && message.starred.find((star) => star._id === Meteor.userId());
+ },
+ order: 9,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'jump-to-star-message',
+ icon: 'jump',
+ label: 'Jump_to_message',
+ context: ['starred'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ if (window.matchMedia('(max-width: 500px)').matches) {
+ Template.instance().tabBar.close();
+ }
+ RoomHistoryManager.getSurroundingMessages(message, 50);
+ },
+ condition(message) {
+ if (Subscriptions.findOne({ rid: message.rid }) == null) {
+ return false;
+ }
+ return true;
+ },
+ order: 100,
+ group: 'menu',
+ });
+
+ MessageAction.addButton({
+ id: 'permalink-star',
+ icon: 'permalink',
+ label: 'Get_link',
+ classes: 'clipboard',
+ context: ['starred'],
+ async action(event) {
+ const { msg: message } = messageArgs(this);
+ $(event.currentTarget).attr('data-clipboard-text', await MessageAction.getPermaLink(message._id));
+ toastr.success(TAPi18n.__('Copied'));
+ },
+ condition(message) {
+ if (Subscriptions.findOne({ rid: message.rid }) == null) {
+ return false;
+ }
+ return true;
+ },
+ order: 101,
+ group: 'menu',
+ });
+});
diff --git a/packages/rocketchat-message-star/client/index.js b/app/message-star/client/index.js
similarity index 100%
rename from packages/rocketchat-message-star/client/index.js
rename to app/message-star/client/index.js
diff --git a/packages/rocketchat-message-star/client/lib/StarredMessage.js b/app/message-star/client/lib/StarredMessage.js
similarity index 100%
rename from packages/rocketchat-message-star/client/lib/StarredMessage.js
rename to app/message-star/client/lib/StarredMessage.js
diff --git a/app/message-star/client/starMessage.js b/app/message-star/client/starMessage.js
new file mode 100644
index 000000000000..04d15d194cb7
--- /dev/null
+++ b/app/message-star/client/starMessage.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { ChatMessage, Subscriptions } from '../../models';
+
+Meteor.methods({
+ starMessage(message) {
+ if (!Meteor.userId()) {
+ return false;
+ }
+ if (Subscriptions.findOne({ rid: message.rid }) == null) {
+ return false;
+ }
+ if (!settings.get('Message_AllowStarring')) {
+ return false;
+ }
+ return ChatMessage.update({
+ _id: message._id,
+ }, {
+ $set: {
+ starred: !!message.starred,
+ },
+ });
+ },
+});
diff --git a/app/message-star/client/tabBar.js b/app/message-star/client/tabBar.js
new file mode 100644
index 000000000000..2e915e48c7da
--- /dev/null
+++ b/app/message-star/client/tabBar.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { TabBar } from '../../ui-utils';
+
+Meteor.startup(function() {
+ TabBar.addButton({
+ groups: ['channel', 'group', 'direct'],
+ id: 'starred-messages',
+ i18nTitle: 'Starred_Messages',
+ icon: 'star',
+ template: 'starredMessages',
+ order: 3,
+ });
+});
diff --git a/app/message-star/client/views/starredMessages.html b/app/message-star/client/views/starredMessages.html
new file mode 100644
index 000000000000..89a8e8c7159d
--- /dev/null
+++ b/app/message-star/client/views/starredMessages.html
@@ -0,0 +1,21 @@
+
+ {{#if Template.subscriptionsReady}}
+ {{#unless hasMessages}}
+
+ {{/unless}}
+ {{/if}}
+
+
+ {{# with messageContext}}
+ {{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
+ {{/with}}
+
+ {{#if hasMore}}
+
+ {{> loading}}
+
+ {{/if}}
+
+
diff --git a/app/message-star/client/views/starredMessages.js b/app/message-star/client/views/starredMessages.js
new file mode 100644
index 000000000000..0326db20f72c
--- /dev/null
+++ b/app/message-star/client/views/starredMessages.js
@@ -0,0 +1,51 @@
+import _ from 'underscore';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Template } from 'meteor/templating';
+import { StarredMessage } from '../lib/StarredMessage';
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
+
+Template.starredMessages.helpers({
+ hasMessages() {
+ return Template.instance().cursor.count() > 0;
+ },
+ messages() {
+ return Template.instance().cursor;
+ },
+ message() {
+ return _.extend(this, { actionContext: 'starred' });
+ },
+ hasMore() {
+ return Template.instance().hasMore.get();
+ },
+ messageContext,
+});
+
+Template.starredMessages.onCreated(function() {
+ this.rid = this.data.rid;
+
+ this.cursor = StarredMessage.find({
+ rid: this.data.rid,
+ }, {
+ sort: {
+ ts: -1,
+ },
+ });
+ this.hasMore = new ReactiveVar(true);
+ this.limit = new ReactiveVar(50);
+ this.autorun(() => {
+ const sub = this.subscribe('starredMessages', this.data.rid, this.limit.get());
+ if (sub.ready()) {
+ if (this.cursor.count() < this.limit.get()) {
+ return this.hasMore.set(false);
+ }
+ }
+ });
+});
+
+Template.starredMessages.events({
+ 'scroll .js-list': _.throttle(function(e, instance) {
+ if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) {
+ return instance.limit.set(instance.limit.get() + 50);
+ }
+ }, 200),
+});
diff --git a/packages/rocketchat-message-star/client/views/stylesheets/messagestar.css b/app/message-star/client/views/stylesheets/messagestar.css
similarity index 100%
rename from packages/rocketchat-message-star/client/views/stylesheets/messagestar.css
rename to app/message-star/client/views/stylesheets/messagestar.css
diff --git a/app/message-star/server/index.js b/app/message-star/server/index.js
new file mode 100644
index 000000000000..ceeedf7d8669
--- /dev/null
+++ b/app/message-star/server/index.js
@@ -0,0 +1,4 @@
+import './settings';
+import './starMessage';
+import './publications/starredMessages';
+import './startup/indexes';
diff --git a/app/message-star/server/publications/starredMessages.js b/app/message-star/server/publications/starredMessages.js
new file mode 100644
index 000000000000..9973adbf94c1
--- /dev/null
+++ b/app/message-star/server/publications/starredMessages.js
@@ -0,0 +1,36 @@
+import { Meteor } from 'meteor/meteor';
+import { Users, Messages } from '../../../models';
+
+Meteor.publish('starredMessages', function(rid, limit = 50) {
+ if (!this.userId) {
+ return this.ready();
+ }
+ const publication = this;
+ const user = Users.findOneById(this.userId);
+ if (!user) {
+ return this.ready();
+ }
+ if (!Meteor.call('canAccessRoom', rid, this.userId)) {
+ return this.ready();
+ }
+ const cursorHandle = Messages.findStarredByUserAtRoom(this.userId, rid, {
+ sort: {
+ ts: -1,
+ },
+ limit,
+ }).observeChanges({
+ added(_id, record) {
+ return publication.added('rocketchat_starred_message', _id, record);
+ },
+ changed(_id, record) {
+ return publication.changed('rocketchat_starred_message', _id, record);
+ },
+ removed(_id) {
+ return publication.removed('rocketchat_starred_message', _id);
+ },
+ });
+ this.ready();
+ return this.onStop(function() {
+ return cursorHandle.stop();
+ });
+});
diff --git a/app/message-star/server/settings.js b/app/message-star/server/settings.js
new file mode 100644
index 000000000000..05ec4f0f1d62
--- /dev/null
+++ b/app/message-star/server/settings.js
@@ -0,0 +1,10 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ return settings.add('Message_AllowStarring', true, {
+ type: 'boolean',
+ group: 'Message',
+ public: true,
+ });
+});
diff --git a/app/message-star/server/starMessage.js b/app/message-star/server/starMessage.js
new file mode 100644
index 000000000000..7b063d012d6d
--- /dev/null
+++ b/app/message-star/server/starMessage.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { isTheLastMessage } from '../../lib';
+import { Subscriptions, Rooms, Messages } from '../../models';
+
+Meteor.methods({
+ starMessage(message) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'starMessage',
+ });
+ }
+
+ if (!settings.get('Message_AllowStarring')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Message starring not allowed', {
+ method: 'pinMessage',
+ action: 'Message_starring',
+ });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { fields: { _id: 1 } });
+ if (!subscription) {
+ return false;
+ }
+ const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
+ if (isTheLastMessage(room, message)) {
+ Rooms.updateLastMessageStar(room._id, Meteor.userId(), message.starred);
+ }
+
+ return Messages.updateUserStarById(message._id, Meteor.userId(), message.starred);
+ },
+});
diff --git a/app/message-star/server/startup/indexes.js b/app/message-star/server/startup/indexes.js
new file mode 100644
index 000000000000..464dbc661808
--- /dev/null
+++ b/app/message-star/server/startup/indexes.js
@@ -0,0 +1,12 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages } from '../../../models';
+
+Meteor.startup(function() {
+ return Meteor.defer(function() {
+ return Messages.tryEnsureIndex({
+ 'starred._id': 1,
+ }, {
+ sparse: 1,
+ });
+ });
+});
diff --git a/packages/meteor-accounts-saml/CHANGELOG.md b/app/meteor-accounts-saml/CHANGELOG.md
similarity index 100%
rename from packages/meteor-accounts-saml/CHANGELOG.md
rename to app/meteor-accounts-saml/CHANGELOG.md
diff --git a/packages/meteor-accounts-saml/README.md b/app/meteor-accounts-saml/README.md
similarity index 100%
rename from packages/meteor-accounts-saml/README.md
rename to app/meteor-accounts-saml/README.md
diff --git a/packages/meteor-accounts-saml/client/index.js b/app/meteor-accounts-saml/client/index.js
similarity index 100%
rename from packages/meteor-accounts-saml/client/index.js
rename to app/meteor-accounts-saml/client/index.js
diff --git a/packages/meteor-accounts-saml/client/saml_client.js b/app/meteor-accounts-saml/client/saml_client.js
similarity index 96%
rename from packages/meteor-accounts-saml/client/saml_client.js
rename to app/meteor-accounts-saml/client/saml_client.js
index c4d1e44ebb37..36c33e1d3e83 100644
--- a/packages/meteor-accounts-saml/client/saml_client.js
+++ b/app/meteor-accounts-saml/client/saml_client.js
@@ -1,6 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
-import { Random } from 'meteor/random';
import { ServiceConfiguration } from 'meteor/service-configuration';
if (!Accounts.saml) {
@@ -95,14 +94,12 @@ Accounts.saml.initiateLogin = function(options, callback, dimensions) {
Meteor.loginWithSaml = function(options, callback) {
options = options || {};
- const credentialToken = `id-${ Random.id() }`;
- options.credentialToken = credentialToken;
+ options.credentialToken = Meteor.default_connection._lastSessionId;
Accounts.saml.initiateLogin(options, function(/* error, result*/) {
Accounts.callLoginMethod({
methodArguments: [{
saml: true,
- credentialToken,
}],
userCallback: callback,
});
diff --git a/packages/meteor-accounts-saml/server/index.js b/app/meteor-accounts-saml/server/index.js
similarity index 100%
rename from packages/meteor-accounts-saml/server/index.js
rename to app/meteor-accounts-saml/server/index.js
diff --git a/app/meteor-accounts-saml/server/saml_rocketchat.js b/app/meteor-accounts-saml/server/saml_rocketchat.js
new file mode 100644
index 000000000000..f6b7f938c6e3
--- /dev/null
+++ b/app/meteor-accounts-saml/server/saml_rocketchat.js
@@ -0,0 +1,209 @@
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+import { Logger } from '../../logger';
+import { ServiceConfiguration } from 'meteor/service-configuration';
+import { settings } from '../../settings';
+
+const logger = new Logger('steffo:meteor-accounts-saml', {
+ methods: {
+ updated: {
+ type: 'info',
+ },
+ },
+});
+
+settings.addGroup('SAML');
+
+Meteor.methods({
+ addSamlService(name) {
+ settings.add(`SAML_Custom_${ name }`, false, {
+ type: 'boolean',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'Accounts_OAuth_Custom_Enable',
+ });
+ settings.add(`SAML_Custom_${ name }_provider`, 'provider-name', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Provider',
+ });
+ settings.add(`SAML_Custom_${ name }_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Entry_point',
+ });
+ settings.add(`SAML_Custom_${ name }_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL',
+ });
+ settings.add(`SAML_Custom_${ name }_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Issuer',
+ });
+ settings.add(`SAML_Custom_${ name }_cert`, '', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Cert',
+ multiline: true,
+ });
+ settings.add(`SAML_Custom_${ name }_public_cert`, '', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ multiline: true,
+ i18nLabel: 'SAML_Custom_Public_Cert',
+ });
+ settings.add(`SAML_Custom_${ name }_private_key`, '', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ multiline: true,
+ i18nLabel: 'SAML_Custom_Private_Key',
+ });
+ settings.add(`SAML_Custom_${ name }_button_label_text`, '', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text',
+ });
+ settings.add(`SAML_Custom_${ name }_button_label_color`, '#FFFFFF', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color',
+ });
+ settings.add(`SAML_Custom_${ name }_button_color`, '#1d74f5', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'Accounts_OAuth_Custom_Button_Color',
+ });
+ settings.add(`SAML_Custom_${ name }_generate_username`, false, {
+ type: 'boolean',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Generate_Username',
+ });
+ settings.add(`SAML_Custom_${ name }_debug`, false, {
+ type: 'boolean',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Debug',
+ });
+ settings.add(`SAML_Custom_${ name }_logout_behaviour`, 'SAML', {
+ type: 'select',
+ values: [
+ { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' },
+ { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' },
+ ],
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Logout_Behaviour',
+ });
+ },
+});
+
+const normalizeCert = function(cert) {
+ if (typeof cert === 'string') {
+ return cert.replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').trim();
+ }
+
+ return cert;
+};
+
+const getSamlConfigs = function(service) {
+ return {
+ buttonLabelText: settings.get(`${ service.key }_button_label_text`),
+ buttonLabelColor: settings.get(`${ service.key }_button_label_color`),
+ buttonColor: settings.get(`${ service.key }_button_color`),
+ clientConfig: {
+ provider: settings.get(`${ service.key }_provider`),
+ },
+ entryPoint: settings.get(`${ service.key }_entry_point`),
+ idpSLORedirectURL: settings.get(`${ service.key }_idp_slo_redirect_url`),
+ generateUsername: settings.get(`${ service.key }_generate_username`),
+ debug: settings.get(`${ service.key }_debug`),
+ issuer: settings.get(`${ service.key }_issuer`),
+ logoutBehaviour: settings.get(`${ service.key }_logout_behaviour`),
+ secret: {
+ privateKey: settings.get(`${ service.key }_private_key`),
+ publicCert: settings.get(`${ service.key }_public_cert`),
+ // People often overlook the instruction to remove the header and footer of the certificate on this specific setting, so let's do it for them.
+ cert: normalizeCert(settings.get(`${ service.key }_cert`)),
+ },
+ };
+};
+
+const debounce = (fn, delay) => {
+ let timer = null;
+ return () => {
+ if (timer != null) {
+ Meteor.clearTimeout(timer);
+ }
+ return timer = Meteor.setTimeout(fn, delay);
+ };
+};
+const serviceName = 'saml';
+
+const configureSamlService = function(samlConfigs) {
+ let privateCert = false;
+ let privateKey = false;
+ if (samlConfigs.secret.privateKey && samlConfigs.secret.publicCert) {
+ privateKey = samlConfigs.secret.privateKey;
+ privateCert = samlConfigs.secret.publicCert;
+ } else if (samlConfigs.secret.privateKey || samlConfigs.secret.publicCert) {
+ logger.error('You must specify both cert and key files.');
+ }
+ // TODO: the function configureSamlService is called many times and Accounts.saml.settings.generateUsername keeps just the last value
+ Accounts.saml.settings.generateUsername = samlConfigs.generateUsername;
+ Accounts.saml.settings.debug = samlConfigs.debug;
+
+ return {
+ provider: samlConfigs.clientConfig.provider,
+ entryPoint: samlConfigs.entryPoint,
+ idpSLORedirectURL: samlConfigs.idpSLORedirectURL,
+ issuer: samlConfigs.issuer,
+ cert: samlConfigs.secret.cert,
+ privateCert,
+ privateKey,
+ };
+};
+
+const updateServices = debounce(() => {
+ const services = settings.get(/^(SAML_Custom_)[a-z]+$/i);
+ Accounts.saml.settings.providers = services.map((service) => {
+ if (service.value === true) {
+ const samlConfigs = getSamlConfigs(service);
+ logger.updated(service.key);
+ ServiceConfiguration.configurations.upsert({
+ service: serviceName.toLowerCase(),
+ }, {
+ $set: samlConfigs,
+ });
+ return configureSamlService(samlConfigs);
+ }
+ return ServiceConfiguration.configurations.remove({
+ service: serviceName.toLowerCase(),
+ });
+ }).filter((e) => e);
+}, 2000);
+
+
+settings.get(/^SAML_.+/, updateServices);
+
+Meteor.startup(() => Meteor.call('addSamlService', 'Default'));
+
+export {
+ updateServices,
+ configureSamlService,
+ getSamlConfigs,
+ debounce,
+ logger,
+};
diff --git a/packages/meteor-accounts-saml/server/saml_server.js b/app/meteor-accounts-saml/server/saml_server.js
similarity index 78%
rename from packages/meteor-accounts-saml/server/saml_server.js
rename to app/meteor-accounts-saml/server/saml_server.js
index 8d4ccddde087..ae750b8a79f8 100644
--- a/packages/meteor-accounts-saml/server/saml_server.js
+++ b/app/meteor-accounts-saml/server/saml_server.js
@@ -2,10 +2,12 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Random } from 'meteor/random';
import { WebApp } from 'meteor/webapp';
-import { RocketChat } from 'meteor/rocketchat:lib';
import { RoutePolicy } from 'meteor/routepolicy';
-import bodyParser from 'body-parser';
+import { CredentialTokens } from '../../models';
+import { generateUsernameSuggestion } from '../../lib';
import { SAML } from './saml_utils';
+import bodyParser from 'body-parser';
+import fiber from 'fibers';
import _ from 'underscore';
if (!Accounts.saml) {
@@ -18,7 +20,6 @@ if (!Accounts.saml) {
};
}
-import fiber from 'fibers';
RoutePolicy.declare('/_saml/', 'network');
/**
@@ -91,11 +92,11 @@ Meteor.methods({
});
Accounts.registerLoginHandler(function(loginRequest) {
- if (!loginRequest.saml || !loginRequest.credentialToken) {
+ if (!loginRequest.saml) {
return undefined;
}
- const loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
+ const loginResult = Accounts.saml.retrieveCredential(this.connection.id);
if (Accounts.saml.settings.debug) {
console.log(`RESULT :${ JSON.stringify(loginResult) }`);
}
@@ -116,7 +117,7 @@ Accounts.registerLoginHandler(function(loginRequest) {
if (!user) {
const newUser = {
- name: loginResult.profile.cn || loginResult.profile.username,
+ name: loginResult.profile.displayName || loginResult.profile.cn || loginResult.profile.username,
active: true,
globalRoles: ['user'],
emails: emailList.map((email) => ({
@@ -126,7 +127,7 @@ Accounts.registerLoginHandler(function(loginRequest) {
};
if (Accounts.saml.settings.generateUsername === true) {
- const username = RocketChat.generateUsernameSuggestion(newUser);
+ const username = generateUsernameSuggestion(newUser);
if (username) {
newUser.username = username;
}
@@ -176,19 +177,19 @@ Accounts.registerLoginHandler(function(loginRequest) {
});
Accounts.saml.hasCredential = function(credentialToken) {
- return RocketChat.models.CredentialTokens.findOneById(credentialToken) != null;
+ return CredentialTokens.findOneById(credentialToken) != null;
};
Accounts.saml.retrieveCredential = function(credentialToken) {
// The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check.
- const data = RocketChat.models.CredentialTokens.findOneById(credentialToken);
+ const data = CredentialTokens.findOneById(credentialToken);
if (data) {
return data.userInfo;
}
};
Accounts.saml.storeCredential = function(credentialToken, loginResult) {
- RocketChat.models.CredentialTokens.create(credentialToken, loginResult);
+ CredentialTokens.create(credentialToken, loginResult);
};
const closePopup = function(res, err) {
@@ -228,6 +229,28 @@ const samlUrlToObject = function(url) {
return result;
};
+const logoutRemoveTokens = function(userId) {
+ if (Accounts.saml.settings.debug) {
+ console.log(`Found user ${ userId }`);
+ }
+
+ Meteor.users.update({
+ _id: userId,
+ }, {
+ $set: {
+ 'services.resume.loginTokens': [],
+ },
+ });
+
+ Meteor.users.update({
+ _id: userId,
+ }, {
+ $unset: {
+ 'services.saml': '',
+ },
+ });
+};
+
const middleware = function(req, res, next) {
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
@@ -267,52 +290,82 @@ const middleware = function(req, res, next) {
case 'logout':
// This is where we receive SAML LogoutResponse
_saml = new SAML(service);
- _saml.validateLogoutResponse(req.query.SAMLResponse, function(err, result) {
- if (!err) {
- const logOutUser = function(inResponseTo) {
- if (Accounts.saml.settings.debug) {
- console.log(`Logging Out user via inResponseTo ${ inResponseTo }`);
- }
+ if (req.query.SAMLRequest) {
+ _saml.validateLogoutRequest(req.query.SAMLRequest, function(err, result) {
+ if (err) {
+ console.error(err);
+ throw new Meteor.Error('Unable to Validate Logout Request');
+ }
+
+ const logOutUser = function(samlInfo) {
const loggedOutUser = Meteor.users.find({
- 'services.saml.inResponseTo': inResponseTo,
+ $or: [
+ { 'services.saml.nameID': samlInfo.nameID },
+ { 'services.saml.idpSession': samlInfo.idpSession },
+ ],
}).fetch();
+
if (loggedOutUser.length === 1) {
- if (Accounts.saml.settings.debug) {
- console.log(`Found user ${ loggedOutUser[0]._id }`);
- }
- Meteor.users.update({
- _id: loggedOutUser[0]._id,
- }, {
- $set: {
- 'services.resume.loginTokens': [],
- },
- });
- Meteor.users.update({
- _id: loggedOutUser[0]._id,
- }, {
- $unset: {
- 'services.saml': '',
- },
- });
- } else {
- throw new Meteor.Error('Found multiple users matching SAML inResponseTo fields');
+ logoutRemoveTokens(loggedOutUser[0]._id);
}
+
};
fiber(function() {
logOutUser(result);
}).run();
+ const { response } = _saml.generateLogoutResponse({
+ nameID: result.nameID,
+ sessionIndex: result.idpSession,
+ });
+
+ _saml.logoutResponseToUrl(response, function(err, url) {
+ if (err) {
+ console.error(err);
+ throw new Meteor.Error('Unable to generate SAML logout Response Url');
+ }
+
+ res.writeHead(302, {
+ Location: url,
+ });
+ res.end();
- res.writeHead(302, {
- Location: req.query.RelayState,
});
- res.end();
- }
- // else {
- // // TBD thinking of sth meaning full.
- // }
- });
+
+ });
+ } else {
+ _saml.validateLogoutResponse(req.query.SAMLResponse, function(err, result) {
+ if (!err) {
+ const logOutUser = function(inResponseTo) {
+ if (Accounts.saml.settings.debug) {
+ console.log(`Logging Out user via inResponseTo ${ inResponseTo }`);
+ }
+ const loggedOutUser = Meteor.users.find({
+ 'services.saml.inResponseTo': inResponseTo,
+ }).fetch();
+ if (loggedOutUser.length === 1) {
+ logoutRemoveTokens(loggedOutUser[0]._id);
+ } else {
+ throw new Meteor.Error('Found multiple users matching SAML inResponseTo fields');
+ }
+ };
+
+ fiber(function() {
+ logOutUser(result);
+ }).run();
+
+
+ res.writeHead(302, {
+ Location: req.query.RelayState,
+ });
+ res.end();
+ }
+ // else {
+ // // TBD thinking of sth meaning full.
+ // }
+ });
+ }
break;
case 'sloRedirect':
res.writeHead(302, {
diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js
new file mode 100644
index 000000000000..e11c6225d166
--- /dev/null
+++ b/app/meteor-accounts-saml/server/saml_utils.js
@@ -0,0 +1,661 @@
+import { Meteor } from 'meteor/meteor';
+import zlib from 'zlib';
+import xmlCrypto from 'xml-crypto';
+import crypto from 'crypto';
+import xmldom from 'xmldom';
+import querystring from 'querystring';
+import xmlbuilder from 'xmlbuilder';
+import array2string from 'arraybuffer-to-string';
+import xmlenc from 'xml-encryption';
+// var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
+
+
+export const SAML = function(options) {
+ this.options = this.initialize(options);
+};
+
+function debugLog(...args) {
+ if (Meteor.settings.debug) {
+ console.log.apply(this, args);
+ }
+}
+
+// var stripPrefix = function(str) {
+// return str.replace(prefixMatch, '');
+// };
+
+SAML.prototype.initialize = function(options) {
+ if (!options) {
+ options = {};
+ }
+
+ if (!options.protocol) {
+ options.protocol = 'https://';
+ }
+
+ if (!options.path) {
+ options.path = '/saml/consume';
+ }
+
+ if (!options.issuer) {
+ options.issuer = 'onelogin_saml';
+ }
+
+ if (options.identifierFormat === undefined) {
+ options.identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
+ }
+
+ if (options.authnContext === undefined) {
+ options.authnContext = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
+ }
+
+ return options;
+};
+
+SAML.prototype.generateUniqueID = function() {
+ const chars = 'abcdef0123456789';
+ let uniqueID = 'id-';
+ for (let i = 0; i < 20; i++) {
+ uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
+ }
+ return uniqueID;
+};
+
+SAML.prototype.generateInstant = function() {
+ return new Date().toISOString();
+};
+
+SAML.prototype.signRequest = function(xml) {
+ const signer = crypto.createSign('RSA-SHA1');
+ signer.update(xml);
+ return signer.sign(this.options.privateKey, 'base64');
+};
+
+SAML.prototype.generateAuthorizeRequest = function(req) {
+ let id = `_${ this.generateUniqueID() }`;
+ const instant = this.generateInstant();
+
+ // Post-auth destination
+ let callbackUrl;
+ if (this.options.callbackUrl) {
+ callbackUrl = this.options.callbackUrl;
+ } else {
+ callbackUrl = this.options.protocol + req.headers.host + this.options.path;
+ }
+
+ if (this.options.id) {
+ id = this.options.id;
+ }
+
+ let request =
+ `
` +
+ `${ this.options.issuer } \n`;
+
+ if (this.options.identifierFormat) {
+ request += ` \n`;
+ }
+
+ request +=
+ '' +
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport \n' +
+ ' ';
+
+ return request;
+};
+
+SAML.prototype.generateLogoutResponse = function() {
+ const id = `_${ this.generateUniqueID() }`;
+ const instant = this.generateInstant();
+
+
+ const response = `${ '
' +
+ `${ this.options.issuer } ` +
+ ' ' +
+ ' ';
+
+ debugLog('------- SAML Logout response -----------');
+ debugLog(response);
+
+ return {
+ response,
+ id,
+ };
+
+};
+
+SAML.prototype.generateLogoutRequest = function(options) {
+ // options should be of the form
+ // nameId:
+ // sessionIndex: sessionIndex
+ // --- NO SAMLsettings: ' +
+ `${ this.options.issuer } ` +
+ '${
+ options.nameID } ` +
+ `${ options.sessionIndex } ` +
+ '';
+
+ debugLog('------- SAML Logout request -----------');
+ debugLog(request);
+
+ return {
+ request,
+ id,
+ };
+};
+
+SAML.prototype.logoutResponseToUrl = function(response, callback) {
+ const self = this;
+
+ zlib.deflateRaw(response, function(err, buffer) {
+ if (err) {
+ return callback(err);
+ }
+
+ const base64 = buffer.toString('base64');
+ let target = self.options.idpSLORedirectURL;
+
+ if (target.indexOf('?') > 0) {
+ target += '&';
+ } else {
+ target += '?';
+ }
+
+ // TBD. We should really include a proper RelayState here
+ const relayState = Meteor.absoluteUrl();
+
+ const samlResponse = {
+ SAMLResponse: base64,
+ RelayState: relayState,
+ };
+
+ if (self.options.privateCert) {
+ samlResponse.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
+ samlResponse.Signature = self.signRequest(querystring.stringify(samlResponse));
+ }
+
+ target += querystring.stringify(samlResponse);
+
+ return callback(null, target);
+ });
+};
+
+SAML.prototype.requestToUrl = function(request, operation, callback) {
+ const self = this;
+ zlib.deflateRaw(request, function(err, buffer) {
+ if (err) {
+ return callback(err);
+ }
+
+ const base64 = buffer.toString('base64');
+ let target = self.options.entryPoint;
+
+ if (operation === 'logout') {
+ if (self.options.idpSLORedirectURL) {
+ target = self.options.idpSLORedirectURL;
+ }
+ }
+
+ if (target.indexOf('?') > 0) {
+ target += '&';
+ } else {
+ target += '?';
+ }
+
+ // TBD. We should really include a proper RelayState here
+ let relayState;
+ if (operation === 'logout') {
+ // in case of logout we want to be redirected back to the Meteor app.
+ relayState = Meteor.absoluteUrl();
+ } else {
+ relayState = self.options.provider;
+ }
+
+ const samlRequest = {
+ SAMLRequest: base64,
+ RelayState: relayState,
+ };
+
+ if (self.options.privateCert) {
+ samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
+ samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
+ }
+
+ target += querystring.stringify(samlRequest);
+
+ debugLog(`requestToUrl: ${ target }`);
+
+ if (operation === 'logout') {
+ // in case of logout we want to be redirected back to the Meteor app.
+ return callback(null, target);
+
+ } else {
+ callback(null, target);
+ }
+ });
+};
+
+SAML.prototype.getAuthorizeUrl = function(req, callback) {
+ const request = this.generateAuthorizeRequest(req);
+
+ this.requestToUrl(request, 'authorize', callback);
+};
+
+SAML.prototype.getLogoutUrl = function(req, callback) {
+ const request = this.generateLogoutRequest(req);
+
+ this.requestToUrl(request, 'logout', callback);
+};
+
+SAML.prototype.certToPEM = function(cert) {
+ cert = cert.match(/.{1,64}/g).join('\n');
+ cert = `-----BEGIN CERTIFICATE-----\n${ cert }`;
+ cert = `${ cert }\n-----END CERTIFICATE-----\n`;
+ return cert;
+};
+
+// functionfindChilds(node, localName, namespace) {
+// var res = [];
+// for (var i = 0; i < node.childNodes.length; i++) {
+// var child = node.childNodes[i];
+// if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) {
+// res.push(child);
+// }
+// }
+// return res;
+// }
+
+SAML.prototype.validateStatus = function(doc) {
+ let successStatus = false;
+ let status = '';
+ let messageText = '';
+ const statusNodes = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusCode');
+
+ if (statusNodes.length) {
+
+ const statusNode = statusNodes[0];
+ const statusMessage = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage')[0];
+
+ if (statusMessage) {
+ messageText = statusMessage.firstChild.textContent;
+ }
+
+ status = statusNode.getAttribute('Value');
+
+ if (status === 'urn:oasis:names:tc:SAML:2.0:status:Success') {
+ successStatus = true;
+ }
+ }
+ return {
+ success: successStatus,
+ message: messageText,
+ statusCode: status,
+ };
+};
+
+SAML.prototype.validateSignature = function(xml, cert) {
+ const self = this;
+
+ const doc = new xmldom.DOMParser().parseFromString(xml);
+ const signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0];
+
+ const sig = new xmlCrypto.SignedXml();
+
+ sig.keyInfoProvider = {
+ getKeyInfo(/* key*/) {
+ return ' ';
+ },
+ getKey(/* keyInfo*/) {
+ return self.certToPEM(cert);
+ },
+ };
+
+ sig.loadSignature(signature);
+
+ return sig.checkSignature(xml);
+};
+
+SAML.prototype.validateLogoutRequest = function(samlRequest, callback) {
+ const compressedSAMLRequest = new Buffer(samlRequest, 'base64');
+ zlib.inflateRaw(compressedSAMLRequest, function(err, decoded) {
+ if (err) {
+ debugLog(`Error while inflating. ${ err }`);
+ return callback(err, null);
+ }
+
+ debugLog(`LogoutRequest: ${ decoded }`);
+ const doc = new xmldom.DOMParser().parseFromString(array2string(decoded), 'text/xml');
+ if (!doc) {
+ return callback('No Doc Found');
+ }
+
+ const request = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutRequest')[0];
+ if (!request) {
+ return callback('No Request Found');
+ }
+
+ try {
+ const sessionNode = request.getElementsByTagName('samlp:SessionIndex')[0];
+ const nameIdNode = request.getElementsByTagName('saml:NameID')[0];
+
+ const idpSession = sessionNode.childNodes[0].nodeValue;
+ const nameID = nameIdNode.childNodes[0].nodeValue;
+
+ return callback(null, { idpSession, nameID });
+
+ } catch (e) {
+ debugLog(`Caught error: ${ e }`);
+
+ const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage');
+ debugLog(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${ msg }`);
+
+ return callback(e, null);
+ }
+ });
+};
+
+SAML.prototype.validateLogoutResponse = function(samlResponse, callback) {
+ const self = this;
+ const compressedSAMLResponse = new Buffer(samlResponse, 'base64');
+ zlib.inflateRaw(compressedSAMLResponse, function(err, decoded) {
+ if (err) {
+ debugLog(`Error while inflating. ${ err }`);
+ return callback(err, null);
+ }
+
+ debugLog(`LogoutResponse: ${ decoded }`);
+ const doc = new xmldom.DOMParser().parseFromString(array2string(decoded), 'text/xml');
+ if (!doc) {
+ return callback('No Doc Found');
+ }
+
+ const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse')[0];
+ if (!response) {
+ return callback('No Response Found', null);
+ }
+
+ // TBD. Check if this msg corresponds to one we sent
+ let inResponseTo;
+ try {
+ inResponseTo = response.getAttribute('InResponseTo');
+ debugLog(`In Response to: ${ inResponseTo }`);
+ } catch (e) {
+ debugLog(`Caught error: ${ e }`);
+ const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage');
+ debugLog(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${ msg }`);
+ }
+
+ const statusValidateObj = self.validateStatus(doc);
+ if (!statusValidateObj.success) {
+ return callback('Error. Logout not confirmed by IDP', null);
+ }
+ return callback(null, inResponseTo);
+ });
+};
+
+SAML.prototype.mapAttributes = function(attributeStatement, profile) {
+ debugLog(`Attribute Statement found in SAML response: ${ attributeStatement }`);
+ const attributes = attributeStatement.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Attribute');
+ debugLog(`Attributes will be processed: ${ attributes.length }`);
+
+ if (attributes) {
+ for (let i = 0; i < attributes.length; i++) {
+ const values = attributes[i].getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeValue');
+ let value;
+ if (values.length === 1) {
+ value = values[0].textContent;
+ } else {
+ value = [];
+ for (let j = 0;j < values.length;j++) {
+ value.push(values[j].textContent);
+ }
+ }
+
+ const key = attributes[i].getAttribute('Name');
+
+ debugLog(`Name: ${ attributes[i] }`);
+ debugLog(`Adding attribute from SAML response to profile: ${ key } = ${ value }`);
+ profile[key] = value;
+ }
+ } else {
+ debugLog('No Attributes found in SAML attribute statement.');
+ }
+
+ if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
+ // See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
+ profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
+ }
+
+ if (!profile.email && profile['urn:oid:1.2.840.113549.1.9.1']) {
+ profile.email = profile['urn:oid:1.2.840.113549.1.9.1'];
+ }
+
+ if (!profile.email && profile.mail) {
+ profile.email = profile.mail;
+ }
+
+ if (!profile.displayName && profile['urn:oid:2.16.840.1.113730.3.1.241']) {
+ profile.displayName = profile['urn:oid:2.16.840.1.113730.3.1.241'];
+ }
+
+ if (!profile.cn && profile['urn:oid:2.5.4.3']) {
+ profile.cn = profile['urn:oid:2.5.4.3'];
+ }
+
+};
+
+SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
+ const self = this;
+ const xml = new Buffer(samlResponse, 'base64').toString('utf8');
+ // We currently use RelayState to save SAML provider
+ debugLog(`Validating response with relay state: ${ xml }`);
+
+ const doc = new xmldom.DOMParser().parseFromString(xml, 'text/xml');
+ if (!doc) {
+ return callback('No Doc Found');
+ }
+
+ debugLog('Verify status');
+ const statusValidateObj = self.validateStatus(doc);
+
+ if (!statusValidateObj.success) {
+ return callback(new Error(`Status is: ${ statusValidateObj.statusCode }`), null, false);
+ }
+ debugLog('Status ok');
+
+ // Verify signature
+ debugLog('Verify signature');
+ if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
+ debugLog('Signature WRONG');
+ return callback(new Error('Invalid signature'), null, false);
+ }
+ debugLog('Signature OK');
+
+ const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response')[0];
+ if (!response) {
+ const logoutResponse = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse');
+
+ if (!logoutResponse) {
+ return callback(new Error('Unknown SAML response message'), null, false);
+ }
+ return callback(null, null, true);
+ }
+ debugLog('Got response');
+
+ let assertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')[0];
+ const encAssertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion')[0];
+
+ const options = { key: this.options.privateKey };
+
+ if (typeof encAssertion !== 'undefined') {
+ xmlenc.decrypt(encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
+ assertion = new xmldom.DOMParser().parseFromString(result, 'text/xml');
+ });
+ }
+
+ if (!assertion) {
+ return callback(new Error('Missing SAML assertion'), null, false);
+ }
+
+ const profile = {};
+
+ if (response.hasAttribute('InResponseTo')) {
+ profile.inResponseToId = response.getAttribute('InResponseTo');
+ }
+
+ const issuer = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer')[0];
+ if (issuer) {
+ profile.issuer = issuer.textContent;
+ }
+
+ let subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0];
+ const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0];
+
+ if (typeof encSubject !== 'undefined') {
+ xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
+ subject = new xmldom.DOMParser().parseFromString(result, 'text/xml');
+ });
+ }
+
+ if (subject) {
+ const nameID = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'NameID')[0];
+ if (nameID) {
+ profile.nameID = nameID.textContent;
+
+ if (nameID.hasAttribute('Format')) {
+ profile.nameIDFormat = nameID.getAttribute('Format');
+ }
+ }
+ }
+
+ const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];
+
+ if (authnStatement) {
+ if (authnStatement.hasAttribute('SessionIndex')) {
+
+ profile.sessionIndex = authnStatement.getAttribute('SessionIndex');
+ debugLog(`Session Index: ${ profile.sessionIndex }`);
+ } else {
+ debugLog('No Session Index Found');
+ }
+ } else {
+ debugLog('No AuthN Statement found');
+ }
+
+ const attributeStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeStatement')[0];
+ if (attributeStatement) {
+ this.mapAttributes(attributeStatement, profile);
+ } else {
+ debugLog('No Attribute Statement found in SAML response.');
+ }
+
+ if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
+ profile.email = profile.nameID;
+ }
+
+ const profileKeys = Object.keys(profile);
+ for (let i = 0; i < profileKeys.length; i++) {
+ const key = profileKeys[i];
+
+ if (key.match(/\./)) {
+ profile[key.replace(/\./g, '-')] = profile[key];
+ delete profile[key];
+ }
+ }
+
+ debugLog(`NameID: ${ JSON.stringify(profile) }`);
+ return callback(null, profile, false);
+};
+
+let decryptionCert;
+SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) {
+
+ if (!decryptionCert) {
+ decryptionCert = this.options.privateCert;
+ }
+
+ if (!this.options.callbackUrl && !callbackUrl) {
+ throw new Error(
+ 'Unable to generate service provider metadata when callbackUrl option is not set');
+ }
+
+ const metadata = {
+ EntityDescriptor: {
+ '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata',
+ '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
+ '@entityID': this.options.issuer,
+ SPSSODescriptor: {
+ '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
+ SingleLogoutService: {
+ '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`,
+ '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`,
+ },
+ NameIDFormat: this.options.identifierFormat,
+ AssertionConsumerService: {
+ '@index': '1',
+ '@isDefault': 'true',
+ '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ '@Location': callbackUrl,
+ },
+ },
+ },
+ };
+
+ if (this.options.privateKey) {
+ if (!decryptionCert) {
+ throw new Error(
+ 'Missing decryptionCert while generating metadata for decrypting service provider');
+ }
+
+ decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
+ decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
+ decryptionCert = decryptionCert.replace(/\r\n/g, '\n');
+
+ metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor = {
+ 'ds:KeyInfo': {
+ 'ds:X509Data': {
+ 'ds:X509Certificate': {
+ '#text': decryptionCert,
+ },
+ },
+ },
+ EncryptionMethod: [
+ // this should be the set that the xmlenc library supports
+ {
+ '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
+ },
+ {
+ '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc',
+ },
+ {
+ '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc',
+ },
+ ],
+ };
+ }
+
+ return xmlbuilder.create(metadata).end({
+ pretty: true,
+ indent: ' ',
+ newline: '\n',
+ });
+};
diff --git a/app/metrics/index.js b/app/metrics/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/metrics/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-metrics/server/callbacksMetrics.js b/app/metrics/server/callbacksMetrics.js
similarity index 93%
rename from packages/rocketchat-metrics/server/callbacksMetrics.js
rename to app/metrics/server/callbacksMetrics.js
index 47cda6fe82f5..4d9e2f0d0d20 100644
--- a/packages/rocketchat-metrics/server/callbacksMetrics.js
+++ b/app/metrics/server/callbacksMetrics.js
@@ -1,4 +1,4 @@
-import { callbacks } from 'meteor/rocketchat:callbacks';
+import { callbacks } from '../../callbacks';
import { metrics } from './lib/metrics';
import StatsTracker from './lib/statsTracker';
diff --git a/packages/rocketchat-metrics/server/index.js b/app/metrics/server/index.js
similarity index 100%
rename from packages/rocketchat-metrics/server/index.js
rename to app/metrics/server/index.js
diff --git a/packages/rocketchat-metrics/server/lib/metrics.js b/app/metrics/server/lib/metrics.js
similarity index 95%
rename from packages/rocketchat-metrics/server/lib/metrics.js
rename to app/metrics/server/lib/metrics.js
index 98bc78942388..9d93284faedb 100644
--- a/packages/rocketchat-metrics/server/lib/metrics.js
+++ b/app/metrics/server/lib/metrics.js
@@ -3,10 +3,10 @@ import connect from 'connect';
import http from 'http';
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
-import { Info } from 'meteor/rocketchat:utils';
-import { Migrations } from 'meteor/rocketchat:migrations';
-import { settings } from 'meteor/rocketchat:settings';
-import { Statistics } from 'meteor/rocketchat:models';
+import { Info } from '../../../utils';
+import { Migrations } from '../../../migrations';
+import { settings } from '../../../settings';
+import { Statistics } from '../../../models';
client.collectDefaultMetrics();
@@ -77,7 +77,7 @@ metrics.totalPrivateGroupMessages = new client.Gauge({ name: 'rocketchat_private
metrics.totalDirectMessages = new client.Gauge({ name: 'rocketchat_direct_messages_total', help: 'total of messages in direct rooms' });
metrics.totalLivechatMessages = new client.Gauge({ name: 'rocketchat_livechat_messages_total', help: 'total of messages in livechat rooms' });
-const setPrometheusData = async() => {
+const setPrometheusData = async () => {
client.register.setDefaultLabels({
uniqueId: settings.get('uniqueID'),
siteUrl: settings.get('Site_Url'),
@@ -156,7 +156,7 @@ app.use('/', (req, res) => {
const server = http.createServer(app);
let timer;
-const updatePrometheusConfig = async() => {
+const updatePrometheusConfig = async () => {
const port = process.env.PROMETHEUS_PORT || settings.get('Prometheus_Port');
const enabled = settings.get('Prometheus_Enabled');
if (port == null || enabled == null) {
@@ -175,7 +175,7 @@ const updatePrometheusConfig = async() => {
}
};
-Meteor.startup(async() => {
+Meteor.startup(async () => {
settings.get('Prometheus_Enabled', updatePrometheusConfig);
settings.get('Prometheus_Port', updatePrometheusConfig);
});
diff --git a/packages/rocketchat-metrics/server/lib/statsTracker.js b/app/metrics/server/lib/statsTracker.js
similarity index 100%
rename from packages/rocketchat-metrics/server/lib/statsTracker.js
rename to app/metrics/server/lib/statsTracker.js
diff --git a/app/migrations/index.js b/app/migrations/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/migrations/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-migrations/server/index.js b/app/migrations/server/index.js
similarity index 100%
rename from packages/rocketchat-migrations/server/index.js
rename to app/migrations/server/index.js
diff --git a/app/migrations/server/migrations.js b/app/migrations/server/migrations.js
new file mode 100644
index 000000000000..04cd0eaa949a
--- /dev/null
+++ b/app/migrations/server/migrations.js
@@ -0,0 +1,413 @@
+/* eslint no-use-before-define:0 */
+import { Meteor } from 'meteor/meteor';
+import { Match, check } from 'meteor/check';
+import { Mongo } from 'meteor/mongo';
+import { Log } from 'meteor/logging';
+import { Info } from '../../utils';
+import _ from 'underscore';
+import s from 'underscore.string';
+import moment from 'moment';
+/*
+ Adds migration capabilities. Migrations are defined like:
+
+ Migrations.add({
+ up: function() {}, //*required* code to run to migrate upwards
+ version: 1, //*required* number to identify migration order
+ down: function() {}, //*optional* code to run to migrate downwards
+ name: 'Something' //*optional* display name for the migration
+ });
+
+ The ordering of migrations is determined by the version you set.
+
+ To run the migrations, set the MIGRATION_VERSION environment variable to either
+ 'latest' or the version number you want to migrate to. Optionally, append
+ ',exit' if you want the migrations to exit the meteor process, e.g if you're
+ migrating from a script (remember to pass the --once parameter).
+
+ e.g:
+ MIGRATION_VERSION="latest" mrt # ensure we'll be at the latest version and run the app
+ MIGRATION_VERSION="latest,exit" mrt --once # ensure we'll be at the latest version and exit
+ MIGRATION_VERSION="2,exit" mrt --once # migrate to version 2 and exit
+ MIGRATION_VERSION="2,rerun,exit" mrt --once # rerun migration script for version 2 and exit
+
+ Note: Migrations will lock ensuring only 1 app can be migrating at once. If
+ a migration crashes, the control record in the migrations collection will
+ remain locked and at the version it was at previously, however the db could
+ be in an inconsistant state.
+*/
+
+// since we'll be at version 0 by default, we should have a migration set for it.
+const DefaultMigration = {
+ version: 0,
+ up() {
+ // @TODO: check if collection "migrations" exist
+ // If exists, rename and rerun _migrateTo
+ },
+};
+
+export const Migrations = {
+ _list: [DefaultMigration],
+ options: {
+ // false disables logging
+ log: true,
+ // null or a function
+ logger: null,
+ // enable/disable info log "already at latest."
+ logIfLatest: true,
+ // lock will be valid for this amount of minutes
+ lockExpiration: 5,
+ // retry interval in seconds
+ retryInterval: 10,
+ // max number of attempts to retry unlock
+ maxAttempts: 30,
+ // migrations collection name
+ collectionName: 'migrations',
+ // collectionName: "rocketchat_migrations"
+ },
+ config(opts) {
+ this.options = _.extend({}, this.options, opts);
+ },
+};
+
+Migrations._collection = new Mongo.Collection(Migrations.options.collectionName);
+
+/* Create a box around messages for displaying on a console.log */
+function makeABox(message, color = 'red') {
+ if (!_.isArray(message)) {
+ message = message.split('\n');
+ }
+ const len = _(message).reduce(function(memo, msg) {
+ return Math.max(memo, msg.length);
+ }, 0) + 4;
+ const text = message.map((msg) => '|' [color] + s.lrpad(msg, len)[color] + '|' [color]).join('\n');
+ const topLine = '+' [color] + s.pad('', len, '-')[color] + '+' [color];
+ const separator = '|' [color] + s.pad('', len, '') + '|' [color];
+ const bottomLine = '+' [color] + s.pad('', len, '-')[color] + '+' [color];
+ return `\n${ topLine }\n${ separator }\n${ text }\n${ separator }\n${ bottomLine }\n`;
+}
+
+/*
+ Logger factory function. Takes a prefix string and options object
+ and uses an injected `logger` if provided, else falls back to
+ Meteor's `Log` package.
+ Will send a log object to the injected logger, on the following form:
+ message: String
+ level: String (info, warn, error, debug)
+ tag: 'Migrations'
+*/
+function createLogger(prefix) {
+ check(prefix, String);
+
+ // Return noop if logging is disabled.
+ if (Migrations.options.log === false) {
+ return function() {};
+ }
+
+ return function(level, message) {
+ check(level, Match.OneOf('info', 'error', 'warn', 'debug'));
+ check(message, Match.OneOf(String, [String]));
+
+ const logger = Migrations.options && Migrations.options.logger;
+
+ if (logger && _.isFunction(logger)) {
+
+ logger({
+ level,
+ message,
+ tag: prefix,
+ });
+
+ } else {
+ Log[level]({
+ message: `${ prefix }: ${ message }`,
+ });
+ }
+ };
+}
+
+// collection holding the control record
+
+const log = createLogger('Migrations');
+
+['info', 'warn', 'error', 'debug'].forEach(function(level) {
+ log[level] = _.partial(log, level);
+});
+
+// if (process.env.MIGRATE)
+// Migrations.migrateTo(process.env.MIGRATE);
+
+// Add a new migration:
+// {up: function *required
+// version: Number *required
+// down: function *optional
+// name: String *optional
+// }
+Migrations.add = function(migration) {
+ if (typeof migration.up !== 'function') { throw new Meteor.Error('Migration must supply an up function.'); }
+
+ if (typeof migration.version !== 'number') { throw new Meteor.Error('Migration must supply a version number.'); }
+
+ if (migration.version <= 0) { throw new Meteor.Error('Migration version must be greater than 0'); }
+
+ // Freeze the migration object to make it hereafter immutable
+ Object.freeze(migration);
+
+ this._list.push(migration);
+ this._list = _.sortBy(this._list, function(m) {
+ return m.version;
+ });
+};
+
+// Attempts to run the migrations using command in the form of:
+// e.g 'latest', 'latest,exit', 2
+// use 'XX,rerun' to re-run the migration at that version
+Migrations.migrateTo = function(command) {
+ if (_.isUndefined(command) || command === '' || this._list.length === 0) { throw new Error(`Cannot migrate using invalid command: ${ command }`); }
+
+ let version;
+ let subcommands;
+ if (typeof command === 'number') {
+ version = command;
+ } else {
+ version = command.split(',')[0];
+ subcommands = command.split(',').slice(1);
+ }
+
+ const { maxAttempts, retryInterval } = Migrations.options;
+ let migrated;
+ for (let attempts = 1; attempts <= maxAttempts; attempts++) {
+ if (version === 'latest') {
+ migrated = this._migrateTo(_.last(this._list).version);
+ } else {
+ migrated = this._migrateTo(parseInt(version), (subcommands.includes('rerun')));
+ }
+ if (migrated) {
+ break;
+ } else {
+ let willRetry;
+ if (attempts < maxAttempts) {
+ willRetry = ` Trying again in ${ retryInterval } seconds.`;
+ Meteor._sleepForMs(retryInterval * 1000);
+ } else {
+ willRetry = '';
+ }
+ console.log(`Not migrating, control is locked. Attempt ${ attempts }/${ maxAttempts }.${ willRetry }`.yellow);
+ }
+ }
+ if (!migrated) {
+ const control = this._getControl(); // Side effect: upserts control document.
+ console.log(makeABox([
+ 'ERROR! SERVER STOPPED',
+ '',
+ 'Your database migration control is locked.',
+ 'Please make sure you are running the latest version and try again.',
+ 'If the problem persists, please contact support.',
+ '',
+ `This Rocket.Chat version: ${ Info.version }`,
+ `Database locked at version: ${ control.version }`,
+ `Database target version: ${ version === 'latest' ? _.last(this._list).version : version }`,
+ '',
+ `Commit: ${ Info.commit.hash }`,
+ `Date: ${ Info.commit.date }`,
+ `Branch: ${ Info.commit.branch }`,
+ `Tag: ${ Info.commit.tag }`,
+ ]));
+ process.exit(1);
+ }
+
+ // remember to run meteor with --once otherwise it will restart
+ if (subcommands.includes('exit')) { process.exit(0); }
+};
+
+// just returns the current version
+Migrations.getVersion = function() {
+ return this._getControl().version;
+};
+
+// migrates to the specific version passed in
+Migrations._migrateTo = function(version, rerun) {
+ const self = this;
+ const control = this._getControl(); // Side effect: upserts control document.
+ let currentVersion = control.version;
+
+ if (lock() === false) {
+ // log.info('Not migrating, control is locked.');
+ // Warning
+ return false;
+ }
+
+ if (rerun) {
+ log.info(`Rerunning version ${ version }`);
+ migrate('up', this._findIndexByVersion(version));
+ log.info('Finished migrating.');
+ unlock();
+ return true;
+ }
+
+ if (currentVersion === version) {
+ if (this.options.logIfLatest) {
+ log.info(`Not migrating, already at version ${ version }`);
+ }
+ unlock();
+ return true;
+ }
+
+ const startIdx = this._findIndexByVersion(currentVersion);
+ const endIdx = this._findIndexByVersion(version);
+
+ // log.info('startIdx:' + startIdx + ' endIdx:' + endIdx);
+ log.info(`Migrating from version ${ this._list[startIdx].version } -> ${ this._list[endIdx].version }`);
+
+ // run the actual migration
+ function migrate(direction, idx) {
+ const migration = self._list[idx];
+
+ if (typeof migration[direction] !== 'function') {
+ unlock();
+ throw new Meteor.Error(`Cannot migrate ${ direction } on version ${ migration.version }`);
+ }
+
+ function maybeName() {
+ return migration.name ? ` (${ migration.name })` : '';
+ }
+
+ log.info(`Running ${ direction }() on version ${ migration.version }${ maybeName() }`);
+
+ try {
+ migration[direction](migration);
+ } catch (e) {
+ console.log(makeABox([
+ 'ERROR! SERVER STOPPED',
+ '',
+ 'Your database migration failed:',
+ e.message,
+ '',
+ 'Please make sure you are running the latest version and try again.',
+ 'If the problem persists, please contact support.',
+ '',
+ `This Rocket.Chat version: ${ Info.version }`,
+ `Database locked at version: ${ control.version }`,
+ `Database target version: ${ version }`,
+ '',
+ `Commit: ${ Info.commit.hash }`,
+ `Date: ${ Info.commit.date }`,
+ `Branch: ${ Info.commit.branch }`,
+ `Tag: ${ Info.commit.tag }`,
+ ]));
+ process.exit(1);
+ }
+ }
+
+ // Returns true if lock was acquired.
+ function lock() {
+ const date = new Date();
+ const dateMinusInterval = moment(date).subtract(self.options.lockExpiration, 'minutes').toDate();
+ const build = Info ? Info.build.date : date;
+
+ // This is atomic. The selector ensures only one caller at a time will see
+ // the unlocked control, and locking occurs in the same update's modifier.
+ // All other simultaneous callers will get false back from the update.
+ return self._collection.update({
+ _id: 'control',
+ $or: [{
+ locked: false,
+ }, {
+ lockedAt: {
+ $lt: dateMinusInterval,
+ },
+ }, {
+ buildAt: {
+ $ne: build,
+ },
+ }],
+ }, {
+ $set: {
+ locked: true,
+ lockedAt: date,
+ buildAt: build,
+ },
+ }) === 1;
+ }
+
+
+ // Side effect: saves version.
+ function unlock() {
+ self._setControl({
+ locked: false,
+ version: currentVersion,
+ });
+ }
+
+ if (currentVersion < version) {
+ for (let i = startIdx; i < endIdx; i++) {
+ migrate('up', i + 1);
+ currentVersion = self._list[i + 1].version;
+ self._setControl({
+ locked: true,
+ version: currentVersion,
+ });
+ }
+ } else {
+ for (let i = startIdx; i > endIdx; i--) {
+ migrate('down', i);
+ currentVersion = self._list[i - 1].version;
+ self._setControl({
+ locked: true,
+ version: currentVersion,
+ });
+ }
+ }
+
+ unlock();
+ log.info('Finished migrating.');
+};
+
+// gets the current control record, optionally creating it if non-existant
+Migrations._getControl = function() {
+ const control = this._collection.findOne({
+ _id: 'control',
+ });
+
+ return control || this._setControl({
+ version: 0,
+ locked: false,
+ });
+};
+
+// sets the control record
+Migrations._setControl = function(control) {
+ // be quite strict
+ check(control.version, Number);
+ check(control.locked, Boolean);
+
+ this._collection.update({
+ _id: 'control',
+ }, {
+ $set: {
+ version: control.version,
+ locked: control.locked,
+ },
+ }, {
+ upsert: true,
+ });
+
+ return control;
+};
+
+// returns the migration index in _list or throws if not found
+Migrations._findIndexByVersion = function(version) {
+ for (let i = 0; i < this._list.length; i++) {
+ if (this._list[i].version === version) { return i; }
+ }
+
+ throw new Meteor.Error(`Can't find migration version ${ version }`);
+};
+
+// reset (mainly intended for tests)
+Migrations._reset = function() {
+ this._list = [{
+ version: 0,
+ up() {},
+ }];
+ this._collection.remove({});
+};
diff --git a/app/models/client/index.js b/app/models/client/index.js
new file mode 100644
index 000000000000..a990bd5fc082
--- /dev/null
+++ b/app/models/client/index.js
@@ -0,0 +1,56 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from './models/_Base';
+import Avatars from './models/Avatars';
+import Uploads from './models/Uploads';
+import UserDataFiles from './models/UserDataFiles';
+import { Roles } from './models/Roles';
+import { Subscriptions as subscriptions } from './models/Subscriptions';
+import { Users as users } from './models/Users';
+import { CachedChannelList } from './models/CachedChannelList';
+import { CachedChatRoom } from './models/CachedChatRoom';
+import { CachedChatSubscription } from './models/CachedChatSubscription';
+import { CachedUserList } from './models/CachedUserList';
+import { ChatRoom } from './models/ChatRoom';
+import { ChatSubscription } from './models/ChatSubscription';
+import { ChatMessage } from './models/ChatMessage';
+import { RoomRoles } from './models/RoomRoles';
+import { UserAndRoom } from './models/UserAndRoom';
+import { UserRoles } from './models/UserRoles';
+import { AuthzCachedCollection, ChatPermissions } from './models/ChatPermissions';
+import { WebdavAccounts } from './models/WebdavAccounts';
+import CustomSounds from './models/CustomSounds';
+import EmojiCustom from './models/EmojiCustom';
+import _ from 'underscore';
+
+const Users = _.extend({}, users, Meteor.users);
+const Subscriptions = _.extend({}, subscriptions, ChatSubscription);
+const Messages = _.extend({}, ChatMessage);
+const Rooms = _.extend({}, ChatRoom);
+
+
+export {
+ Base,
+ Avatars,
+ Uploads,
+ UserDataFiles,
+ Roles,
+ Subscriptions,
+ Users,
+ Messages,
+ CachedChannelList,
+ CachedChatRoom,
+ CachedChatSubscription,
+ CachedUserList,
+ ChatRoom,
+ RoomRoles,
+ UserAndRoom,
+ UserRoles,
+ AuthzCachedCollection,
+ ChatPermissions,
+ ChatMessage,
+ ChatSubscription,
+ Rooms,
+ CustomSounds,
+ EmojiCustom,
+ WebdavAccounts,
+};
diff --git a/packages/rocketchat-models/client/models/Avatars.js b/app/models/client/models/Avatars.js
similarity index 100%
rename from packages/rocketchat-models/client/models/Avatars.js
rename to app/models/client/models/Avatars.js
diff --git a/packages/rocketchat-models/client/models/CachedChannelList.js b/app/models/client/models/CachedChannelList.js
similarity index 100%
rename from packages/rocketchat-models/client/models/CachedChannelList.js
rename to app/models/client/models/CachedChannelList.js
diff --git a/app/models/client/models/CachedChatRoom.js b/app/models/client/models/CachedChatRoom.js
new file mode 100644
index 000000000000..6ed43bc15749
--- /dev/null
+++ b/app/models/client/models/CachedChatRoom.js
@@ -0,0 +1,3 @@
+import { CachedCollection } from '../../../ui-cached-collection';
+
+export const CachedChatRoom = new CachedCollection({ name: 'rooms' });
diff --git a/app/models/client/models/CachedChatSubscription.js b/app/models/client/models/CachedChatSubscription.js
new file mode 100644
index 000000000000..a237624ba113
--- /dev/null
+++ b/app/models/client/models/CachedChatSubscription.js
@@ -0,0 +1,3 @@
+import { CachedCollection } from '../../../ui-cached-collection';
+
+export const CachedChatSubscription = new CachedCollection({ name: 'subscriptions' });
diff --git a/packages/rocketchat-models/client/models/CachedUserList.js b/app/models/client/models/CachedUserList.js
similarity index 100%
rename from packages/rocketchat-models/client/models/CachedUserList.js
rename to app/models/client/models/CachedUserList.js
diff --git a/app/models/client/models/ChatMessage.js b/app/models/client/models/ChatMessage.js
new file mode 100644
index 000000000000..9109ffabfc2b
--- /dev/null
+++ b/app/models/client/models/ChatMessage.js
@@ -0,0 +1,11 @@
+import { Mongo } from 'meteor/mongo';
+
+export const ChatMessage = new Mongo.Collection(null);
+
+ChatMessage.setReactions = function(messageId, reactions) {
+ return this.update({ _id: messageId }, { $set: { reactions } });
+};
+
+ChatMessage.unsetReactions = function(messageId) {
+ return this.update({ _id: messageId }, { $unset: { reactions: 1 } });
+};
diff --git a/app/models/client/models/ChatPermissions.js b/app/models/client/models/ChatPermissions.js
new file mode 100644
index 000000000000..e50ce6f66ba3
--- /dev/null
+++ b/app/models/client/models/ChatPermissions.js
@@ -0,0 +1,8 @@
+import { CachedCollection } from '../../../ui-cached-collection';
+
+export const AuthzCachedCollection = new CachedCollection({
+ name: 'permissions',
+ eventType: 'onLogged',
+});
+
+export const ChatPermissions = AuthzCachedCollection.collection;
diff --git a/app/models/client/models/ChatRoom.js b/app/models/client/models/ChatRoom.js
new file mode 100644
index 000000000000..e55c3e94f930
--- /dev/null
+++ b/app/models/client/models/ChatRoom.js
@@ -0,0 +1,11 @@
+import { CachedChatRoom } from './CachedChatRoom';
+
+export const ChatRoom = CachedChatRoom.collection;
+
+ChatRoom.setReactionsInLastMessage = function(roomId, lastMessage) {
+ return this.update({ _id: roomId }, { $set: { lastMessage } });
+};
+
+ChatRoom.unsetReactionsInLastMessage = function(roomId) {
+ return this.update({ _id: roomId }, { $unset: { lastMessage: { reactions: 1 } } });
+};
diff --git a/packages/rocketchat-models/client/models/ChatSubscription.js b/app/models/client/models/ChatSubscription.js
similarity index 100%
rename from packages/rocketchat-models/client/models/ChatSubscription.js
rename to app/models/client/models/ChatSubscription.js
diff --git a/packages/rocketchat-models/client/models/CustomSounds.js b/app/models/client/models/CustomSounds.js
similarity index 100%
rename from packages/rocketchat-models/client/models/CustomSounds.js
rename to app/models/client/models/CustomSounds.js
diff --git a/app/models/client/models/EmojiCustom.js b/app/models/client/models/EmojiCustom.js
new file mode 100644
index 000000000000..bb70c24fa1a5
--- /dev/null
+++ b/app/models/client/models/EmojiCustom.js
@@ -0,0 +1,22 @@
+import { Base } from './_Base';
+
+export class EmojiCustom extends Base {
+ constructor() {
+ super();
+ this._initModel('custom_emoji');
+ }
+
+ // find
+ findByNameOrAlias(name, options) {
+ const query = {
+ $or: [
+ { name },
+ { aliases: name },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+}
+
+export default new EmojiCustom();
diff --git a/packages/rocketchat-models/client/models/Roles.js b/app/models/client/models/Roles.js
similarity index 100%
rename from packages/rocketchat-models/client/models/Roles.js
rename to app/models/client/models/Roles.js
diff --git a/packages/rocketchat-models/client/models/RoomRoles.js b/app/models/client/models/RoomRoles.js
similarity index 100%
rename from packages/rocketchat-models/client/models/RoomRoles.js
rename to app/models/client/models/RoomRoles.js
diff --git a/app/models/client/models/Subscriptions.js b/app/models/client/models/Subscriptions.js
new file mode 100644
index 000000000000..5d2f9198daf0
--- /dev/null
+++ b/app/models/client/models/Subscriptions.js
@@ -0,0 +1,45 @@
+import { Users } from '..';
+import _ from 'underscore';
+import mem from 'mem';
+
+const Subscriptions = {};
+
+Object.assign(Subscriptions, {
+ isUserInRole: mem(function(userId, roleName, roomId) {
+ if (roomId == null) {
+ return false;
+ }
+
+ const query = {
+ rid: roomId,
+ };
+
+ const subscription = this.findOne(query, { fields: { roles: 1 } });
+
+ return subscription && Array.isArray(subscription.roles) && subscription.roles.includes(roleName);
+ }, { maxAge: 1000 }),
+
+ findUsersInRoles: mem(function(roles, scope, options) {
+ roles = [].concat(roles);
+
+ const query = {
+ roles: { $in: roles },
+ };
+
+ if (scope) {
+ query.rid = scope;
+ }
+
+ const subscriptions = this.find(query).fetch();
+
+ const users = _.compact(_.map(subscriptions, function(subscription) {
+ if ('undefined' !== typeof subscription.u && 'undefined' !== typeof subscription.u._id) {
+ return subscription.u._id;
+ }
+ }));
+
+ return Users.find({ _id: { $in: users } }, options);
+ }, { maxAge: 1000 }),
+});
+
+export { Subscriptions };
diff --git a/packages/rocketchat-models/client/models/Uploads.js b/app/models/client/models/Uploads.js
similarity index 100%
rename from packages/rocketchat-models/client/models/Uploads.js
rename to app/models/client/models/Uploads.js
diff --git a/packages/rocketchat-models/client/models/UserAndRoom.js b/app/models/client/models/UserAndRoom.js
similarity index 100%
rename from packages/rocketchat-models/client/models/UserAndRoom.js
rename to app/models/client/models/UserAndRoom.js
diff --git a/packages/rocketchat-models/client/models/UserDataFiles.js b/app/models/client/models/UserDataFiles.js
similarity index 100%
rename from packages/rocketchat-models/client/models/UserDataFiles.js
rename to app/models/client/models/UserDataFiles.js
diff --git a/packages/rocketchat-models/client/models/UserRoles.js b/app/models/client/models/UserRoles.js
similarity index 100%
rename from packages/rocketchat-models/client/models/UserRoles.js
rename to app/models/client/models/UserRoles.js
diff --git a/app/models/client/models/Users.js b/app/models/client/models/Users.js
new file mode 100644
index 000000000000..298cddb9cd96
--- /dev/null
+++ b/app/models/client/models/Users.js
@@ -0,0 +1,24 @@
+const Users = {};
+
+Object.assign(Users, {
+ isUserInRole(userId, roleName) {
+ const query = {
+ _id: userId,
+ };
+
+ const user = this.findOne(query, { fields: { roles: 1 } });
+ return user && Array.isArray(user.roles) && user.roles.includes(roleName);
+ },
+
+ findUsersInRoles(roles, scope, options) {
+ roles = [].concat(roles);
+
+ const query = {
+ roles: { $in: roles },
+ };
+
+ return this.find(query, options);
+ },
+});
+
+export { Users };
diff --git a/app/models/client/models/WebdavAccounts.js b/app/models/client/models/WebdavAccounts.js
new file mode 100644
index 000000000000..fafe9bf792d0
--- /dev/null
+++ b/app/models/client/models/WebdavAccounts.js
@@ -0,0 +1,3 @@
+import { Mongo } from 'meteor/mongo';
+
+export const WebdavAccounts = new Mongo.Collection('rocketchat_webdav_accounts');
diff --git a/packages/rocketchat-models/client/models/_Base.js b/app/models/client/models/_Base.js
similarity index 100%
rename from packages/rocketchat-models/client/models/_Base.js
rename to app/models/client/models/_Base.js
diff --git a/app/models/index.js b/app/models/index.js
new file mode 100644
index 000000000000..a67eca871efb
--- /dev/null
+++ b/app/models/index.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+if (Meteor.isClient) {
+ module.exports = require('./client/index.js');
+}
+if (Meteor.isServer) {
+ module.exports = require('./server/index.js');
+}
diff --git a/app/models/server/index.js b/app/models/server/index.js
new file mode 100644
index 000000000000..74b882789e00
--- /dev/null
+++ b/app/models/server/index.js
@@ -0,0 +1,77 @@
+import { Base } from './models/_Base';
+import { BaseDb } from './models/_BaseDb';
+import Avatars from './models/Avatars';
+import ExportOperations from './models/ExportOperations';
+import Messages from './models/Messages';
+import Reports from './models/Reports';
+import Rooms from './models/Rooms';
+import Settings from './models/Settings';
+import Subscriptions from './models/Subscriptions';
+import Uploads from './models/Uploads';
+import UserDataFiles from './models/UserDataFiles';
+import Users from './models/Users';
+import Sessions from './models/Sessions';
+import Statistics from './models/Statistics';
+import Permissions from './models/Permissions';
+import Roles from './models/Roles';
+import CustomSounds from './models/CustomSounds';
+import Integrations from './models/Integrations';
+import IntegrationHistory from './models/IntegrationHistory';
+import CredentialTokens from './models/CredentialTokens';
+import EmojiCustom from './models/EmojiCustom';
+import OAuthApps from './models/OAuthApps';
+import OEmbedCache from './models/OEmbedCache';
+import SmarshHistory from './models/SmarshHistory';
+import WebdavAccounts from './models/WebdavAccounts';
+import LivechatCustomField from './models/LivechatCustomField';
+import LivechatDepartment from './models/LivechatDepartment';
+import LivechatDepartmentAgents from './models/LivechatDepartmentAgents';
+import LivechatOfficeHour from './models/LivechatOfficeHour';
+import LivechatPageVisited from './models/LivechatPageVisited';
+import LivechatTrigger from './models/LivechatTrigger';
+import LivechatVisitors from './models/LivechatVisitors';
+import ReadReceipts from './models/ReadReceipts';
+
+export { AppsLogsModel } from './models/apps-logs-model';
+export { AppsPersistenceModel } from './models/apps-persistence-model';
+export { AppsModel } from './models/apps-model';
+export { FederationDNSCache } from './models/FederationDNSCache';
+export { FederationEvents } from './models/FederationEvents';
+export { FederationKeys } from './models/FederationKeys';
+export { FederationPeers } from './models/FederationPeers';
+
+export {
+ Base,
+ BaseDb,
+ Avatars,
+ ExportOperations,
+ Messages,
+ Reports,
+ Rooms,
+ Settings,
+ Subscriptions,
+ Uploads,
+ UserDataFiles,
+ Users,
+ Sessions,
+ Statistics,
+ Permissions,
+ Roles,
+ CustomSounds,
+ Integrations,
+ IntegrationHistory,
+ CredentialTokens,
+ EmojiCustom,
+ OAuthApps,
+ OEmbedCache,
+ SmarshHistory,
+ WebdavAccounts,
+ LivechatCustomField,
+ LivechatDepartment,
+ LivechatDepartmentAgents,
+ LivechatOfficeHour,
+ LivechatPageVisited,
+ LivechatTrigger,
+ LivechatVisitors,
+ ReadReceipts,
+};
diff --git a/packages/rocketchat-models/server/models/Avatars.js b/app/models/server/models/Avatars.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Avatars.js
rename to app/models/server/models/Avatars.js
diff --git a/packages/rocketchat-cas/server/models/CredentialTokens.js b/app/models/server/models/CredentialTokens.js
similarity index 78%
rename from packages/rocketchat-cas/server/models/CredentialTokens.js
rename to app/models/server/models/CredentialTokens.js
index a484eb978b39..7659538e032e 100644
--- a/packages/rocketchat-cas/server/models/CredentialTokens.js
+++ b/app/models/server/models/CredentialTokens.js
@@ -1,6 +1,6 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
-RocketChat.models.CredentialTokens = new class extends RocketChat.models._Base {
+export class CredentialTokens extends Base {
constructor() {
super('credential_tokens');
@@ -27,4 +27,6 @@ RocketChat.models.CredentialTokens = new class extends RocketChat.models._Base {
return this.findOne(query);
}
-};
+}
+
+export default new CredentialTokens();
diff --git a/packages/rocketchat-models/server/models/CustomSounds.js b/app/models/server/models/CustomSounds.js
similarity index 100%
rename from packages/rocketchat-models/server/models/CustomSounds.js
rename to app/models/server/models/CustomSounds.js
diff --git a/app/models/server/models/EmojiCustom.js b/app/models/server/models/EmojiCustom.js
new file mode 100644
index 000000000000..8f9f676072f5
--- /dev/null
+++ b/app/models/server/models/EmojiCustom.js
@@ -0,0 +1,91 @@
+import { Base } from './_Base';
+
+class EmojiCustom extends Base {
+ constructor() {
+ super('custom_emoji');
+
+ this.tryEnsureIndex({ name: 1 });
+ this.tryEnsureIndex({ aliases: 1 });
+ this.tryEnsureIndex({ extension: 1 });
+ }
+
+ // find one
+ findOneByID(_id, options) {
+ return this.findOne(_id, options);
+ }
+
+ // find
+ findByNameOrAlias(emojiName, options) {
+ let name = emojiName;
+
+ if (typeof emojiName === 'string') {
+ name = emojiName.replace(/:/g, '');
+ }
+
+ const query = {
+ $or: [
+ { name },
+ { aliases: name },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+
+ findByNameOrAliasExceptID(name, except, options) {
+ const query = {
+ _id: { $nin: [except] },
+ $or: [
+ { name },
+ { aliases: name },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+
+
+ // update
+ setName(_id, name) {
+ const update = {
+ $set: {
+ name,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ setAliases(_id, aliases) {
+ const update = {
+ $set: {
+ aliases,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ setExtension(_id, extension) {
+ const update = {
+ $set: {
+ extension,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ // INSERT
+ create(data) {
+ return this.insert(data);
+ }
+
+
+ // REMOVE
+ removeByID(_id) {
+ return this.remove(_id);
+ }
+}
+
+export default new EmojiCustom();
diff --git a/packages/rocketchat-models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js
similarity index 100%
rename from packages/rocketchat-models/server/models/ExportOperations.js
rename to app/models/server/models/ExportOperations.js
diff --git a/app/models/server/models/FederationDNSCache.js b/app/models/server/models/FederationDNSCache.js
new file mode 100644
index 000000000000..155deed53b95
--- /dev/null
+++ b/app/models/server/models/FederationDNSCache.js
@@ -0,0 +1,13 @@
+import { Base } from './_Base';
+
+class FederationDNSCacheModel extends Base {
+ constructor() {
+ super('federation_dns_cache');
+ }
+
+ findOneByDomain(domain) {
+ return this.findOne({ domain });
+ }
+}
+
+export const FederationDNSCache = new FederationDNSCacheModel();
diff --git a/app/models/server/models/FederationEvents.js b/app/models/server/models/FederationEvents.js
new file mode 100644
index 000000000000..b0955f82029a
--- /dev/null
+++ b/app/models/server/models/FederationEvents.js
@@ -0,0 +1,262 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from './_Base';
+
+const normalizePeers = (basePeers, options) => {
+ const { peers: sentPeers, skipPeers } = options;
+
+ let peers = sentPeers || basePeers || [];
+
+ if (skipPeers) {
+ peers = peers.filter((p) => skipPeers.indexOf(p) === -1);
+ }
+
+ return peers;
+};
+
+//
+// We should create a time to live index in this table to remove fulfilled events
+//
+class FederationEventsModel extends Base {
+ constructor() {
+ super('federation_events');
+ }
+
+ // Sometimes events errored but the error is final
+ setEventAsErrored(e, error, fulfilled = false) {
+ this.update({ _id: e._id }, {
+ $set: {
+ fulfilled,
+ lastAttemptAt: new Date(),
+ error,
+ },
+ });
+ }
+
+ setEventAsFullfilled(e) {
+ this.update({ _id: e._id }, {
+ $set: { fulfilled: true },
+ $unset: { error: 1 },
+ });
+ }
+
+ createEvent(type, payload, peer, options) {
+ const record = {
+ t: type,
+ ts: new Date(),
+ fulfilled: false,
+ payload,
+ peer,
+ options,
+ };
+
+ record._id = this.insert(record);
+
+ Meteor.defer(() => {
+ this.emit('createEvent', record);
+ });
+
+ return record;
+ }
+
+ createEventForPeers(type, payload, peers, options = {}) {
+ const records = [];
+
+ for (const peer of peers) {
+ const record = this.createEvent(type, payload, peer, options);
+
+ records.push(record);
+ }
+
+ return records;
+ }
+
+ // Create a `ping(png)` event
+ ping(peers) {
+ return this.createEventForPeers('png', {}, peers, { retry: { total: 1 } });
+ }
+
+ // Create a `directRoomCreated(drc)` event
+ directRoomCreated(federatedRoom, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ room: federatedRoom.getRoom(),
+ owner: federatedRoom.getOwner(),
+ users: federatedRoom.getUsers(),
+ };
+
+ return this.createEventForPeers('drc', payload, peers);
+ }
+
+ // Create a `roomCreated(roc)` event
+ roomCreated(federatedRoom, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ room: federatedRoom.getRoom(),
+ owner: federatedRoom.getOwner(),
+ users: federatedRoom.getUsers(),
+ };
+
+ return this.createEventForPeers('roc', payload, peers);
+ }
+
+ // Create a `userJoined(usj)` event
+ userJoined(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ user: federatedUser.getUser(),
+ };
+
+ return this.createEventForPeers('usj', payload, peers);
+ }
+
+ // Create a `userAdded(usa)` event
+ userAdded(federatedRoom, federatedUser, federatedInviter, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_inviter_id: federatedInviter.getFederationId(),
+ user: federatedUser.getUser(),
+ };
+
+ return this.createEventForPeers('usa', payload, peers);
+ }
+
+ // Create a `userLeft(usl)` event
+ userLeft(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('usl', payload, peers);
+ }
+
+ // Create a `userRemoved(usr)` event
+ userRemoved(federatedRoom, federatedUser, federatedRemovedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_removed_by_user_id: federatedRemovedByUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('usr', payload, peers);
+ }
+
+ // Create a `userMuted(usm)` event
+ userMuted(federatedRoom, federatedUser, federatedMutedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_muted_by_user_id: federatedMutedByUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('usm', payload, peers);
+ }
+
+ // Create a `userUnmuted(usu)` event
+ userUnmuted(federatedRoom, federatedUser, federatedUnmutedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_unmuted_by_user_id: federatedUnmutedByUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('usu', payload, peers);
+ }
+
+ // Create a `messageCreated(msc)` event
+ messageCreated(federatedRoom, federatedMessage, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ message: federatedMessage.getMessage(),
+ };
+
+ return this.createEventForPeers('msc', payload, peers);
+ }
+
+ // Create a `messageUpdated(msu)` event
+ messageUpdated(federatedRoom, federatedMessage, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ message: federatedMessage.getMessage(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('msu', payload, peers);
+ }
+
+ // Create a `deleteMessage(msd)` event
+ messageDeleted(federatedRoom, federatedMessage, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_message_id: federatedMessage.getFederationId(),
+ };
+
+ return this.createEventForPeers('msd', payload, peers);
+ }
+
+ // Create a `messagesRead(msr)` event
+ messagesRead(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('msr', payload, peers);
+ }
+
+ // Create a `messagesSetReaction(mrs)` event
+ messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_message_id: federatedMessage.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ reaction,
+ shouldReact,
+ };
+
+ return this.createEventForPeers('mrs', payload, peers);
+ }
+
+ // Create a `messagesUnsetReaction(mru)` event
+ messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_message_id: federatedMessage.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ reaction,
+ shouldReact,
+ };
+
+ return this.createEventForPeers('mru', payload, peers);
+ }
+
+ // Get all unfulfilled events
+ getUnfulfilled() {
+ return this.find({ fulfilled: false }, { sort: { ts: 1 } }).fetch();
+ }
+}
+
+export const FederationEvents = new FederationEventsModel();
diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js
new file mode 100644
index 000000000000..8e2e9c26756d
--- /dev/null
+++ b/app/models/server/models/FederationKeys.js
@@ -0,0 +1,69 @@
+import NodeRSA from 'node-rsa';
+import uuid from 'uuid/v4';
+
+import { Base } from './_Base';
+
+class FederationKeysModel extends Base {
+ constructor() {
+ super('federation_keys');
+ }
+
+ getKey(type) {
+ const keyResource = this.findOne({ type });
+
+ if (!keyResource) { return null; }
+
+ return keyResource.key;
+ }
+
+ loadKey(keyData, type) {
+ return new NodeRSA(keyData, `pkcs8-${ type }-pem`);
+ }
+
+ generateKeys() {
+ const key = new NodeRSA({ b: 512 });
+
+ key.generateKeyPair();
+
+ this.update({ type: 'private' }, { type: 'private', key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, '') }, { upsert: true });
+
+ this.update({ type: 'public' }, { type: 'public', key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, '') }, { upsert: true });
+
+ return {
+ privateKey: this.getPrivateKey(),
+ publicKey: this.getPublicKey(),
+ };
+ }
+
+ generateUniqueId() {
+ const uniqueId = uuid();
+
+ this.update({ type: 'unique' }, { type: 'unique', key: uniqueId }, { upsert: true });
+ }
+
+ getUniqueId() {
+ return (this.findOne({ type: 'unique' }) || {}).key;
+ }
+
+ getPrivateKey() {
+ const keyData = this.getKey('private');
+
+ return keyData && this.loadKey(keyData, 'private');
+ }
+
+ getPrivateKeyString() {
+ return this.getKey('private');
+ }
+
+ getPublicKey() {
+ const keyData = this.getKey('public');
+
+ return keyData && this.loadKey(keyData, 'public');
+ }
+
+ getPublicKeyString() {
+ return this.getKey('public');
+ }
+}
+
+export const FederationKeys = new FederationKeysModel();
diff --git a/app/models/server/models/FederationPeers.js b/app/models/server/models/FederationPeers.js
new file mode 100644
index 000000000000..e2d712ce6ee9
--- /dev/null
+++ b/app/models/server/models/FederationPeers.js
@@ -0,0 +1,52 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Base } from './_Base';
+import { Users } from '..';
+
+class FederationPeersModel extends Base {
+ constructor() {
+ super('federation_peers');
+ }
+
+ refreshPeers() {
+ const collectionObj = this.model.rawCollection();
+ const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);
+
+ const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch();
+
+ const peers = [...new Set(users.map((u) => u.federation.peer))];
+
+ for (const peer of peers) {
+ findAndModify({ peer }, [], {
+ $setOnInsert: {
+ active: false,
+ peer,
+ last_seen_at: null,
+ last_failure_at: null,
+ },
+ }, { upsert: true });
+ }
+
+ this.remove({ peer: { $nin: peers } });
+ }
+
+ updateStatuses(seenPeers) {
+ for (const peer of Object.keys(seenPeers)) {
+ const seen = seenPeers[peer];
+
+ const updateQuery = {};
+
+ if (seen) {
+ updateQuery.active = true;
+ updateQuery.last_seen_at = new Date();
+ } else {
+ updateQuery.active = false;
+ updateQuery.last_failure_at = new Date();
+ }
+
+ this.update({ peer }, { $set: updateQuery });
+ }
+ }
+}
+
+export const FederationPeers = new FederationPeersModel();
diff --git a/packages/rocketchat-integrations/server/models/IntegrationHistory.js b/app/models/server/models/IntegrationHistory.js
similarity index 85%
rename from packages/rocketchat-integrations/server/models/IntegrationHistory.js
rename to app/models/server/models/IntegrationHistory.js
index 138979e29939..bda9b16c5388 100644
--- a/packages/rocketchat-integrations/server/models/IntegrationHistory.js
+++ b/app/models/server/models/IntegrationHistory.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
-RocketChat.models.IntegrationHistory = new class IntegrationHistory extends RocketChat.models._Base {
+export class IntegrationHistory extends Base {
constructor() {
super('integration_history');
}
@@ -37,4 +37,7 @@ RocketChat.models.IntegrationHistory = new class IntegrationHistory extends Rock
removeByIntegrationId(integrationId) {
return this.remove({ 'integration._id': integrationId });
}
-};
+}
+
+export default new IntegrationHistory();
+
diff --git a/app/models/server/models/Integrations.js b/app/models/server/models/Integrations.js
new file mode 100644
index 000000000000..2d9128c99cd7
--- /dev/null
+++ b/app/models/server/models/Integrations.js
@@ -0,0 +1,28 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from './_Base';
+
+export class Integrations extends Base {
+ constructor() {
+ super('integrations');
+ }
+
+ findByType(type, options) {
+ if (type !== 'webhook-incoming' && type !== 'webhook-outgoing') {
+ throw new Meteor.Error('invalid-type-to-find');
+ }
+
+ return this.find({ type }, options);
+ }
+
+ disableByUserId(userId) {
+ return this.update({ userId }, { $set: { enabled: false } }, { multi: true });
+ }
+
+ updateRoomName(oldRoomName, newRoomName) {
+ const hashedOldRoomName = `#${ oldRoomName }`;
+ const hashedNewRoomName = `#${ newRoomName }`;
+ return this.update({ channel: hashedOldRoomName }, { $set: { 'channel.$': hashedNewRoomName } }, { multi: true });
+ }
+}
+
+export default new Integrations();
diff --git a/packages/rocketchat-livechat/server/models/LivechatCustomField.js b/app/models/server/models/LivechatCustomField.js
similarity index 78%
rename from packages/rocketchat-livechat/server/models/LivechatCustomField.js
rename to app/models/server/models/LivechatCustomField.js
index d5fdb5706b94..cf0ae022ee2d 100644
--- a/packages/rocketchat-livechat/server/models/LivechatCustomField.js
+++ b/app/models/server/models/LivechatCustomField.js
@@ -1,10 +1,10 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
import _ from 'underscore';
/**
* Livechat Custom Fields model
*/
-class LivechatCustomField extends RocketChat.models._Base {
+export class LivechatCustomField extends Base {
constructor() {
super('livechat_custom_field');
}
@@ -43,4 +43,4 @@ class LivechatCustomField extends RocketChat.models._Base {
}
}
-RocketChat.models.LivechatCustomField = new LivechatCustomField();
+export default new LivechatCustomField();
diff --git a/packages/rocketchat-livechat/server/models/LivechatDepartment.js b/app/models/server/models/LivechatDepartment.js
similarity index 75%
rename from packages/rocketchat-livechat/server/models/LivechatDepartment.js
rename to app/models/server/models/LivechatDepartment.js
index 15d9a01a433c..120813aab43e 100644
--- a/packages/rocketchat-livechat/server/models/LivechatDepartment.js
+++ b/app/models/server/models/LivechatDepartment.js
@@ -1,10 +1,10 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
+import LivechatDepartmentAgents from './LivechatDepartmentAgents';
import _ from 'underscore';
-
/**
* Livechat Department model
*/
-class LivechatDepartment extends RocketChat.models._Base {
+export class LivechatDepartment extends Base {
constructor() {
super('livechat_department');
@@ -27,7 +27,7 @@ class LivechatDepartment extends RocketChat.models._Base {
return this.find(query, options);
}
- createOrUpdateDepartment(_id, { enabled, name, description, showOnRegistration }, agents) {
+ createOrUpdateDepartment(_id, { enabled, name, description, showOnRegistration, email, showOnOfflineForm }, agents) {
agents = [].concat(agents);
const record = {
@@ -36,6 +36,8 @@ class LivechatDepartment extends RocketChat.models._Base {
description,
numAgents: agents.length,
showOnRegistration,
+ showOnOfflineForm,
+ email,
};
if (_id) {
@@ -44,16 +46,16 @@ class LivechatDepartment extends RocketChat.models._Base {
_id = this.insert(record);
}
- const savedAgents = _.pluck(RocketChat.models.LivechatDepartmentAgents.findByDepartmentId(_id).fetch(), 'agentId');
+ const savedAgents = _.pluck(LivechatDepartmentAgents.findByDepartmentId(_id).fetch(), 'agentId');
const agentsToSave = _.pluck(agents, 'agentId');
// remove other agents
_.difference(savedAgents, agentsToSave).forEach((agentId) => {
- RocketChat.models.LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(_id, agentId);
+ LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(_id, agentId);
});
agents.forEach((agent) => {
- RocketChat.models.LivechatDepartmentAgents.saveAgent({
+ LivechatDepartmentAgents.saveAgent({
agentId: agent.agentId,
departmentId: _id,
username: agent.username,
@@ -92,5 +94,4 @@ class LivechatDepartment extends RocketChat.models._Base {
return this.findOne(query, options);
}
}
-
-RocketChat.models.LivechatDepartment = new LivechatDepartment();
+export default new LivechatDepartment();
diff --git a/packages/rocketchat-livechat/server/models/LivechatDepartmentAgents.js b/app/models/server/models/LivechatDepartmentAgents.js
similarity index 80%
rename from packages/rocketchat-livechat/server/models/LivechatDepartmentAgents.js
rename to app/models/server/models/LivechatDepartmentAgents.js
index 760b54f2a7e7..f590ab954d02 100644
--- a/packages/rocketchat-livechat/server/models/LivechatDepartmentAgents.js
+++ b/app/models/server/models/LivechatDepartmentAgents.js
@@ -1,10 +1,11 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
+import Users from './Users';
import _ from 'underscore';
/**
* Livechat Department model
*/
-class LivechatDepartmentAgents extends RocketChat.models._Base {
+export class LivechatDepartmentAgents extends Base {
constructor() {
super('livechat_department_agents');
}
@@ -37,7 +38,7 @@ class LivechatDepartmentAgents extends RocketChat.models._Base {
return;
}
- const onlineUsers = RocketChat.models.Users.findOnlineUserFromList(_.pluck(agents, 'username'));
+ const onlineUsers = Users.findOnlineUserFromList(_.pluck(agents, 'username'));
const onlineUsernames = _.pluck(onlineUsers.fetch(), 'username');
@@ -77,10 +78,10 @@ class LivechatDepartmentAgents extends RocketChat.models._Base {
const agents = this.findByDepartmentId(departmentId).fetch();
if (agents.length === 0) {
- return [];
+ return;
}
- const onlineUsers = RocketChat.models.Users.findOnlineUserFromList(_.pluck(agents, 'username'));
+ const onlineUsers = Users.findOnlineUserFromList(_.pluck(agents, 'username'));
const onlineUsernames = _.pluck(onlineUsers.fetch(), 'username');
@@ -91,13 +92,7 @@ class LivechatDepartmentAgents extends RocketChat.models._Base {
},
};
- const depAgents = this.find(query);
-
- if (depAgents) {
- return depAgents;
- } else {
- return [];
- }
+ return this.find(query);
}
findUsersInQueue(usersList) {
@@ -133,5 +128,4 @@ class LivechatDepartmentAgents extends RocketChat.models._Base {
return this.update(query, update, { multi: true });
}
}
-
-RocketChat.models.LivechatDepartmentAgents = new LivechatDepartmentAgents();
+export default new LivechatDepartmentAgents();
diff --git a/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js b/app/models/server/models/LivechatOfficeHour.js
similarity index 95%
rename from packages/rocketchat-livechat/server/models/LivechatOfficeHour.js
rename to app/models/server/models/LivechatOfficeHour.js
index 3413f30ea6e3..45a5ef8d4ded 100644
--- a/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js
+++ b/app/models/server/models/LivechatOfficeHour.js
@@ -1,7 +1,7 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
import moment from 'moment';
-class LivechatOfficeHour extends RocketChat.models._Base {
+export class LivechatOfficeHour extends Base {
constructor() {
super('livechat_office_hour');
@@ -107,5 +107,4 @@ class LivechatOfficeHour extends RocketChat.models._Base {
return finish.isSame(currentTime, 'minute');
}
}
-
-RocketChat.models.LivechatOfficeHour = new LivechatOfficeHour();
+export default new LivechatOfficeHour();
diff --git a/packages/rocketchat-livechat/server/models/LivechatPageVisited.js b/app/models/server/models/LivechatPageVisited.js
similarity index 82%
rename from packages/rocketchat-livechat/server/models/LivechatPageVisited.js
rename to app/models/server/models/LivechatPageVisited.js
index 9582831bf2c3..f6668e7e9380 100644
--- a/packages/rocketchat-livechat/server/models/LivechatPageVisited.js
+++ b/app/models/server/models/LivechatPageVisited.js
@@ -1,9 +1,9 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
/**
* Livechat Page Visited model
*/
-class LivechatPageVisited extends RocketChat.models._Base {
+class LivechatPageVisited extends Base {
constructor() {
super('livechat_page_visited');
@@ -45,5 +45,4 @@ class LivechatPageVisited extends RocketChat.models._Base {
});
}
}
-
-RocketChat.models.LivechatPageVisited = new LivechatPageVisited();
+export default new LivechatPageVisited();
diff --git a/app/models/server/models/LivechatTrigger.js b/app/models/server/models/LivechatTrigger.js
new file mode 100644
index 000000000000..a5380016cadd
--- /dev/null
+++ b/app/models/server/models/LivechatTrigger.js
@@ -0,0 +1,32 @@
+import { Base } from './_Base';
+
+/**
+ * Livechat Trigger model
+ */
+export class LivechatTrigger extends Base {
+ constructor() {
+ super('livechat_trigger');
+ }
+
+ updateById(_id, data) {
+ return this.update({ _id }, { $set: data });
+ }
+
+ removeAll() {
+ return this.remove({});
+ }
+
+ findById(_id) {
+ return this.find({ _id });
+ }
+
+ removeById(_id) {
+ return this.remove({ _id });
+ }
+
+ findEnabled() {
+ return this.find({ enabled: true });
+ }
+}
+
+export default new LivechatTrigger();
diff --git a/packages/rocketchat-livechat/server/models/LivechatVisitors.js b/app/models/server/models/LivechatVisitors.js
similarity index 95%
rename from packages/rocketchat-livechat/server/models/LivechatVisitors.js
rename to app/models/server/models/LivechatVisitors.js
index 4da75b6af15d..c34f73c7b4dd 100644
--- a/packages/rocketchat-livechat/server/models/LivechatVisitors.js
+++ b/app/models/server/models/LivechatVisitors.js
@@ -1,9 +1,10 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
+import Settings from './Settings';
import _ from 'underscore';
import s from 'underscore.string';
-class LivechatVisitors extends RocketChat.models._Base {
+export class LivechatVisitors extends Base {
constructor() {
super('livechat_visitor');
}
@@ -93,7 +94,7 @@ class LivechatVisitors extends RocketChat.models._Base {
* @return {string} The next visitor name
*/
getNextVisitorUsername() {
- const settingsRaw = RocketChat.models.Settings.model.rawCollection();
+ const settingsRaw = Settings.model.rawCollection();
const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw);
const query = {
diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js
new file mode 100644
index 000000000000..eb9e47349c5b
--- /dev/null
+++ b/app/models/server/models/Messages.js
@@ -0,0 +1,1109 @@
+import { Match } from 'meteor/check';
+import { Base } from './_Base';
+import Rooms from './Rooms';
+import { settings } from '../../../settings/server/functions/settings';
+import { FileUpload } from '../../../file-upload/server/lib/FileUpload';
+import _ from 'underscore';
+
+export class Messages extends Base {
+ constructor() {
+ super('message');
+
+ this.tryEnsureIndex({ rid: 1, ts: 1 });
+ this.tryEnsureIndex({ ts: 1 });
+ this.tryEnsureIndex({ 'u._id': 1 });
+ this.tryEnsureIndex({ editedAt: 1 }, { sparse: true });
+ this.tryEnsureIndex({ 'editedBy._id': 1 }, { sparse: true });
+ this.tryEnsureIndex({ rid: 1, t: 1, 'u._id': 1 });
+ this.tryEnsureIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
+ this.tryEnsureIndex({ msg: 'text' });
+ this.tryEnsureIndex({ 'file._id': 1 }, { sparse: true });
+ this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: true });
+ this.tryEnsureIndex({ pinned: 1 }, { sparse: true });
+ this.tryEnsureIndex({ snippeted: 1 }, { sparse: true });
+ this.tryEnsureIndex({ location: '2dsphere' });
+ this.tryEnsureIndex({ slackBotId: 1, slackTs: 1 }, { sparse: true });
+ this.tryEnsureIndex({ unread: 1 }, { sparse: true });
+
+ // discussions
+ this.tryEnsureIndex({ drid: 1 }, { sparse: true });
+ // threads
+ this.tryEnsureIndex({ tmid: 1 }, { sparse: true });
+ this.tryEnsureIndex({ tcount: 1, tlm: 1 }, { sparse: true });
+
+ }
+
+ setReactions(messageId, reactions) {
+ return this.update({ _id: messageId }, { $set: { reactions } });
+ }
+
+ keepHistoryForToken(token) {
+ return this.update({
+ 'navigation.token': token,
+ expireAt: {
+ $exists: true,
+ },
+ }, {
+ $unset: {
+ expireAt: 1,
+ },
+ }, {
+ multi: true,
+ });
+ }
+
+ setRoomIdByToken(token, rid) {
+ return this.update({
+ 'navigation.token': token,
+ rid: null,
+ }, {
+ $set: {
+ rid,
+ },
+ }, {
+ multi: true,
+ });
+ }
+
+ createRoomArchivedByRoomIdAndUser(roomId, user) {
+ return this.createWithTypeRoomIdMessageAndUser('room-archived', roomId, '', user);
+ }
+
+ createRoomUnarchivedByRoomIdAndUser(roomId, user) {
+ return this.createWithTypeRoomIdMessageAndUser('room-unarchived', roomId, '', user);
+ }
+
+ unsetReactions(messageId) {
+ return this.update({ _id: messageId }, { $unset: { reactions: 1 } });
+ }
+
+ deleteOldOTRMessages(roomId, ts) {
+ const query = { rid: roomId, t: 'otr', ts: { $lte: ts } };
+ return this.remove(query);
+ }
+
+ updateOTRAck(_id, otrAck) {
+ const query = { _id };
+ const update = { $set: { otrAck } };
+ return this.update(query, update);
+ }
+
+ setGoogleVisionData(messageId, visionData) {
+ const updateObj = {};
+ for (const index in visionData) {
+ if (visionData.hasOwnProperty(index)) {
+ updateObj[`attachments.0.${ index }`] = visionData[index];
+ }
+ }
+
+ return this.update({ _id: messageId }, { $set: updateObj });
+ }
+
+ createRoomSettingsChangedWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) {
+ return this.createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData);
+ }
+
+ createRoomRenamedWithRoomIdRoomNameAndUser(roomId, roomName, user, extraData) {
+ return this.createWithTypeRoomIdMessageAndUser('r', roomId, roomName, user, extraData);
+ }
+
+ addTranslations(messageId, translations) {
+ const updateObj = {};
+ Object.keys(translations).forEach((key) => {
+ const translation = translations[key];
+ updateObj[`translations.${ key }`] = translation;
+ });
+ return this.update({ _id: messageId }, { $set: updateObj });
+ }
+
+ addAttachmentTranslations = function(messageId, attachmentIndex, translations) {
+ const updateObj = {};
+ Object.keys(translations).forEach((key) => {
+ const translation = translations[key];
+ updateObj[`attachments.${ attachmentIndex }.translations.${ key }`] = translation;
+ });
+ return this.update({ _id: messageId }, { $set: updateObj });
+ }
+
+ countVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $gte: afterTimestamp,
+ $lte: beforeTimestamp,
+ },
+ };
+
+ return this.find(query, options).count();
+ }
+
+ // FIND
+ findByMention(username, options) {
+ const query = { 'mentions.username': username };
+
+ return this.find(query, options);
+ }
+
+ findFilesByUserId(userId, options = {}) {
+ const query = {
+ 'u._id': userId,
+ 'file._id': { $exists: true },
+ };
+ return this.find(query, { fields: { 'file._id': 1 }, ...options });
+ }
+
+ findFilesByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ignoreDiscussion = true, ts, users = [], options = {}) {
+ const query = {
+ rid,
+ ts,
+ 'file._id': { $exists: true },
+ };
+
+ if (excludePinned) {
+ query.pinned = { $ne: true };
+ }
+
+ if (ignoreDiscussion) {
+ query.drid = { $exists: 0 };
+ }
+
+ if (users.length) {
+ query['u.username'] = { $in: users };
+ }
+
+ return this.find(query, { fields: { 'file._id': 1 }, ...options });
+ }
+
+ findDiscussionByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, users = [], options = {}) {
+ const query = {
+ rid,
+ ts,
+ drid: { $exists: 1 },
+ };
+
+ if (excludePinned) {
+ query.pinned = { $ne: true };
+ }
+
+ if (users.length) {
+ query['u.username'] = { $in: users };
+ }
+
+ return this.find(query, options);
+ }
+
+ findVisibleByMentionAndRoomId(username, rid, options) {
+ const query = {
+ _hidden: { $ne: true },
+ 'mentions.username': username,
+ rid,
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomId(roomId, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdNotContainingTypes(roomId, types, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+
+ rid: roomId,
+ };
+
+ if (Match.test(types, [String]) && (types.length > 0)) {
+ query.t =
+ { $nin: types };
+ }
+
+ return this.find(query, options);
+ }
+
+ findInvisibleByRoomId(roomId, options) {
+ const query = {
+ _hidden: true,
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdAfterTimestamp(roomId, timestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $gt: timestamp,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findForUpdates(roomId, timestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ _updatedAt: {
+ $gt: timestamp,
+ },
+ };
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBeforeTimestamp(roomId, timestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $lt: timestamp,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBeforeTimestampInclusive(roomId, timestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $lte: timestamp,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBetweenTimestamps(roomId, afterTimestamp, beforeTimestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $gt: afterTimestamp,
+ $lt: beforeTimestamp,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $gte: afterTimestamp,
+ $lte: beforeTimestamp,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBeforeTimestampNotContainingTypes(roomId, timestamp, types, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $lt: timestamp,
+ },
+ };
+
+ if (Match.test(types, [String]) && (types.length > 0)) {
+ query.t =
+ { $nin: types };
+ }
+
+ return this.find(query, options);
+ }
+
+ findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, afterTimestamp, beforeTimestamp, types, options) {
+ const query = {
+ _hidden: {
+ $ne: true,
+ },
+ rid: roomId,
+ ts: {
+ $gt: afterTimestamp,
+ $lt: beforeTimestamp,
+ },
+ };
+
+ if (Match.test(types, [String]) && (types.length > 0)) {
+ query.t =
+ { $nin: types };
+ }
+
+ return this.find(query, options);
+ }
+
+ findVisibleCreatedOrEditedAfterTimestamp(timestamp, options) {
+ const query = {
+ _hidden: { $ne: true },
+ $or: [{
+ ts: {
+ $gt: timestamp,
+ },
+ },
+ {
+ editedAt: {
+ $gt: timestamp,
+ },
+ },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+
+ findStarredByUserAtRoom(userId, roomId, options) {
+ const query = {
+ _hidden: { $ne: true },
+ 'starred._id': userId,
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findPinnedByRoom(roomId, options) {
+ const query = {
+ t: { $ne: 'rm' },
+ _hidden: { $ne: true },
+ pinned: true,
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findSnippetedByRoom(roomId, options) {
+ const query = {
+ _hidden: { $ne: true },
+ snippeted: true,
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ getLastTimestamp(options) {
+ if (options == null) { options = {}; }
+ const query = { ts: { $exists: 1 } };
+ options.sort = { ts: -1 };
+ options.limit = 1;
+ const [message] = this.find(query, options).fetch();
+ return message && message.ts;
+ }
+
+ findByRoomIdAndMessageIds(rid, messageIds, options) {
+ const query = {
+ rid,
+ _id: {
+ $in: messageIds,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findOneBySlackBotIdAndSlackTs(slackBotId, slackTs) {
+ const query = {
+ slackBotId,
+ slackTs,
+ };
+
+ return this.findOne(query);
+ }
+
+ findOneBySlackTs(slackTs) {
+ const query = { slackTs };
+
+ return this.findOne(query);
+ }
+
+ findByRoomIdAndType(roomId, type, options) {
+ const query = {
+ rid: roomId,
+ t: type,
+ };
+
+ if (options == null) { options = {}; }
+
+ return this.find(query, options);
+ }
+
+ findByRoomId(roomId, options) {
+ const query = {
+ rid: roomId,
+ };
+
+ return this.find(query, options);
+ }
+
+ getLastVisibleMessageSentWithNoTypeByRoomId(rid, messageId) {
+ const query = {
+ rid,
+ _hidden: { $ne: true },
+ t: { $exists: false },
+ };
+
+ if (messageId) {
+ query._id = { $ne: messageId };
+ }
+
+ const options = {
+ sort: {
+ ts: -1,
+ },
+ };
+
+ return this.findOne(query, options);
+ }
+
+ cloneAndSaveAsHistoryById(_id, user) {
+ const record = this.findOneById(_id);
+ record._hidden = true;
+ record.parent = record._id;
+ record.editedAt = new Date;
+ record.editedBy = {
+ _id: user._id,
+ username: user.username,
+ };
+ delete record._id;
+ return this.insert(record);
+ }
+
+ // UPDATE
+ setHiddenById(_id, hidden) {
+ if (hidden == null) { hidden = true; }
+ const query = { _id };
+
+ const update = {
+ $set: {
+ _hidden: hidden,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setAsDeletedByIdAndUser(_id, user) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ msg: '',
+ t: 'rm',
+ urls: [],
+ mentions: [],
+ attachments: [],
+ reactions: [],
+ editedAt: new Date(),
+ editedBy: {
+ _id: user._id,
+ username: user.username,
+ },
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setPinnedByIdAndUserId(_id, pinnedBy, pinned, pinnedAt) {
+ if (pinned == null) { pinned = true; }
+ if (pinnedAt == null) { pinnedAt = 0; }
+ const query = { _id };
+
+ const update = {
+ $set: {
+ pinned,
+ pinnedAt: pinnedAt || new Date,
+ pinnedBy,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) {
+ if (snippeted == null) { snippeted = true; }
+ if (snippetedAt == null) { snippetedAt = 0; }
+ const query = { _id: message._id };
+
+ const msg = `\`\`\`${ message.msg }\`\`\``;
+
+ const update = {
+ $set: {
+ msg,
+ snippeted,
+ snippetedAt: snippetedAt || new Date,
+ snippetedBy,
+ snippetName,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setUrlsById(_id, urls) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ urls,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateAllUsernamesByUserId(userId, username) {
+ const query = { 'u._id': userId };
+
+ const update = {
+ $set: {
+ 'u.username': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateUsernameOfEditByUserId(userId, username) {
+ const query = { 'editedBy._id': userId };
+
+ const update = {
+ $set: {
+ 'editedBy.username': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateUsernameAndMessageOfMentionByIdAndOldUsername(_id, oldUsername, newUsername, newMessage) {
+ const query = {
+ _id,
+ 'mentions.username': oldUsername,
+ };
+
+ const update = {
+ $set: {
+ 'mentions.$.username': newUsername,
+ msg: newMessage,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateUserStarById(_id, userId, starred) {
+ let update;
+ const query = { _id };
+
+ if (starred) {
+ update = {
+ $addToSet: {
+ starred: { _id: userId },
+ },
+ };
+ } else {
+ update = {
+ $pull: {
+ starred: { _id: userId },
+ },
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ upgradeEtsToEditAt() {
+ const query = { ets: { $exists: 1 } };
+
+ const update = {
+ $rename: {
+ ets: 'editedAt',
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ setMessageAttachments(_id, attachments) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ attachments,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setSlackBotIdAndSlackTs(_id, slackBotId, slackTs) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ slackBotId,
+ slackTs,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ unlinkUserId(userId, newUserId, newUsername, newNameAlias) {
+ const query = {
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ alias: newNameAlias,
+ 'u._id': newUserId,
+ 'u.username' : newUsername,
+ 'u.name' : undefined,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ // INSERT
+ createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) {
+ const room = Rooms.findOneById(roomId, { fields: { sysMes: 1 } });
+ if ((room != null ? room.sysMes : undefined) === false) {
+ return;
+ }
+ const record = {
+ t: type,
+ rid: roomId,
+ ts: new Date,
+ msg: message,
+ u: {
+ _id: user._id,
+ username: user.username,
+ },
+ groupable: false,
+ };
+
+ if (settings.get('Message_Read_Receipt_Enabled')) {
+ record.unread = true;
+ }
+
+ _.extend(record, extraData);
+
+ record._id = this.insertOrUpsert(record);
+ Rooms.incMsgCountById(room._id, 1);
+ return record;
+ }
+
+ createNavigationHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) {
+ const type = 'livechat_navigation_history';
+ const room = Rooms.findOneById(roomId, { fields: { sysMes: 1 } });
+ if ((room != null ? room.sysMes : undefined) === false) {
+ return;
+ }
+ const record = {
+ t: type,
+ rid: roomId,
+ ts: new Date,
+ msg: message,
+ u: {
+ _id: user._id,
+ username: user.username,
+ },
+ groupable: false,
+ };
+
+ if (settings.get('Message_Read_Receipt_Enabled')) {
+ record.unread = true;
+ }
+
+ _.extend(record, extraData);
+
+ record._id = this.insertOrUpsert(record);
+ return record;
+ }
+
+ createUserJoinWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData);
+ }
+
+ createUserJoinWithRoomIdAndUserDiscussion(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('ut', roomId, message, user, extraData);
+ }
+
+ createUserLeaveWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('ul', roomId, message, user, extraData);
+ }
+
+ createUserRemovedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('ru', roomId, message, user, extraData);
+ }
+
+ createUserAddedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('au', roomId, message, user, extraData);
+ }
+
+ createCommandWithRoomIdAndUser(command, roomId, user, extraData) {
+ return this.createWithTypeRoomIdMessageAndUser('command', roomId, command, user, extraData);
+ }
+
+ createUserMutedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('user-muted', roomId, message, user, extraData);
+ }
+
+ createUserUnmutedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('user-unmuted', roomId, message, user, extraData);
+ }
+
+ createNewModeratorWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('new-moderator', roomId, message, user, extraData);
+ }
+
+ createModeratorRemovedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('moderator-removed', roomId, message, user, extraData);
+ }
+
+ createNewOwnerWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('new-owner', roomId, message, user, extraData);
+ }
+
+ createOwnerRemovedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData);
+ }
+
+ createNewLeaderWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('new-leader', roomId, message, user, extraData);
+ }
+
+ createLeaderRemovedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('leader-removed', roomId, message, user, extraData);
+ }
+
+ createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData);
+ }
+
+ createSubscriptionRoleRemovedWithRoomIdAndUser(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData);
+ }
+
+ createRejectedMessageByPeer(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('rejected-message-by-peer', roomId, message, user, extraData);
+ }
+
+ createPeerDoesNotExist(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('peer-does-not-exist', roomId, message, user, extraData);
+ }
+
+ // REMOVE
+ removeById(_id) {
+ const query = { _id };
+
+ return this.remove(query);
+ }
+
+ removeByRoomId(roomId) {
+ const query = { rid: roomId };
+
+ return this.remove(query);
+ }
+
+ removeByIdPinnedTimestampLimitAndUsers(rid, pinned, ignoreDiscussion = true, ts, limit, users = []) {
+ const query = {
+ rid,
+ ts,
+ };
+
+ if (pinned) {
+ query.pinned = { $ne: true };
+ }
+
+ if (ignoreDiscussion) {
+ query.drid = { $exists: 0 };
+ }
+
+ if (users.length) {
+ query['u.username'] = { $in: users };
+ }
+
+ if (!limit) {
+ return this.remove(query);
+ }
+
+ const messagesToDelete = this.find(query, {
+ fields: {
+ _id: 1,
+ },
+ limit,
+ }).map(({ _id }) => _id);
+
+ return this.remove({
+ _id: {
+ $in: messagesToDelete,
+ },
+ });
+ }
+
+ removeByUserId(userId) {
+ const query = { 'u._id': userId };
+
+ return this.remove(query);
+ }
+
+ async removeFilesByRoomId(roomId) {
+ this.find({
+ rid: roomId,
+ 'file._id': {
+ $exists: true,
+ },
+ }, {
+ fields: {
+ 'file._id': 1,
+ },
+ }).fetch().forEach((document) => FileUpload.getStore('Uploads').deleteById(document.file._id));
+ }
+
+ getMessageByFileId(fileID) {
+ return this.findOne({ 'file._id': fileID });
+ }
+
+ setAsRead(rid, until) {
+ return this.update({
+ rid,
+ unread: true,
+ ts: { $lt: until },
+ }, {
+ $unset: {
+ unread: 1,
+ },
+ }, {
+ multi: true,
+ });
+ }
+
+ setAsReadById(_id) {
+ return this.update({
+ _id,
+ }, {
+ $unset: {
+ unread: 1,
+ },
+ });
+ }
+
+ findUnreadMessagesByRoomAndDate(rid, after) {
+ const query = {
+ unread: true,
+ rid,
+ };
+
+ if (after) {
+ query.ts = { $gt: after };
+ }
+
+ return this.find(query, {
+ fields: {
+ _id: 1,
+ },
+ });
+ }
+
+ /**
+ * Copy metadata from the discussion to the system message in the parent channel
+ * which links to the discussion.
+ * Since we don't pass this metadata into the model's function, it is not a subject
+ * to race conditions: If multiple updates occur, the current state will be updated
+ * only if the new state of the discussion room is really newer.
+ */
+ refreshDiscussionMetadata({ rid }) {
+ if (!rid) {
+ return false;
+ }
+ const { lm: dlm, msgs: dcount } = Rooms.findOneById(rid, {
+ fields: {
+ msgs: 1,
+ lm: 1,
+ },
+ });
+
+ const query = {
+ drid: rid,
+ };
+
+ return this.update(query, {
+ $set: {
+ dcount,
+ dlm,
+ },
+ }, { multi: 1 });
+ }
+
+ // //////////////////////////////////////////////////////////////////
+ // threads
+
+ countThreads() {
+ return this.find({ tcount: { $exists: true } }).count();
+ }
+
+ removeThreadRefByThreadId(tmid) {
+ const query = { tmid };
+ const update = {
+ $unset: {
+ tmid: 1,
+ },
+ };
+ return this.update(query, update, { multi: true });
+ }
+
+ updateRepliesByThreadId(tmid, replies, ts) {
+ const query = {
+ _id: tmid,
+ };
+
+ const update = {
+ $addToSet: {
+ replies: {
+ $each: replies,
+ },
+ },
+ $set: {
+ tlm: ts,
+ },
+ $inc: {
+ tcount: 1,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ getThreadFollowsByThreadId(tmid) {
+ const msg = this.findOneById(tmid, { fields: { replies: 1 } });
+ return msg && msg.replies;
+ }
+
+ getFirstReplyTsByThreadId(tmid) {
+ return this.findOne({ tmid }, { fields: { ts: 1 }, sort: { ts: 1 } });
+ }
+
+ unsetThreadByThreadId(tmid) {
+ const query = {
+ _id: tmid,
+ };
+
+ const update = {
+ $unset: {
+ tcount: 1,
+ tlm: 1,
+ replies: 1,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateThreadLastMessageAndCountByThreadId(tmid, tlm, tcount) {
+ const query = {
+ _id: tmid,
+ };
+
+ const update = {
+ $set: {
+ tlm,
+ },
+ $inc: {
+ tcount,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ addThreadFollowerByThreadId(tmid, userId) {
+ const query = {
+ _id: tmid,
+ };
+
+ const update = {
+ $addToSet: {
+ replies: userId,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ removeThreadFollowerByThreadId(tmid, userId) {
+ const query = {
+ _id: tmid,
+ };
+
+ const update = {
+ $pull: {
+ replies: userId,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ findThreadsByRoomId(rid, skip, limit) {
+ return this.find({ rid, tcount: { $exists: true } }, { sort: { tlm: -1 }, skip, limit });
+ }
+}
+
+export default new Messages();
diff --git a/app/models/server/models/OAuthApps.js b/app/models/server/models/OAuthApps.js
new file mode 100644
index 000000000000..6aedffb63ae0
--- /dev/null
+++ b/app/models/server/models/OAuthApps.js
@@ -0,0 +1,9 @@
+import { Base } from './_Base';
+
+export class OAuthApps extends Base {
+ constructor() {
+ super('oauth_apps');
+ }
+}
+
+export default new OAuthApps();
diff --git a/packages/rocketchat-oembed/server/models/OEmbedCache.js b/app/models/server/models/OEmbedCache.js
similarity index 79%
rename from packages/rocketchat-oembed/server/models/OEmbedCache.js
rename to app/models/server/models/OEmbedCache.js
index 7f83342b51b2..444d30b1c63f 100644
--- a/packages/rocketchat-oembed/server/models/OEmbedCache.js
+++ b/app/models/server/models/OEmbedCache.js
@@ -1,6 +1,6 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Base } from './_Base';
-RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base {
+export class OEmbedCache extends Base {
constructor() {
super('oembed_cache');
this.tryEnsureIndex({ updatedAt: 1 });
@@ -34,6 +34,6 @@ RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base {
};
return this.remove(query);
}
-};
-
+}
+export default new OEmbedCache();
diff --git a/packages/rocketchat-models/server/models/Permissions.js b/app/models/server/models/Permissions.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Permissions.js
rename to app/models/server/models/Permissions.js
diff --git a/app/models/server/models/ReadReceipts.js b/app/models/server/models/ReadReceipts.js
new file mode 100644
index 000000000000..717f4d91afc4
--- /dev/null
+++ b/app/models/server/models/ReadReceipts.js
@@ -0,0 +1,21 @@
+import { Base } from './_Base';
+
+export class ReadReceipts extends Base {
+ constructor(...args) {
+ super(...args);
+
+ this.tryEnsureIndex({
+ roomId: 1,
+ userId: 1,
+ messageId: 1,
+ }, {
+ unique: 1,
+ });
+ }
+
+ findByMessageId(messageId) {
+ return this.find({ messageId });
+ }
+}
+
+export default new ReadReceipts('message_read_receipt');
diff --git a/packages/rocketchat-models/server/models/Reports.js b/app/models/server/models/Reports.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Reports.js
rename to app/models/server/models/Reports.js
diff --git a/packages/rocketchat-models/server/models/Roles.js b/app/models/server/models/Roles.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Roles.js
rename to app/models/server/models/Roles.js
diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js
new file mode 100644
index 000000000000..5023f36e14c9
--- /dev/null
+++ b/app/models/server/models/Rooms.js
@@ -0,0 +1,1422 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from './_Base';
+import Messages from './Messages';
+import Subscriptions from './Subscriptions';
+import Settings from './Settings';
+import _ from 'underscore';
+import s from 'underscore.string';
+
+export class Rooms extends Base {
+ constructor(...args) {
+ super(...args);
+
+ this.tryEnsureIndex({ name: 1 }, { unique: true, sparse: true });
+ this.tryEnsureIndex({ default: 1 });
+ this.tryEnsureIndex({ t: 1 });
+ this.tryEnsureIndex({ 'u._id': 1 });
+ this.tryEnsureIndex({ 'tokenpass.tokens.token': 1 });
+ this.tryEnsureIndex({ open: 1 }, { sparse: true });
+ this.tryEnsureIndex({ departmentId: 1 }, { sparse: true });
+ this.tryEnsureIndex({ ts: 1 });
+
+ // discussions
+ this.tryEnsureIndex({ prid: 1 }, { sparse: true });
+ }
+
+ findOneByIdOrName(_idOrName, options) {
+ const query = {
+ $or: [{
+ _id: _idOrName,
+ }, {
+ name: _idOrName,
+ }],
+ };
+
+ return this.findOne(query, options);
+ }
+
+ updateSurveyFeedbackById(_id, surveyFeedback) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ surveyFeedback,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateLivechatDataByToken(token, key, value, overwrite = true) {
+ const query = {
+ 'v.token': token,
+ open: true,
+ };
+
+ if (!overwrite) {
+ const room = this.findOne(query, { fields: { livechatData: 1 } });
+ if (room.livechatData && typeof room.livechatData[key] !== 'undefined') {
+ return true;
+ }
+ }
+
+ const update = {
+ $set: {
+ [`livechatData.${ key }`]: value,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ findLivechat(filter = {}, offset = 0, limit = 20) {
+ const query = _.extend(filter, {
+ t: 'l',
+ });
+
+ return this.find(query, { sort: { ts: - 1 }, offset, limit });
+ }
+
+ findLivechatById(_id, fields) {
+ const options = {};
+
+ if (fields) {
+ options.fields = fields;
+ }
+
+ const query = {
+ t: 'l',
+ _id,
+ };
+
+ return this.find(query, options);
+ }
+
+ findOneLivechatById(_id, fields) {
+ const options = {};
+
+ if (fields) {
+ options.fields = fields;
+ }
+
+ const query = {
+ t: 'l',
+ _id,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findLivechatByIdAndVisitorToken(_id, visitorToken, fields) {
+ const options = {};
+
+ if (fields) {
+ options.fields = fields;
+ }
+
+ const query = {
+ t: 'l',
+ _id,
+ 'v.token': visitorToken,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findLivechatByVisitorToken(visitorToken, fields) {
+ const options = {};
+
+ if (fields) {
+ options.fields = fields;
+ }
+
+ const query = {
+ t: 'l',
+ 'v.token': visitorToken,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ updateLivechatRoomCount = function() {
+ const settingsRaw = Settings.model.rawCollection();
+ const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw);
+
+ const query = {
+ _id: 'Livechat_Room_Count',
+ };
+
+ const update = {
+ $inc: {
+ value: 1,
+ },
+ };
+
+ const livechatCount = findAndModify(query, null, update);
+
+ return livechatCount.value.value;
+ }
+
+ findOpenByVisitorToken(visitorToken, options) {
+ const query = {
+ open: true,
+ 'v.token': visitorToken,
+ };
+
+ return this.find(query, options);
+ }
+
+ findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options) {
+ const query = {
+ open: true,
+ 'v.token': visitorToken,
+ departmentId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByVisitorToken(visitorToken) {
+ const query = {
+ 'v.token': visitorToken,
+ };
+
+ return this.find(query);
+ }
+
+ findByVisitorId(visitorId) {
+ const query = {
+ 'v._id': visitorId,
+ };
+
+ return this.find(query);
+ }
+
+ findOneOpenByRoomIdAndVisitorToken(roomId, visitorToken, options) {
+ const query = {
+ _id: roomId,
+ open: true,
+ 'v.token': visitorToken,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ setResponseByRoomId(roomId, response) {
+ return this.update({
+ _id: roomId,
+ }, {
+ $set: {
+ responseBy: {
+ _id: response.user._id,
+ username: response.user.username,
+ },
+ },
+ $unset: {
+ waitingResponse: 1,
+ },
+ });
+ }
+
+ saveAnalyticsDataByRoomId(room, message, analyticsData) {
+ const update = {
+ $set: {},
+ };
+
+ if (analyticsData) {
+ update.$set['metrics.response.avg'] = analyticsData.avgResponseTime;
+
+ update.$inc = {};
+ update.$inc['metrics.response.total'] = 1;
+ update.$inc['metrics.response.tt'] = analyticsData.responseTime;
+ update.$inc['metrics.reaction.tt'] = analyticsData.reactionTime;
+ }
+
+ if (analyticsData && analyticsData.firstResponseTime) {
+ update.$set['metrics.response.fd'] = analyticsData.firstResponseDate;
+ update.$set['metrics.response.ft'] = analyticsData.firstResponseTime;
+ update.$set['metrics.reaction.fd'] = analyticsData.firstReactionDate;
+ update.$set['metrics.reaction.ft'] = analyticsData.firstReactionTime;
+ }
+
+ // livechat analytics : update last message timestamps
+ const visitorLastQuery = (room.metrics && room.metrics.v) ? room.metrics.v.lq : room.ts;
+ const agentLastReply = (room.metrics && room.metrics.servedBy) ? room.metrics.servedBy.lr : room.ts;
+
+ if (message.token) { // update visitor timestamp, only if its new inquiry and not continuing message
+ if (agentLastReply >= visitorLastQuery) { // if first query, not continuing query from visitor
+ update.$set['metrics.v.lq'] = message.ts;
+ }
+ } else if (visitorLastQuery > agentLastReply) { // update agent timestamp, if first response, not continuing
+ update.$set['metrics.servedBy.lr'] = message.ts;
+ }
+
+ return this.update({
+ _id: room._id,
+ }, update);
+ }
+
+ getTotalConversationsBetweenDate(t, date) {
+ const query = {
+ t,
+ ts: {
+ $gte: new Date(date.gte), // ISO Date, ts >= date.gte
+ $lt: new Date(date.lt), // ISODate, ts < date.lt
+ },
+ };
+
+ return this.find(query).count();
+ }
+
+ getAnalyticsMetricsBetweenDate(t, date) {
+ const query = {
+ t,
+ ts: {
+ $gte: new Date(date.gte), // ISO Date, ts >= date.gte
+ $lt: new Date(date.lt), // ISODate, ts < date.lt
+ },
+ };
+
+ return this.find(query, { fields: { ts: 1, departmentId: 1, open: 1, servedBy: 1, metrics: 1, msgs: 1 } });
+ }
+
+ closeByRoomId(roomId, closeInfo) {
+ return this.update({
+ _id: roomId,
+ }, {
+ $set: {
+ closer: closeInfo.closer,
+ closedBy: closeInfo.closedBy,
+ closedAt: closeInfo.closedAt,
+ 'metrics.chatDuration': closeInfo.chatDuration,
+ 'v.status': 'offline',
+ },
+ $unset: {
+ open: 1,
+ },
+ });
+ }
+
+ findOpenByAgent(userId) {
+ const query = {
+ open: true,
+ 'servedBy._id': userId,
+ };
+
+ return this.find(query);
+ }
+
+ changeAgentByRoomId(roomId, newAgent) {
+ const query = {
+ _id: roomId,
+ };
+ const update = {
+ $set: {
+ servedBy: {
+ _id: newAgent.agentId,
+ username: newAgent.username,
+ ts: new Date(),
+ },
+ },
+ };
+
+ if (newAgent.ts) {
+ update.$set.servedBy.ts = newAgent.ts;
+ }
+
+ this.update(query, update);
+ }
+
+ changeDepartmentIdByRoomId(roomId, departmentId) {
+ const query = {
+ _id: roomId,
+ };
+ const update = {
+ $set: {
+ departmentId,
+ },
+ };
+
+ this.update(query, update);
+ }
+
+ saveCRMDataByRoomId(roomId, crmData) {
+ const query = {
+ _id: roomId,
+ };
+ const update = {
+ $set: {
+ crmData,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateVisitorStatus(token, status) {
+ const query = {
+ 'v.token': token,
+ open: true,
+ };
+
+ const update = {
+ $set: {
+ 'v.status': status,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ removeAgentByRoomId(roomId) {
+ const query = {
+ _id: roomId,
+ };
+ const update = {
+ $unset: {
+ servedBy: 1,
+ },
+ };
+
+ this.update(query, update);
+ }
+
+ removeByVisitorToken(token) {
+ const query = {
+ 'v.token': token,
+ };
+
+ this.remove(query);
+ }
+
+ setJitsiTimeout(_id, time) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ jitsiTimeout: time,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ findByTokenpass(tokens) {
+ const query = {
+ 'tokenpass.tokens.token': {
+ $in: tokens,
+ },
+ };
+
+ return this._db.find(query).fetch();
+ }
+
+ setTokensById(_id, tokens) {
+ const update = {
+ $set: {
+ 'tokenpass.tokens.token': tokens,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ findAllTokenChannels() {
+ const query = {
+ tokenpass: { $exists: true },
+ };
+ const options = {
+ fields: {
+ tokenpass: 1,
+ },
+ };
+ return this._db.find(query, options);
+ }
+
+ setReactionsInLastMessage(roomId, lastMessage) {
+ return this.update({ _id: roomId }, { $set: { 'lastMessage.reactions': lastMessage.reactions } });
+ }
+
+ unsetReactionsInLastMessage(roomId) {
+ return this.update({ _id: roomId }, { $unset: { lastMessage: { reactions: 1 } } });
+ }
+
+ updateLastMessageStar(roomId, userId, starred) {
+ let update;
+ const query = { _id: roomId };
+
+ if (starred) {
+ update = {
+ $addToSet: {
+ 'lastMessage.starred': { _id: userId },
+ },
+ };
+ } else {
+ update = {
+ $pull: {
+ 'lastMessage.starred': { _id: userId },
+ },
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ setLastMessageSnippeted(roomId, message, snippetName, snippetedBy, snippeted, snippetedAt) {
+ const query = { _id: roomId };
+
+ const msg = `\`\`\`${ message.msg }\`\`\``;
+
+ const update = {
+ $set: {
+ 'lastMessage.msg': msg,
+ 'lastMessage.snippeted': snippeted,
+ 'lastMessage.snippetedAt': snippetedAt || new Date,
+ 'lastMessage.snippetedBy': snippetedBy,
+ 'lastMessage.snippetName': snippetName,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setLastMessagePinned(roomId, pinnedBy, pinned, pinnedAt) {
+ const query = { _id: roomId };
+
+ const update = {
+ $set: {
+ 'lastMessage.pinned': pinned,
+ 'lastMessage.pinnedAt': pinnedAt || new Date,
+ 'lastMessage.pinnedBy': pinnedBy,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setSentiment(roomId, sentiment) {
+ return this.update({ _id: roomId }, { $set: { sentiment } });
+ }
+
+ setDescriptionById(_id, description) {
+ const query = {
+ _id,
+ };
+ const update = {
+ $set: {
+ description,
+ },
+ };
+ return this.update(query, update);
+ }
+
+ setStreamingOptionsById(_id, streamingOptions) {
+ const update = {
+ $set: {
+ streamingOptions,
+ },
+ };
+ return this.update({ _id }, update);
+ }
+
+ setTokenpassById(_id, tokenpass) {
+ const update = {
+ $set: {
+ tokenpass,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ setReadOnlyById(_id, readOnly, hasPermission) {
+ if (!hasPermission) {
+ throw new Error('You must provide "hasPermission" function to be able to call this method');
+ }
+ const query = {
+ _id,
+ };
+ const update = {
+ $set: {
+ ro: readOnly,
+ muted: [],
+ },
+ };
+ if (readOnly) {
+ Subscriptions.findByRoomIdWhenUsernameExists(_id, { fields: { 'u._id': 1, 'u.username': 1 } }).forEach(function({ u: user }) {
+ if (hasPermission(user._id, 'post-readonly')) {
+ return;
+ }
+ return update.$set.muted.push(user.username);
+ });
+ } else {
+ update.$unset = {
+ muted: '',
+ };
+ }
+
+ if (update.$set.muted.length === 0) {
+ delete update.$set.muted;
+ }
+
+ return this.update(query, update);
+ }
+
+ setAllowReactingWhenReadOnlyById = function(_id, allowReacting) {
+ const query = {
+ _id,
+ };
+ const update = {
+ $set: {
+ reactWhenReadOnly: allowReacting,
+ },
+ };
+ return this.update(query, update);
+ }
+
+ setSystemMessagesById = function(_id, systemMessages) {
+ const query = {
+ _id,
+ };
+ const update = {
+ $set: {
+ sysMes: systemMessages,
+ },
+ };
+ return this.update(query, update);
+ }
+
+ setE2eKeyId(_id, e2eKeyId, options) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ e2eKeyId,
+ },
+ };
+
+ return this.update(query, update, options);
+ }
+
+ findOneByImportId(_id, options) {
+ const query = { importIds: _id };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByName(name, options) {
+ const query = { name };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByNameAndNotId(name, rid) {
+ const query = {
+ _id: { $ne: rid },
+ name,
+ };
+
+ return this.findOne(query);
+ }
+
+ findOneByDisplayName(fname, options) {
+ const query = { fname };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByNameAndType(name, type, options) {
+ const query = {
+ name,
+ t: type,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ // FIND
+
+ findById(roomId, options) {
+ return this.find({ _id: roomId }, options);
+ }
+
+ findByIds(roomIds, options) {
+ return this.find({ _id: { $in: [].concat(roomIds) } }, options);
+ }
+
+ findByType(type, options) {
+ const query = { t: type };
+
+ return this.find(query, options);
+ }
+
+ findByTypeInIds(type, ids, options) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ t: type,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByTypes(types, options) {
+ const query = {
+ t: {
+ $in: types,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByUserId(userId, options) {
+ const query = { 'u._id': userId };
+
+ return this.find(query, options);
+ }
+
+ findBySubscriptionUserId(userId, options) {
+ const data = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch()
+ .map((item) => item.rid);
+
+ const query = {
+ _id: {
+ $in: data,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findBySubscriptionTypeAndUserId(type, userId, options) {
+ const data = Subscriptions.findByUserIdAndType(userId, type, { fields: { rid: 1 } }).fetch()
+ .map((item) => item.rid);
+
+ const query = {
+ t: type,
+ _id: {
+ $in: data,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) {
+ const ids = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch()
+ .map((item) => item.rid);
+
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ _updatedAt: {
+ $gt: _updatedAt,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByNameContaining(name, options) {
+ const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
+
+ const query = {
+ $or: [
+ { name: nameRegex },
+ {
+ t: 'd',
+ usernames: nameRegex,
+ },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+
+ findByNameContainingAndTypes(name, types, options) {
+ const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
+
+ const query = {
+ t: {
+ $in: types,
+ },
+ $or: [
+ { name: nameRegex },
+ {
+ t: 'd',
+ usernames: nameRegex,
+ },
+ ],
+ };
+
+ return this.find(query, options);
+ }
+
+ findByNameAndType(name, type, options) {
+ const query = {
+ t: type,
+ name,
+ };
+
+ // do not use cache
+ return this._db.find(query, options);
+ }
+
+ findByNameAndTypeNotDefault(name, type, options) {
+ const query = {
+ t: type,
+ name,
+ default: {
+ $ne: true,
+ },
+ };
+
+ // do not use cache
+ return this._db.find(query, options);
+ }
+
+ findByNameAndTypesNotInIds(name, types, ids, options) {
+ const query = {
+ _id: {
+ $ne: ids,
+ },
+ t: {
+ $in: types,
+ },
+ name,
+ };
+
+ // do not use cache
+ return this._db.find(query, options);
+ }
+
+ findChannelAndPrivateByNameStarting(name, options) {
+ const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i');
+
+ const query = {
+ t: {
+ $in: ['c', 'p'],
+ },
+ name: nameRegex,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByDefaultAndTypes(defaultValue, types, options) {
+ const query = {
+ default: defaultValue,
+ t: {
+ $in: types,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findDirectRoomContainingUsername(username, options) {
+ const query = {
+ t: 'd',
+ usernames: username,
+ };
+
+ return this.find(query, options);
+ }
+
+ findDirectRoomContainingAllUsernames(usernames, options) {
+ const query = {
+ t: 'd',
+ usernames: { $size: usernames.length, $all: usernames },
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findByTypeAndName(type, name, options) {
+ const query = {
+ name,
+ t: type,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findByTypeAndNameContaining(type, name, options) {
+ const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
+
+ const query = {
+ name: nameRegex,
+ t: type,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByTypeInIdsAndNameContaining(type, ids, name, options) {
+ const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i');
+
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ name: nameRegex,
+ t: type,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByTypeAndArchivationState(type, archivationstate, options) {
+ const query = { t: type };
+
+ if (archivationstate) {
+ query.archived = true;
+ } else {
+ query.archived = { $ne: true };
+ }
+
+ return this.find(query, options);
+ }
+
+ // UPDATE
+ addImportIds(_id, importIds) {
+ importIds = [].concat(importIds);
+ const query = { _id };
+
+ const update = {
+ $addToSet: {
+ importIds: {
+ $each: importIds,
+ },
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ archiveById(_id) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ archived: true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ unarchiveById(_id) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ archived: false,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setNameById(_id, name, fname) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ name,
+ fname,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setFnameById(_id, fname) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ fname,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ incMsgCountById(_id, inc) {
+ if (inc == null) { inc = 1; }
+ const query = { _id };
+
+ const update = {
+ $inc: {
+ msgs: inc,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ incMsgCountAndSetLastMessageById(_id, inc, lastMessageTimestamp, lastMessage) {
+ if (inc == null) { inc = 1; }
+ const query = { _id };
+
+ const update = {
+ $set: {
+ lm: lastMessageTimestamp,
+ },
+ $inc: {
+ msgs: inc,
+ },
+ };
+
+ if (lastMessage) {
+ update.$set.lastMessage = lastMessage;
+ }
+
+ return this.update(query, update);
+ }
+
+ incUsersCountById(_id, inc = 1) {
+ const query = { _id };
+
+ const update = {
+ $inc: {
+ usersCount: inc,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ incUsersCountByIds(ids, inc = 1) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ };
+
+ const update = {
+ $inc: {
+ usersCount: inc,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ setLastMessageById(_id, lastMessage) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ lastMessage,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ resetLastMessageById(_id, messageId) {
+ const query = { _id };
+ const lastMessage = Messages.getLastVisibleMessageSentWithNoTypeByRoomId(_id, messageId);
+
+ const update = lastMessage ? {
+ $set: {
+ lastMessage,
+ },
+ } : {
+ $unset: {
+ lastMessage: 1,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ replaceUsername(previousUsername, username) {
+ const query = { usernames: previousUsername };
+
+ const update = {
+ $set: {
+ 'usernames.$': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ replaceMutedUsername(previousUsername, username) {
+ const query = { muted: previousUsername };
+
+ const update = {
+ $set: {
+ 'muted.$': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ replaceUsernameOfUserByUserId(userId, username) {
+ const query = { 'u._id': userId };
+
+ const update = {
+ $set: {
+ 'u.username': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ setJoinCodeById(_id, joinCode) {
+ let update;
+ const query = { _id };
+
+ if ((joinCode != null ? joinCode.trim() : undefined) !== '') {
+ update = {
+ $set: {
+ joinCodeRequired: true,
+ joinCode,
+ },
+ };
+ } else {
+ update = {
+ $set: {
+ joinCodeRequired: false,
+ },
+ $unset: {
+ joinCode: 1,
+ },
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ setUserById(_id, user) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ u: {
+ _id: user._id,
+ username: user.username,
+ },
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setTypeById(_id, type) {
+ const query = { _id };
+ const update = {
+ $set: {
+ t: type,
+ },
+ };
+ if (type === 'p') {
+ update.$unset = { default: '' };
+ }
+
+ return this.update(query, update);
+ }
+
+ setTopicById(_id, topic) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ topic,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setAnnouncementById(_id, announcement, announcementDetails) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ announcement,
+ announcementDetails,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setCustomFieldsById(_id, customFields) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ customFields,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ muteUsernameByRoomId(_id, username) {
+ const query = { _id };
+
+ const update = {
+ $addToSet: {
+ muted: username,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ unmuteUsernameByRoomId(_id, username) {
+ const query = { _id };
+
+ const update = {
+ $pull: {
+ muted: username,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveDefaultById(_id, defaultValue) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ default: defaultValue === 'true',
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveRetentionEnabledById(_id, value) {
+ const query = { _id };
+
+ const update = {};
+
+ if (value == null) {
+ update.$unset = { 'retention.enabled': true };
+ } else {
+ update.$set = { 'retention.enabled': !!value };
+ }
+
+ return this.update(query, update);
+ }
+
+ saveRetentionMaxAgeById(_id, value) {
+ const query = { _id };
+
+ value = Number(value);
+ if (!value) {
+ value = 30;
+ }
+
+ const update = {
+ $set: {
+ 'retention.maxAge': value,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveRetentionExcludePinnedById(_id, value) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ 'retention.excludePinned': value === true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveRetentionFilesOnlyById(_id, value) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ 'retention.filesOnly': value === true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveRetentionOverrideGlobalById(_id, value) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ 'retention.overrideGlobal': value === true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveEncryptedById(_id, value) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ encrypted: value === true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setTopicAndTagsById(_id, topic, tags) {
+ const setData = {};
+ const unsetData = {};
+
+ if (topic != null) {
+ if (!_.isEmpty(s.trim(topic))) {
+ setData.topic = s.trim(topic);
+ } else {
+ unsetData.topic = 1;
+ }
+ }
+
+ if (tags != null) {
+ if (!_.isEmpty(s.trim(tags))) {
+ setData.tags = s.trim(tags).split(',').map((tag) => s.trim(tag));
+ } else {
+ unsetData.tags = 1;
+ }
+ }
+
+ const update = {};
+
+ if (!_.isEmpty(setData)) {
+ update.$set = setData;
+ }
+
+ if (!_.isEmpty(unsetData)) {
+ update.$unset = unsetData;
+ }
+
+ if (_.isEmpty(update)) {
+ return;
+ }
+
+ return this.update({ _id }, update);
+ }
+
+ // INSERT
+ createWithTypeNameUserAndUsernames(type, name, fname, user, usernames, extraData) {
+ const room = {
+ name,
+ fname,
+ t: type,
+ usernames,
+ msgs: 0,
+ usersCount: 0,
+ u: {
+ _id: user._id,
+ username: user.username,
+ },
+ };
+
+ _.extend(room, extraData);
+
+ room._id = this.insert(room);
+ return room;
+ }
+
+ createWithIdTypeAndName(_id, type, name, extraData) {
+ const room = {
+ _id,
+ ts: new Date(),
+ t: type,
+ name,
+ usernames: [],
+ msgs: 0,
+ usersCount: 0,
+ };
+
+ _.extend(room, extraData);
+
+ this.insert(room);
+ return room;
+ }
+
+ createWithFullRoomData(room) {
+ delete room._id;
+
+ room._id = this.insert(room);
+ return room;
+ }
+
+
+ // REMOVE
+ removeById(_id) {
+ const query = { _id };
+
+ return this.remove(query);
+ }
+
+ removeDirectRoomContainingUsername(username) {
+ const query = {
+ t: 'd',
+ usernames: username,
+ };
+
+ return this.remove(query);
+ }
+
+ // ############################
+ // Discussion
+ findDiscussionParentByNameStarting(name, options) {
+ const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i');
+
+ const query = {
+ t: {
+ $in: ['c'],
+ },
+ name: nameRegex,
+ archived: { $ne: true },
+ prid: {
+ $exists: false,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ setLinkMessageById(_id, linkMessageId) {
+ const query = { _id };
+
+ const update = {
+ $set: {
+ linkMessageId,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ countDiscussions() {
+ return this.find({ prid: { $exists: true } }).count();
+ }
+}
+
+export default new Rooms('room', true);
diff --git a/packages/rocketchat-models/server/models/Sessions.js b/app/models/server/models/Sessions.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Sessions.js
rename to app/models/server/models/Sessions.js
diff --git a/packages/rocketchat-models/server/models/Settings.js b/app/models/server/models/Settings.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Settings.js
rename to app/models/server/models/Settings.js
diff --git a/app/models/server/models/SmarshHistory.js b/app/models/server/models/SmarshHistory.js
new file mode 100644
index 000000000000..9b2b7abc5043
--- /dev/null
+++ b/app/models/server/models/SmarshHistory.js
@@ -0,0 +1,9 @@
+import { Base } from './_Base';
+
+export class SmarshHistory extends Base {
+ constructor() {
+ super('smarsh_history');
+ }
+}
+
+export default new SmarshHistory();
diff --git a/packages/rocketchat-models/server/models/Statistics.js b/app/models/server/models/Statistics.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Statistics.js
rename to app/models/server/models/Statistics.js
diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js
new file mode 100644
index 000000000000..9b915999d28c
--- /dev/null
+++ b/app/models/server/models/Subscriptions.js
@@ -0,0 +1,1344 @@
+import { Meteor } from 'meteor/meteor';
+import { Base } from './_Base';
+import { Match } from 'meteor/check';
+import Rooms from './Rooms';
+import Users from './Users';
+import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref';
+import _ from 'underscore';
+
+export class Subscriptions extends Base {
+ constructor(...args) {
+ super(...args);
+
+ this.tryEnsureIndex({ rid: 1, 'u._id': 1 }, { unique: 1 });
+ this.tryEnsureIndex({ rid: 1, 'u.username': 1 });
+ this.tryEnsureIndex({ rid: 1, alert: 1, 'u._id': 1 });
+ this.tryEnsureIndex({ rid: 1, roles: 1 });
+ this.tryEnsureIndex({ 'u._id': 1, name: 1, t: 1 });
+ this.tryEnsureIndex({ open: 1 });
+ this.tryEnsureIndex({ alert: 1 });
+
+ this.tryEnsureIndex({ rid: 1, 'u._id': 1, open: 1 });
+
+ this.tryEnsureIndex({ ts: 1 });
+ this.tryEnsureIndex({ ls: 1 });
+ this.tryEnsureIndex({ audioNotifications: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ desktopNotifications: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ mobilePushNotifications: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ emailNotifications: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ autoTranslate: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ autoTranslateLanguage: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ 'userHighlights.0': 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ prid: 1 });
+ }
+
+ findByRoomIds(roomIds) {
+ const query = {
+ rid: {
+ $in: roomIds,
+ },
+ };
+ const options = {
+ fields: {
+ 'u._id': 1,
+ rid: 1,
+ },
+ };
+
+ return this._db.find(query, options);
+ }
+
+ removeByVisitorToken(token) {
+ const query = {
+ 'v.token': token,
+ };
+
+ this.remove(query);
+ }
+
+ updateAutoTranslateById(_id, autoTranslate) {
+ const query = {
+ _id,
+ };
+
+ let update;
+ if (autoTranslate) {
+ update = {
+ $set: {
+ autoTranslate,
+ },
+ };
+ } else {
+ update = {
+ $unset: {
+ autoTranslate: 1,
+ },
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ updateAutoTranslateLanguageById(_id, autoTranslateLanguage) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ autoTranslateLanguage,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ getAutoTranslateLanguagesByRoomAndNotUser(rid, userId) {
+ const subscriptionsRaw = this.model.rawCollection();
+ const distinct = Meteor.wrapAsync(subscriptionsRaw.distinct, subscriptionsRaw);
+ const query = {
+ rid,
+ 'u._id': { $ne: userId },
+ autoTranslate: true,
+ };
+ return distinct('autoTranslateLanguage', query);
+ }
+
+ roleBaseQuery(userId, scope) {
+ if (scope == null) {
+ return;
+ }
+
+ const query = { 'u._id': userId };
+ if (!_.isUndefined(scope)) {
+ query.rid = scope;
+ }
+ return query;
+ }
+
+ findByRidWithoutE2EKey(rid, options) {
+ const query = {
+ rid,
+ E2EKey: {
+ $exists: false,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ updateAudioNotificationsById(_id, audioNotifications) {
+ const query = {
+ _id,
+ };
+
+ const update = {};
+
+ if (audioNotifications === 'default') {
+ update.$unset = { audioNotifications: 1 };
+ } else {
+ update.$set = { audioNotifications };
+ }
+
+ return this.update(query, update);
+ }
+
+ updateAudioNotificationValueById(_id, audioNotificationValue) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ audioNotificationValue,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateDesktopNotificationsById(_id, desktopNotifications) {
+ const query = {
+ _id,
+ };
+
+ const update = {};
+
+ if (desktopNotifications === null) {
+ update.$unset = {
+ desktopNotifications: 1,
+ desktopPrefOrigin: 1,
+ };
+ } else {
+ update.$set = {
+ desktopNotifications: desktopNotifications.value,
+ desktopPrefOrigin: desktopNotifications.origin,
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ updateDesktopNotificationDurationById(_id, value) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ desktopNotificationDuration: parseInt(value),
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateMobilePushNotificationsById(_id, mobilePushNotifications) {
+ const query = {
+ _id,
+ };
+
+ const update = {};
+
+ if (mobilePushNotifications === null) {
+ update.$unset = {
+ mobilePushNotifications: 1,
+ mobilePrefOrigin: 1,
+ };
+ } else {
+ update.$set = {
+ mobilePushNotifications: mobilePushNotifications.value,
+ mobilePrefOrigin: mobilePushNotifications.origin,
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ updateEmailNotificationsById(_id, emailNotifications) {
+ const query = {
+ _id,
+ };
+
+ const update = {};
+
+ if (emailNotifications === null) {
+ update.$unset = {
+ emailNotifications: 1,
+ emailPrefOrigin: 1,
+ };
+ } else {
+ update.$set = {
+ emailNotifications: emailNotifications.value,
+ emailPrefOrigin: emailNotifications.origin,
+ };
+ }
+
+ return this.update(query, update);
+ }
+
+ updateUnreadAlertById(_id, unreadAlert) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ unreadAlert,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateDisableNotificationsById(_id, disableNotifications) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ disableNotifications,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateHideUnreadStatusById(_id, hideUnreadStatus) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ hideUnreadStatus,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateMuteGroupMentions(_id, muteGroupMentions) {
+ const query = {
+ _id,
+ };
+
+ const update = {
+ $set: {
+ muteGroupMentions,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ findAlwaysNotifyAudioUsersByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ audioNotifications: 'all',
+ };
+
+ return this.find(query);
+ }
+
+ findAlwaysNotifyDesktopUsersByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ desktopNotifications: 'all',
+ };
+
+ return this.find(query);
+ }
+
+ findDontNotifyDesktopUsersByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ desktopNotifications: 'nothing',
+ };
+
+ return this.find(query);
+ }
+
+ findAlwaysNotifyMobileUsersByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ mobilePushNotifications: 'all',
+ };
+
+ return this.find(query);
+ }
+
+ findDontNotifyMobileUsersByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ mobilePushNotifications: 'nothing',
+ };
+
+ return this.find(query);
+ }
+
+ findWithSendEmailByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ emailNotifications: {
+ $exists: true,
+ },
+ };
+
+ return this.find(query, { fields: { emailNotifications: 1, u: 1 } });
+ }
+
+ findNotificationPreferencesByRoom(query/* { roomId: rid, desktopFilter: desktopNotifications, mobileFilter: mobilePushNotifications, emailFilter: emailNotifications }*/) {
+
+ return this._db.find(query, {
+ fields: {
+
+ // fields needed for notifications
+ rid: 1,
+ t: 1,
+ u: 1,
+ name: 1,
+ fname: 1,
+ code: 1,
+
+ // fields to define if should send a notification
+ ignored: 1,
+ audioNotifications: 1,
+ audioNotificationValue: 1,
+ desktopNotificationDuration: 1,
+ desktopNotifications: 1,
+ mobilePushNotifications: 1,
+ emailNotifications: 1,
+ disableNotifications: 1,
+ muteGroupMentions: 1,
+ userHighlights: 1,
+ },
+ });
+ }
+
+ findAllMessagesNotificationPreferencesByRoom(roomId) {
+ const query = {
+ rid: roomId,
+ 'u._id': { $exists: true },
+ $or: [
+ { desktopNotifications: { $in: ['all', 'mentions'] } },
+ { mobilePushNotifications: { $in: ['all', 'mentions'] } },
+ { emailNotifications: { $in: ['all', 'mentions'] } },
+ ],
+ };
+
+ return this._db.find(query, {
+ fields: {
+ 'u._id': 1,
+ audioNotifications: 1,
+ audioNotificationValue: 1,
+ desktopNotificationDuration: 1,
+ desktopNotifications: 1,
+ mobilePushNotifications: 1,
+ emailNotifications: 1,
+ disableNotifications: 1,
+ muteGroupMentions: 1,
+ },
+ });
+ }
+
+ resetUserE2EKey(userId) {
+ this.update({ 'u._id': userId }, {
+ $unset: {
+ E2EKey: '',
+ },
+ }, {
+ multi: true,
+ });
+ }
+
+ findByUserIdWithoutE2E(userId, options) {
+ const query = {
+ 'u._id': userId,
+ E2EKey: {
+ $exists: false,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ updateGroupE2EKey(_id, key) {
+ const query = { _id };
+ const update = { $set: { E2EKey: key } };
+ this.update(query, update);
+ return this.findOne({ _id });
+ }
+
+ findUsersInRoles(roles, scope, options) {
+ roles = [].concat(roles);
+
+ const query = {
+ roles: { $in: roles },
+ };
+
+ if (scope) {
+ query.rid = scope;
+ }
+
+ const subscriptions = this.find(query).fetch();
+
+ const users = _.compact(_.map(subscriptions, function(subscription) {
+ if ('undefined' !== typeof subscription.u && 'undefined' !== typeof subscription.u._id) {
+ return subscription.u._id;
+ }
+ }));
+
+ return Users.find({ _id: { $in: users } }, options);
+ }
+
+ // FIND ONE
+ findOneByRoomIdAndUserId(roomId, userId, options) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByRoomIdAndUsername(roomId, username, options) {
+ const query = {
+ rid: roomId,
+ 'u.username': username,
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByRoomNameAndUserId(roomName, userId) {
+ const query = {
+ name: roomName,
+ 'u._id': userId,
+ };
+
+ return this.findOne(query);
+ }
+
+ // FIND
+ findByUserId(userId, options) {
+ const query =
+ { 'u._id': userId };
+
+ return this.find(query, options);
+ }
+
+ findByUserIdAndType(userId, type, options) {
+ const query = {
+ 'u._id': userId,
+ t: type,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByUserIdAndTypes(userId, types, options) {
+ const query = {
+ 'u._id': userId,
+ t: {
+ $in: types,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByUserIdUpdatedAfter(userId, updatedAt, options) {
+ const query = {
+ 'u._id': userId,
+ _updatedAt: {
+ $gt: updatedAt,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomIdAndRoles(roomId, roles, options) {
+ roles = [].concat(roles);
+ const query = {
+ rid: roomId,
+ roles: { $in: roles },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByType(types, options) {
+ const query = {
+ t: {
+ $in: types,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByTypeAndUserId(type, userId, options) {
+ const query = {
+ t: type,
+ 'u._id': userId,
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomId(roomId, options) {
+ const query =
+ { rid: roomId };
+
+ return this.find(query, options);
+ }
+
+ findByRoomIdAndNotUserId(roomId, userId, options) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $ne: userId,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomAndUsersWithUserHighlights(roomId, users, options) {
+ const query = {
+ rid: roomId,
+ 'u._id': { $in: users },
+ 'userHighlights.0': { $exists: true },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomWithUserHighlights(roomId, options) {
+ const query = {
+ rid: roomId,
+ 'userHighlights.0': { $exists: true },
+ };
+
+ return this.find(query, options);
+ }
+
+ getLastSeen(options) {
+ if (options == null) {
+ options = {};
+ }
+ const query = { ls: { $exists: 1 } };
+ options.sort = { ls: -1 };
+ options.limit = 1;
+ const [subscription] = this.find(query, options).fetch();
+ return subscription && subscription.ls;
+ }
+
+ findByRoomIdAndUserIds(roomId, userIds, options) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $in: userIds,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomIdAndUserIdsOrAllMessages(roomId, userIds) {
+ const query = {
+ rid: roomId,
+ $or: [
+ { 'u._id': { $in: userIds } },
+ { emailNotifications: 'all' },
+ ],
+ };
+
+ return this.find(query);
+ }
+
+ findByRoomIdWhenUserIdExists(rid, options) {
+ const query = { rid, 'u._id': { $exists: 1 } };
+
+ return this.find(query, options);
+ }
+
+ findByRoomIdWhenUsernameExists(rid, options) {
+ const query = { rid, 'u.username': { $exists: 1 } };
+
+ return this.find(query, options);
+ }
+
+ findUnreadByUserId(userId) {
+ const query = {
+ 'u._id': userId,
+ unread: {
+ $gt: 0,
+ },
+ };
+
+ return this.find(query, { fields: { unread: 1 } });
+ }
+
+ getMinimumLastSeenByRoomId(rid) {
+ return this.db.findOne({
+ rid,
+ }, {
+ sort: {
+ ls: 1,
+ },
+ fields: {
+ ls: 1,
+ },
+ });
+ }
+
+ // UPDATE
+ archiveByRoomId(roomId) {
+ const query =
+ { rid: roomId };
+
+ const update = {
+ $set: {
+ alert: false,
+ open: false,
+ archived: true,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ unarchiveByRoomId(roomId) {
+ const query =
+ { rid: roomId };
+
+ const update = {
+ $set: {
+ alert: false,
+ open: true,
+ archived: false,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ hideByRoomIdAndUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ alert: false,
+ open: false,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ openByRoomIdAndUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ open: true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setAsReadByRoomIdAndUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ open: true,
+ alert: false,
+ unread: 0,
+ userMentions: 0,
+ groupMentions: 0,
+ ls: new Date,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setAsUnreadByRoomIdAndUserId(roomId, userId, firstMessageUnreadTimestamp) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ open: true,
+ alert: true,
+ ls: firstMessageUnreadTimestamp,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setCustomFieldsDirectMessagesByUserId(userId, fields) {
+ const query = {
+ 'u._id': userId,
+ t: 'd',
+ };
+ const update = { $set: { customFields: fields } };
+ const options = { multi: true };
+
+ return this.update(query, update, options);
+ }
+
+ setFavoriteByRoomIdAndUserId(roomId, userId, favorite) {
+ if (favorite == null) {
+ favorite = true;
+ }
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ f: favorite,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateNameAndAlertByRoomId(roomId, name, fname) {
+ const query =
+ { rid: roomId };
+
+ const update = {
+ $set: {
+ name,
+ fname,
+ alert: true,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateDisplayNameByRoomId(roomId, fname) {
+ const query =
+ { rid: roomId };
+
+ const update = {
+ $set: {
+ fname,
+ name: fname,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ setUserUsernameByUserId(userId, username) {
+ const query =
+ { 'u._id': userId };
+
+ const update = {
+ $set: {
+ 'u.username': username,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ setNameForDirectRoomsWithOldName(oldName, name) {
+ const query = {
+ name: oldName,
+ t: 'd',
+ };
+
+ const update = {
+ $set: {
+ name,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ incUnreadForRoomIdExcludingUserId(roomId, userId, inc) {
+ if (inc == null) {
+ inc = 1;
+ }
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $ne: userId,
+ },
+ };
+
+ const update = {
+ $set: {
+ alert: true,
+ open: true,
+ },
+ $inc: {
+ unread: inc,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ incGroupMentionsAndUnreadForRoomIdExcludingUserId(roomId, userId, incGroup = 1, incUnread = 1) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $ne: userId,
+ },
+ };
+
+ const update = {
+ $set: {
+ alert: true,
+ open: true,
+ },
+ $inc: {
+ unread: incUnread,
+ groupMentions: incGroup,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ incUserMentionsAndUnreadForRoomIdAndUserIds(roomId, userIds, incUser = 1, incUnread = 1) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $in: userIds,
+ },
+ };
+
+ const update = {
+ $set: {
+ alert: true,
+ open: true,
+ },
+ $inc: {
+ unread: incUnread,
+ userMentions: incUser,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ ignoreUser({ _id, ignoredUser : ignored, ignore = true }) {
+ const query = {
+ _id,
+ };
+ const update = {
+ };
+ if (ignore) {
+ update.$addToSet = { ignored };
+ } else {
+ update.$pull = { ignored };
+ }
+
+ return this.update(query, update);
+ }
+
+ setAlertForRoomIdExcludingUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $ne: userId,
+ },
+ alert: { $ne: true },
+ };
+
+ const update = {
+ $set: {
+ alert: true,
+ },
+ };
+ return this.update(query, update, { multi: true });
+ }
+
+ setOpenForRoomIdExcludingUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': {
+ $ne: userId,
+ },
+ open: { $ne: true },
+ };
+
+ const update = {
+ $set: {
+ open: true,
+ },
+ };
+ return this.update(query, update, { multi: true });
+ }
+
+ setBlockedByRoomId(rid, blocked, blocker) {
+ const query = {
+ rid,
+ 'u._id': blocked,
+ };
+
+ const update = {
+ $set: {
+ blocked: true,
+ },
+ };
+
+ const query2 = {
+ rid,
+ 'u._id': blocker,
+ };
+
+ const update2 = {
+ $set: {
+ blocker: true,
+ },
+ };
+
+ return this.update(query, update) && this.update(query2, update2);
+ }
+
+ unsetBlockedByRoomId(rid, blocked, blocker) {
+ const query = {
+ rid,
+ 'u._id': blocked,
+ };
+
+ const update = {
+ $unset: {
+ blocked: 1,
+ },
+ };
+
+ const query2 = {
+ rid,
+ 'u._id': blocker,
+ };
+
+ const update2 = {
+ $unset: {
+ blocker: 1,
+ },
+ };
+
+ return this.update(query, update) && this.update(query2, update2);
+ }
+
+ updateCustomFieldsByRoomId(rid, cfields) {
+ const query = { rid };
+ const customFields = cfields || {};
+ const update = {
+ $set: {
+ customFields,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateTypeByRoomId(roomId, type) {
+ const query =
+ { rid: roomId };
+
+ const update = {
+ $set: {
+ t: type,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ addRoleById(_id, role) {
+ const query =
+ { _id };
+
+ const update = {
+ $addToSet: {
+ roles: role,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ removeRoleById(_id, role) {
+ const query =
+ { _id };
+
+ const update = {
+ $pull: {
+ roles: role,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setArchivedByUsername(username, archived) {
+ const query = {
+ t: 'd',
+ name: username,
+ };
+
+ const update = {
+ $set: {
+ archived,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ clearDesktopNotificationUserPreferences(userId) {
+ const query = {
+ 'u._id': userId,
+ desktopPrefOrigin: 'user',
+ };
+
+ const update = {
+ $unset: {
+ desktopNotifications: 1,
+ desktopPrefOrigin: 1,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateDesktopNotificationUserPreferences(userId, desktopNotifications) {
+ const query = {
+ 'u._id': userId,
+ desktopPrefOrigin: {
+ $ne: 'subscription',
+ },
+ };
+
+ const update = {
+ $set: {
+ desktopNotifications,
+ desktopPrefOrigin: 'user',
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ clearMobileNotificationUserPreferences(userId) {
+ const query = {
+ 'u._id': userId,
+ mobilePrefOrigin: 'user',
+ };
+
+ const update = {
+ $unset: {
+ mobilePushNotifications: 1,
+ mobilePrefOrigin: 1,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateMobileNotificationUserPreferences(userId, mobilePushNotifications) {
+ const query = {
+ 'u._id': userId,
+ mobilePrefOrigin: {
+ $ne: 'subscription',
+ },
+ };
+
+ const update = {
+ $set: {
+ mobilePushNotifications,
+ mobilePrefOrigin: 'user',
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ clearEmailNotificationUserPreferences(userId) {
+ const query = {
+ 'u._id': userId,
+ emailPrefOrigin: 'user',
+ };
+
+ const update = {
+ $unset: {
+ emailNotifications: 1,
+ emailPrefOrigin: 1,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateEmailNotificationUserPreferences(userId, emailNotifications) {
+ const query = {
+ 'u._id': userId,
+ emailPrefOrigin: {
+ $ne: 'subscription',
+ },
+ };
+
+ const update = {
+ $set: {
+ emailNotifications,
+ emailPrefOrigin: 'user',
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateUserHighlights(userId, userHighlights) {
+ const query = {
+ 'u._id': userId,
+ };
+
+ const update = {
+ $set: {
+ userHighlights,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
+ updateDirectFNameByName(name, fname) {
+ const query = {
+ t: 'd',
+ name,
+ };
+
+ let update;
+ if (fname) {
+ update = {
+ $set: {
+ fname,
+ },
+ };
+ } else {
+ update = {
+ $unset: {
+ fname: true,
+ },
+ };
+ }
+
+ return this.update(query, update, { multi: true });
+ }
+
+ // INSERT
+ createWithRoomAndUser(room, user, extraData) {
+ const subscription = {
+ open: false,
+ alert: false,
+ unread: 0,
+ userMentions: 0,
+ groupMentions: 0,
+ ts: room.ts,
+ rid: room._id,
+ name: room.name,
+ fname: room.fname,
+ customFields: room.customFields,
+ t: room.t,
+ u: {
+ _id: user._id,
+ username: user.username,
+ name: user.name,
+ },
+ ...getDefaultSubscriptionPref(user),
+ ...extraData,
+ };
+
+ if (room.prid) {
+ subscription.prid = room.prid;
+ }
+
+ const result = this.insert(subscription);
+
+ Rooms.incUsersCountById(room._id);
+
+ return result;
+ }
+
+
+ // REMOVE
+ removeByUserId(userId) {
+ const query = {
+ 'u._id': userId,
+ };
+
+ const roomIds = this.findByUserId(userId).map((s) => s.rid);
+
+ const result = this.remove(query);
+
+ if (Match.test(result, Number) && result > 0) {
+ Rooms.incUsersCountByIds(roomIds, -1);
+ }
+
+ return result;
+ }
+
+ removeByRoomId(roomId) {
+ const query = {
+ rid: roomId,
+ };
+
+ const result = this.remove(query);
+
+ if (Match.test(result, Number) && result > 0) {
+ Rooms.incUsersCountById(roomId, - result);
+ }
+
+ return result;
+ }
+
+ removeByRoomIdAndUserId(roomId, userId) {
+ const query = {
+ rid: roomId,
+ 'u._id': userId,
+ };
+
+ const result = this.remove(query);
+
+ if (Match.test(result, Number) && result > 0) {
+ Rooms.incUsersCountById(roomId, - result);
+ }
+
+ return result;
+ }
+
+ // //////////////////////////////////////////////////////////////////
+ // threads
+
+ addUnreadThreadByRoomIdAndUserIds(rid, users, tmid) {
+ if (!users) {
+ return;
+ }
+ return this.update({
+ 'u._id': { $in: users },
+ rid,
+ }, {
+ $addToSet: {
+ tunread: tmid,
+ },
+ }, { multi: true });
+ }
+
+ removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid) {
+ return this.update({
+ 'u._id': userId,
+ rid,
+ }, {
+ $pull: {
+ tunread: tmid,
+ },
+ });
+ }
+
+ removeAllUnreadThreadsByRoomIdAndUserId(rid, userId) {
+ const query = {
+ rid,
+ 'u._id': userId,
+ };
+
+ const update = {
+ $unset: {
+ tunread: 1,
+ },
+ };
+
+ return this.update(query, update);
+ }
+}
+
+export default new Subscriptions('subscription', true);
diff --git a/packages/rocketchat-models/server/models/Uploads.js b/app/models/server/models/Uploads.js
similarity index 100%
rename from packages/rocketchat-models/server/models/Uploads.js
rename to app/models/server/models/Uploads.js
diff --git a/packages/rocketchat-models/server/models/UserDataFiles.js b/app/models/server/models/UserDataFiles.js
similarity index 100%
rename from packages/rocketchat-models/server/models/UserDataFiles.js
rename to app/models/server/models/UserDataFiles.js
diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js
new file mode 100644
index 000000000000..14c86ee35bbe
--- /dev/null
+++ b/app/models/server/models/Users.js
@@ -0,0 +1,1074 @@
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+import { Base } from './_Base';
+import Subscriptions from './Subscriptions';
+import { settings } from '../../../settings/server/functions/settings';
+import _ from 'underscore';
+import s from 'underscore.string';
+
+export class Users extends Base {
+ constructor(...args) {
+ super(...args);
+
+ this.tryEnsureIndex({ roles: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ name: 1 });
+ this.tryEnsureIndex({ lastLogin: 1 });
+ this.tryEnsureIndex({ status: 1 });
+ this.tryEnsureIndex({ active: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ statusConnection: 1 }, { sparse: 1 });
+ this.tryEnsureIndex({ type: 1 });
+ this.tryEnsureIndex({ 'visitorEmails.address': 1 });
+ this.tryEnsureIndex({ federation: 1 }, { sparse: true });
+ }
+
+ getLoginTokensByUserId(userId) {
+ const query = {
+ 'services.resume.loginTokens.type': {
+ $exists: true,
+ $eq: 'personalAccessToken',
+ },
+ _id: userId,
+ };
+
+ return this.find(query, { fields: { 'services.resume.loginTokens': 1 } });
+ }
+
+ addPersonalAccessTokenToUser({ userId, loginTokenObject }) {
+ return this.update(userId, {
+ $push: {
+ 'services.resume.loginTokens': loginTokenObject,
+ },
+ });
+ }
+
+ removePersonalAccessTokenOfUser({ userId, loginTokenObject }) {
+ return this.update(userId, {
+ $pull: {
+ 'services.resume.loginTokens': loginTokenObject,
+ },
+ });
+ }
+
+ findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }) {
+ const query = {
+ 'services.resume.loginTokens': {
+ $elemMatch: { name: tokenName, type: 'personalAccessToken' },
+ },
+ _id: userId,
+ };
+
+ return this.findOne(query);
+ }
+
+ setOperator(_id, operator) {
+ const update = {
+ $set: {
+ operator,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ findOnlineAgents() {
+ const query = {
+ status: {
+ $exists: true,
+ $ne: 'offline',
+ },
+ statusLivechat: 'available',
+ roles: 'livechat-agent',
+ };
+
+ return this.find(query);
+ }
+
+ findOneOnlineAgentByUsername(username) {
+ const query = {
+ username,
+ status: {
+ $exists: true,
+ $ne: 'offline',
+ },
+ statusLivechat: 'available',
+ roles: 'livechat-agent',
+ };
+
+ return this.findOne(query);
+ }
+
+ findOneOnlineAgentById(_id) {
+ const query = {
+ _id,
+ status: {
+ $exists: true,
+ $ne: 'offline',
+ },
+ statusLivechat: 'available',
+ roles: 'livechat-agent',
+ };
+
+ return this.findOne(query);
+ }
+
+ findAgents() {
+ const query = {
+ roles: 'livechat-agent',
+ };
+
+ return this.find(query);
+ }
+
+ findOnlineUserFromList(userList) {
+ const query = {
+ status: {
+ $exists: true,
+ $ne: 'offline',
+ },
+ statusLivechat: 'available',
+ roles: 'livechat-agent',
+ username: {
+ $in: [].concat(userList),
+ },
+ };
+
+ return this.find(query);
+ }
+
+ getNextAgent() {
+ const query = {
+ status: {
+ $exists: true,
+ $ne: 'offline',
+ },
+ statusLivechat: 'available',
+ roles: 'livechat-agent',
+ };
+
+ const collectionObj = this.model.rawCollection();
+ const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);
+
+ const sort = {
+ livechatCount: 1,
+ username: 1,
+ };
+
+ const update = {
+ $inc: {
+ livechatCount: 1,
+ },
+ };
+
+ const user = findAndModify(query, sort, update);
+ if (user && user.value) {
+ return {
+ agentId: user.value._id,
+ username: user.value.username,
+ };
+ } else {
+ return null;
+ }
+ }
+
+ setLivechatStatus(userId, status) {
+ const query = {
+ _id: userId,
+ };
+
+ const update = {
+ $set: {
+ statusLivechat: status,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ closeOffice() {
+ this.findAgents().forEach((agent) => this.setLivechatStatus(agent._id, 'not-available'));
+ }
+
+ openOffice() {
+ this.findAgents().forEach((agent) => this.setLivechatStatus(agent._id, 'available'));
+ }
+
+ getAgentInfo(agentId) {
+ const query = {
+ _id: agentId,
+ };
+
+ const options = {
+ fields: {
+ name: 1,
+ username: 1,
+ phone: 1,
+ customFields: 1,
+ status: 1,
+ },
+ };
+
+ if (settings.get('Livechat_show_agent_email')) {
+ options.fields.emails = 1;
+ }
+
+ return this.findOne(query, options);
+ }
+
+ setTokenpassTcaBalances(_id, tcaBalances) {
+ const update = {
+ $set: {
+ 'services.tokenpass.tcaBalances': tcaBalances,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ getTokenBalancesByUserId(userId) {
+ const query = {
+ _id: userId,
+ };
+
+ const options = {
+ fields: {
+ 'services.tokenpass.tcaBalances': 1,
+ },
+ };
+
+ return this.findOne(query, options);
+ }
+
+ roleBaseQuery(userId) {
+ return { _id: userId };
+ }
+
+ setE2EPublicAndPivateKeysByUserId(userId, { public_key, private_key }) {
+ this.update({ _id: userId }, {
+ $set: {
+ 'e2e.public_key': public_key,
+ 'e2e.private_key': private_key,
+ },
+ });
+ }
+
+ rocketMailUnsubscribe(_id, createdAt) {
+ const query = {
+ _id,
+ createdAt: new Date(parseInt(createdAt)),
+ };
+ const update = {
+ $set: {
+ 'mailer.unsubscribed': true,
+ },
+ };
+ const affectedRows = this.update(query, update);
+ console.log('[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt(createdAt)), affectedRows);
+ return affectedRows;
+ }
+
+ fetchKeysByUserId(userId) {
+ const user = this.findOne({ _id: userId }, { fields: { e2e: 1 } });
+
+ if (!user || !user.e2e || !user.e2e.public_key) {
+ return {};
+ }
+
+ return {
+ public_key: user.e2e.public_key,
+ private_key: user.e2e.private_key,
+ };
+ }
+
+ disable2FAAndSetTempSecretByUserId(userId, tempToken) {
+ return this.update({
+ _id: userId,
+ }, {
+ $set: {
+ 'services.totp': {
+ enabled: false,
+ tempSecret: tempToken,
+ },
+ },
+ });
+ }
+
+ enable2FAAndSetSecretAndCodesByUserId(userId, secret, backupCodes) {
+ return this.update({
+ _id: userId,
+ }, {
+ $set: {
+ 'services.totp.enabled': true,
+ 'services.totp.secret': secret,
+ 'services.totp.hashedBackup': backupCodes,
+ },
+ $unset: {
+ 'services.totp.tempSecret': 1,
+ },
+ });
+ }
+
+ disable2FAByUserId(userId) {
+ return this.update({
+ _id: userId,
+ }, {
+ $set: {
+ 'services.totp': {
+ enabled: false,
+ },
+ },
+ });
+ }
+
+ update2FABackupCodesByUserId(userId, backupCodes) {
+ return this.update({
+ _id: userId,
+ }, {
+ $set: {
+ 'services.totp.hashedBackup': backupCodes,
+ },
+ });
+ }
+
+ findByIdsWithPublicE2EKey(ids, options) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ 'e2e.public_key': {
+ $exists: 1,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ resetE2EKey(userId) {
+ this.update({ _id: userId }, {
+ $unset: {
+ e2e: '',
+ },
+ });
+ }
+
+ findUsersInRoles(roles, scope, options) {
+ roles = [].concat(roles);
+
+ const query = {
+ roles: { $in: roles },
+ };
+
+ return this.find(query, options);
+ }
+
+ findOneByImportId(_id, options) {
+ return this.findOne({ importIds: _id }, options);
+ }
+
+ findOneByUsername(username, options) {
+ if (typeof username === 'string') {
+ username = new RegExp(`^${ username }$`, 'i');
+ }
+
+ const query = { username };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByEmailAddress(emailAddress, options) {
+ const query = { 'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i') };
+
+ return this.findOne(query, options);
+ }
+
+ findOneAdmin(admin, options) {
+ const query = { admin };
+
+ return this.findOne(query, options);
+ }
+
+ findOneByIdAndLoginToken(_id, token, options) {
+ const query = {
+ _id,
+ 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token),
+ };
+
+ return this.findOne(query, options);
+ }
+
+ findOneById(userId, options) {
+ const query = { _id: userId };
+
+ return this.findOne(query, options);
+ }
+
+ // FIND
+ findById(userId) {
+ const query = { _id: userId };
+
+ return this.find(query);
+ }
+
+ findByIds(users, options) {
+ const query = { _id: { $in: users } };
+ return this.find(query, options);
+ }
+
+ findUsersNotOffline(options) {
+ const query = {
+ username: {
+ $exists: 1,
+ },
+ status: {
+ $in: ['online', 'away', 'busy'],
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByRoomId(rid, options) {
+ const data = Subscriptions.findByRoomId(rid).fetch().map((item) => item.u._id);
+ const query = {
+ _id: {
+ $in: data,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByUsername(username, options) {
+ const query = { username };
+
+ return this.find(query, options);
+ }
+
+ findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) {
+ if (exceptions == null) { exceptions = []; }
+ if (options == null) { options = {}; }
+ if (!_.isArray(exceptions)) {
+ exceptions = [exceptions];
+ }
+
+ const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i');
+ const query = {
+ $or: [{
+ username: termRegex,
+ }, {
+ name: termRegex,
+ }],
+ active: true,
+ type: {
+ $in: ['user', 'bot'],
+ },
+ $and: [{
+ username: {
+ $exists: true,
+ },
+ }, {
+ username: {
+ $nin: exceptions,
+ },
+ }],
+ };
+
+ return this.find(query, options);
+ }
+
+ findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery = []) {
+ if (exceptions == null) { exceptions = []; }
+ if (options == null) { options = {}; }
+ if (!_.isArray(exceptions)) {
+ exceptions = [exceptions];
+ }
+
+ const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i');
+
+ const searchFields = forcedSearchFields || settings.get('Accounts_SearchFields').trim().split(',');
+
+ const orStmt = _.reduce(searchFields, function(acc, el) {
+ acc.push({ [el.trim()]: termRegex });
+ return acc;
+ }, []);
+ const query = {
+ $and: [
+ {
+ active: true,
+ $or: orStmt,
+ },
+ {
+ username: { $exists: true, $nin: exceptions },
+ },
+ ...extraQuery,
+ ],
+ };
+
+ // do not use cache
+ return this._db.find(query, options);
+ }
+
+ findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) {
+ const extraQuery = [
+ {
+ $or: [
+ { federation: { $exists: false } },
+ { 'federation.peer': localPeer },
+ ],
+ },
+ ];
+ return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery);
+ }
+
+ findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) {
+ const extraQuery = [
+ { federation: { $exists: true } },
+ { 'federation.peer': { $ne: localPeer } },
+ ];
+ return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery);
+ }
+
+ findUsersByNameOrUsername(nameOrUsername, options) {
+ const query = {
+ username: {
+ $exists: 1,
+ },
+
+ $or: [
+ { name: nameOrUsername },
+ { username: nameOrUsername },
+ ],
+
+ type: {
+ $in: ['user'],
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) {
+ const query = {
+ $or: [
+ { name: usernameNameOrEmailAddress },
+ { username: usernameNameOrEmailAddress },
+ { 'emails.address': usernameNameOrEmailAddress },
+ ],
+ type: {
+ $in: ['user', 'bot'],
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findLDAPUsers(options) {
+ const query = { ldap: true };
+
+ return this.find(query, options);
+ }
+
+ findCrowdUsers(options) {
+ const query = { crowd: true };
+
+ return this.find(query, options);
+ }
+
+ getLastLogin(options) {
+ if (options == null) { options = {}; }
+ const query = { lastLogin: { $exists: 1 } };
+ options.sort = { lastLogin: -1 };
+ options.limit = 1;
+ const [user] = this.find(query, options).fetch();
+ return user && user.lastLogin;
+ }
+
+ findUsersByUsernames(usernames, options) {
+ const query = {
+ username: {
+ $in: usernames,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findUsersByIds(ids, options) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ };
+ return this.find(query, options);
+ }
+
+ findUsersWithUsernameByIds(ids, options) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ username: {
+ $exists: 1,
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ findUsersWithUsernameByIdsNotOffline(ids, options) {
+ const query = {
+ _id: {
+ $in: ids,
+ },
+ username: {
+ $exists: 1,
+ },
+ status: {
+ $in: ['online', 'away', 'busy'],
+ },
+ };
+
+ return this.find(query, options);
+ }
+
+ getOldest(fields = { _id: 1 }) {
+ const query = {
+ _id: {
+ $ne: 'rocket.cat',
+ },
+ };
+
+ const options = {
+ fields,
+ sort: {
+ createdAt: 1,
+ },
+ };
+
+ return this.findOne(query, options);
+ }
+
+ // UPDATE
+ addImportIds(_id, importIds) {
+ importIds = [].concat(importIds);
+
+ const query = { _id };
+
+ const update = {
+ $addToSet: {
+ importIds: {
+ $each: importIds,
+ },
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ updateLastLoginById(_id) {
+ const update = {
+ $set: {
+ lastLogin: new Date,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setServiceId(_id, serviceName, serviceId) {
+ const update =
+ { $set: {} };
+
+ const serviceIdKey = `services.${ serviceName }.id`;
+ update.$set[serviceIdKey] = serviceId;
+
+ return this.update(_id, update);
+ }
+
+ setUsername(_id, username) {
+ const update =
+ { $set: { username } };
+
+ return this.update(_id, update);
+ }
+
+ setEmail(_id, email) {
+ const update = {
+ $set: {
+ emails: [{
+ address: email,
+ verified: false,
+ },
+ ],
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setEmailVerified(_id, email) {
+ const query = {
+ _id,
+ emails: {
+ $elemMatch: {
+ address: email,
+ verified: false,
+ },
+ },
+ };
+
+ const update = {
+ $set: {
+ 'emails.$.verified': true,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setName(_id, name) {
+ const update = {
+ $set: {
+ name,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ unsetName(_id) {
+ const update = {
+ $unset: {
+ name,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setCustomFields(_id, fields) {
+ const values = {};
+ Object.keys(fields).forEach((key) => {
+ values[`customFields.${ key }`] = fields[key];
+ });
+
+ const update = { $set: values };
+
+ return this.update(_id, update);
+ }
+
+ setAvatarOrigin(_id, origin) {
+ const update = {
+ $set: {
+ avatarOrigin: origin,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ unsetAvatarOrigin(_id) {
+ const update = {
+ $unset: {
+ avatarOrigin: 1,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setUserActive(_id, active) {
+ if (active == null) { active = true; }
+ const update = {
+ $set: {
+ active,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setAllUsersActive(active) {
+ const update = {
+ $set: {
+ active,
+ },
+ };
+
+ return this.update({}, update, { multi: true });
+ }
+
+ unsetLoginTokens(_id) {
+ const update = {
+ $set: {
+ 'services.resume.loginTokens' : [],
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ unsetRequirePasswordChange(_id) {
+ const update = {
+ $unset: {
+ requirePasswordChange : true,
+ requirePasswordChangeReason : true,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) {
+ const update = {
+ $unset: {
+ 'services.password': 1,
+ },
+ $set: {
+ requirePasswordChange,
+ requirePasswordChangeReason,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setLanguage(_id, language) {
+ const update = {
+ $set: {
+ language,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setProfile(_id, profile) {
+ const update = {
+ $set: {
+ 'settings.profile': profile,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ clearSettings(_id) {
+ const update = {
+ $set: {
+ settings: {},
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ setPreferences(_id, preferences) {
+ const settingsObject = Object.assign(
+ {},
+ ...Object.keys(preferences).map((key) => ({ [`settings.preferences.${ key }`]: preferences[key] }))
+ );
+
+ const update = {
+ $set: settingsObject,
+ };
+ if (parseInt(preferences.clockMode) === 0) {
+ delete update.$set['settings.preferences.clockMode'];
+ update.$unset = { 'settings.preferences.clockMode': 1 };
+ }
+
+ return this.update(_id, update);
+ }
+
+ setUtcOffset(_id, utcOffset) {
+ const query = {
+ _id,
+ utcOffset: {
+ $ne: utcOffset,
+ },
+ };
+
+ const update = {
+ $set: {
+ utcOffset,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ saveUserById(_id, data) {
+ const setData = {};
+ const unsetData = {};
+
+ if (data.name != null) {
+ if (!_.isEmpty(s.trim(data.name))) {
+ setData.name = s.trim(data.name);
+ } else {
+ unsetData.name = 1;
+ }
+ }
+
+ if (data.email != null) {
+ if (!_.isEmpty(s.trim(data.email))) {
+ setData.emails = [{ address: s.trim(data.email) }];
+ } else {
+ unsetData.emails = 1;
+ }
+ }
+
+ if (data.phone != null) {
+ if (!_.isEmpty(s.trim(data.phone))) {
+ setData.phone = [{ phoneNumber: s.trim(data.phone) }];
+ } else {
+ unsetData.phone = 1;
+ }
+ }
+
+ const update = {};
+
+ if (!_.isEmpty(setData)) {
+ update.$set = setData;
+ }
+
+ if (!_.isEmpty(unsetData)) {
+ update.$unset = unsetData;
+ }
+
+ if (_.isEmpty(update)) {
+ return true;
+ }
+
+ return this.update({ _id }, update);
+ }
+
+ setReason(_id, reason) {
+ const update = {
+ $set: {
+ reason,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ unsetReason(_id) {
+ const update = {
+ $unset: {
+ reason: true,
+ },
+ };
+
+ return this.update(_id, update);
+ }
+
+ bannerExistsById(_id, bannerId) {
+ const query = {
+ _id,
+ [`banners.${ bannerId }`]: {
+ $exists: true,
+ },
+ };
+
+ return this.find(query).count() !== 0;
+ }
+
+ addBannerById(_id, banner) {
+ const query = {
+ _id,
+ [`banners.${ banner.id }.read`]: {
+ $ne: true,
+ },
+ };
+
+ const update = {
+ $set: {
+ [`banners.${ banner.id }`]: banner,
+ },
+ };
+
+ return this.update(query, update);
+ }
+
+ setBannerReadById(_id, bannerId) {
+ const update = {
+ $set: {
+ [`banners.${ bannerId }.read`]: true,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ removeBannerById(_id, banner) {
+ const update = {
+ $unset: {
+ [`banners.${ banner.id }`]: true,
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ removeResumeService(_id) {
+ const update = {
+ $unset: {
+ 'services.resume': '',
+ },
+ };
+
+ return this.update({ _id }, update);
+ }
+
+ // INSERT
+ create(data) {
+ const user = {
+ createdAt: new Date,
+ avatarOrigin: 'none',
+ };
+
+ _.extend(user, data);
+
+ return this.insert(user);
+ }
+
+
+ // REMOVE
+ removeById(_id) {
+ return this.remove(_id);
+ }
+
+ /*
+Find users to send a message by email if:
+- he is not online
+- has a verified email
+- has not disabled email notifications
+- `active` is equal to true (false means they were deactivated and can't login)
+*/
+ getUsersToSendOfflineEmail(usersIds) {
+ const query = {
+ _id: {
+ $in: usersIds,
+ },
+ active: true,
+ status: 'offline',
+ statusConnection: {
+ $ne: 'online',
+ },
+ 'emails.verified': true,
+ };
+
+ const options = {
+ fields: {
+ name: 1,
+ username: 1,
+ emails: 1,
+ 'settings.preferences.emailNotificationMode': 1,
+ language: 1,
+ },
+ };
+
+ return this.find(query, options);
+ }
+}
+
+export default new Users(Meteor.users, true);
diff --git a/app/models/server/models/WebdavAccounts.js b/app/models/server/models/WebdavAccounts.js
new file mode 100644
index 000000000000..f1ce436cdd58
--- /dev/null
+++ b/app/models/server/models/WebdavAccounts.js
@@ -0,0 +1,28 @@
+/**
+ * Webdav Accounts model
+ */
+import { Base } from './_Base';
+
+export class WebdavAccounts extends Base {
+ constructor() {
+ super('webdav_accounts');
+
+ this.tryEnsureIndex({ user_id: 1, server_url: 1, username: 1, name: 1 }, { unique: 1 });
+ }
+
+ findWithUserId(user_id, options) {
+ const query = { user_id };
+ return this.find(query, options);
+ }
+
+ removeByUserAndId(_id, user_id) {
+ return this.remove({ _id, user_id });
+ }
+
+ removeById(_id) {
+ return this.remove({ _id });
+ }
+
+}
+
+export default new WebdavAccounts();
diff --git a/packages/rocketchat-models/server/models/_Base.js b/app/models/server/models/_Base.js
similarity index 100%
rename from packages/rocketchat-models/server/models/_Base.js
rename to app/models/server/models/_Base.js
diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js
new file mode 100644
index 000000000000..c5c79c8fbc5a
--- /dev/null
+++ b/app/models/server/models/_BaseDb.js
@@ -0,0 +1,307 @@
+import { Match } from 'meteor/check';
+import { Mongo, MongoInternals } from 'meteor/mongo';
+import _ from 'underscore';
+import { EventEmitter } from 'events';
+
+const baseName = 'rocketchat_';
+
+const trash = new Mongo.Collection(`${ baseName }_trash`);
+try {
+ trash._ensureIndex({ collection: 1 });
+ trash._ensureIndex({ _deletedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 30 });
+} catch (e) {
+ console.log(e);
+}
+
+const isOplogEnabled = MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle && !!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry;
+
+export class BaseDb extends EventEmitter {
+ constructor(model, baseModel) {
+ super();
+
+ if (Match.test(model, String)) {
+ this.name = model;
+ this.collectionName = this.baseName + this.name;
+ this.model = new Mongo.Collection(this.collectionName);
+ } else {
+ this.name = model._name;
+ this.collectionName = this.name;
+ this.model = model;
+ }
+
+ this.baseModel = baseModel;
+
+ this.wrapModel();
+
+ let alreadyListeningToOplog = false;
+ // When someone start listening for changes we start oplog if available
+ this.on('newListener', (event/* , listener*/) => {
+ if (event === 'change' && alreadyListeningToOplog === false) {
+ alreadyListeningToOplog = true;
+ if (isOplogEnabled) {
+ const query = {
+ collection: this.collectionName,
+ };
+
+ MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry(query, this.processOplogRecord.bind(this));
+ // Meteor will handle if we have a value https://github.com/meteor/meteor/blob/5dcd0b2eb9c8bf881ffbee98bc4cb7631772c4da/packages/mongo/oplog_tailing.js#L5
+ if (process.env.METEOR_OPLOG_TOO_FAR_BEHIND == null) {
+ MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle._defineTooFarBehind(Number.MAX_SAFE_INTEGER);
+ }
+ }
+ }
+ });
+
+ this.tryEnsureIndex({ _updatedAt: 1 });
+ }
+
+ get baseName() {
+ return baseName;
+ }
+
+ setUpdatedAt(record = {}) {
+
+ // TODO: Check if this can be deleted, Rodrigo does not rememebr WHY he added it. So he removed it to fix issue #5541
+ // setUpdatedAt(record = {}, checkQuery = false, query) {
+ // if (checkQuery === true) {
+ // if (!query || Object.keys(query).length === 0) {
+ // throw new Meteor.Error('Models._Base: Empty query');
+ // }
+ // }
+
+ if (/(^|,)\$/.test(Object.keys(record).join(','))) {
+ record.$set = record.$set || {};
+ record.$set._updatedAt = new Date;
+ } else {
+ record._updatedAt = new Date;
+ }
+
+ return record;
+ }
+
+ wrapModel() {
+ this.originals = {
+ insert: this.model.insert.bind(this.model),
+ update: this.model.update.bind(this.model),
+ remove: this.model.remove.bind(this.model),
+ };
+ const self = this;
+
+ this.model.insert = function(...args) {
+ return self.insert(...args);
+ };
+
+ this.model.update = function(...args) {
+ return self.update(...args);
+ };
+
+ this.model.remove = function(...args) {
+ return self.remove(...args);
+ };
+ }
+
+ _doNotMixInclusionAndExclusionFields(options) {
+ if (options && options.fields) {
+ const keys = Object.keys(options.fields);
+ const removeKeys = keys.filter((key) => options.fields[key] === 0);
+ if (keys.length > removeKeys.length) {
+ removeKeys.forEach((key) => delete options.fields[key]);
+ }
+ }
+ }
+
+ find(...args) {
+ this._doNotMixInclusionAndExclusionFields(args[1]);
+ return this.model.find(...args);
+ }
+
+ findOne(...args) {
+ this._doNotMixInclusionAndExclusionFields(args[1]);
+ return this.model.findOne(...args);
+ }
+
+ findOneById(_id, options) {
+ return this.findOne({ _id }, options);
+ }
+
+ findOneByIds(ids, options) {
+ return this.findOne({ _id: { $in: ids } }, options);
+ }
+
+ updateHasPositionalOperator(update) {
+ return Object.keys(update).some((key) => key.includes('.$') || (Match.test(update[key], Object) && this.updateHasPositionalOperator(update[key])));
+ }
+
+ processOplogRecord(action) {
+ if (action.op.op === 'i') {
+ this.emit('change', {
+ action: 'insert',
+ clientAction: 'inserted',
+ id: action.op.o._id,
+ data: action.op.o,
+ oplog: true,
+ });
+ return;
+ }
+
+ if (action.op.op === 'u') {
+ if (!action.op.o.$set && !action.op.o.$unset) {
+ this.emit('change', {
+ action: 'update',
+ clientAction: 'updated',
+ id: action.id,
+ data: action.op.o,
+ oplog: true,
+ });
+ return;
+ }
+
+ const diff = {};
+ if (action.op.o.$set) {
+ for (const key in action.op.o.$set) {
+ if (action.op.o.$set.hasOwnProperty(key)) {
+ diff[key] = action.op.o.$set[key];
+ }
+ }
+ }
+
+ if (action.op.o.$unset) {
+ for (const key in action.op.o.$unset) {
+ if (action.op.o.$unset.hasOwnProperty(key)) {
+ diff[key] = undefined;
+ }
+ }
+ }
+
+ this.emit('change', {
+ action: 'update',
+ clientAction: 'updated',
+ id: action.id,
+ diff,
+ oplog: true,
+ });
+ return;
+ }
+
+ if (action.op.op === 'd') {
+ this.emit('change', {
+ action: 'remove',
+ clientAction: 'removed',
+ id: action.id,
+ oplog: true,
+ });
+ return;
+ }
+ }
+
+ insert(record, ...args) {
+ this.setUpdatedAt(record);
+
+ const result = this.originals.insert(record, ...args);
+
+ record._id = result;
+
+ return result;
+ }
+
+ update(query, update, options = {}) {
+ this.setUpdatedAt(update, true, query);
+
+ return this.originals.update(query, update, options);
+ }
+
+ upsert(query, update, options = {}) {
+ options.upsert = true;
+ options._returnObject = true;
+ return this.update(query, update, options);
+ }
+
+ remove(query) {
+ const records = this.model.find(query).fetch();
+
+ const ids = [];
+ for (const record of records) {
+ ids.push(record._id);
+
+ record._deletedAt = new Date;
+ record.__collection__ = this.name;
+
+ trash.upsert({ _id: record._id }, _.omit(record, '_id'));
+ }
+
+ query = { _id: { $in: ids } };
+
+ return this.originals.remove(query);
+ }
+
+ insertOrUpsert(...args) {
+ if (args[0] && args[0]._id) {
+ const { _id } = args[0];
+ delete args[0]._id;
+ args.unshift({
+ _id,
+ });
+
+ this.upsert(...args);
+ return _id;
+ } else {
+ return this.insert(...args);
+ }
+ }
+
+ allow(...args) {
+ return this.model.allow(...args);
+ }
+
+ deny(...args) {
+ return this.model.deny(...args);
+ }
+
+ ensureIndex(...args) {
+ return this.model._ensureIndex(...args);
+ }
+
+ dropIndex(...args) {
+ return this.model._dropIndex(...args);
+ }
+
+ tryEnsureIndex(...args) {
+ try {
+ return this.ensureIndex(...args);
+ } catch (e) {
+ console.error('Error creating index:', this.name, '->', ...args, e);
+ }
+ }
+
+ tryDropIndex(...args) {
+ try {
+ return this.dropIndex(...args);
+ } catch (e) {
+ console.error('Error dropping index:', this.name, '->', ...args, e);
+ }
+ }
+
+ trashFind(query, options) {
+ query.__collection__ = this.name;
+
+ return trash.find(query, options);
+ }
+
+ trashFindOneById(_id, options) {
+ const query = {
+ _id,
+ __collection__: this.name,
+ };
+
+ return trash.findOne(query, options);
+ }
+
+ trashFindDeletedAfter(deletedAt, query = {}, options) {
+ query.__collection__ = this.name;
+ query._deletedAt = {
+ $gt: deletedAt,
+ };
+
+ return trash.find(query, options);
+ }
+}
diff --git a/app/models/server/models/apps-logs-model.js b/app/models/server/models/apps-logs-model.js
new file mode 100644
index 000000000000..a2888e3ba062
--- /dev/null
+++ b/app/models/server/models/apps-logs-model.js
@@ -0,0 +1,7 @@
+import { Base } from './_Base';
+
+export class AppsLogsModel extends Base {
+ constructor() {
+ super('apps_logs');
+ }
+}
diff --git a/app/models/server/models/apps-model.js b/app/models/server/models/apps-model.js
new file mode 100644
index 000000000000..086db3ccb7e6
--- /dev/null
+++ b/app/models/server/models/apps-model.js
@@ -0,0 +1,7 @@
+import { Base } from './_Base';
+
+export class AppsModel extends Base {
+ constructor() {
+ super('apps');
+ }
+}
diff --git a/app/models/server/models/apps-persistence-model.js b/app/models/server/models/apps-persistence-model.js
new file mode 100644
index 000000000000..bad009b7faeb
--- /dev/null
+++ b/app/models/server/models/apps-persistence-model.js
@@ -0,0 +1,7 @@
+import { Base } from './_Base';
+
+export class AppsPersistenceModel extends Base {
+ constructor() {
+ super('apps_persistence');
+ }
+}
diff --git a/packages/rocketchat-notifications/client/index.js b/app/notifications/client/index.js
similarity index 100%
rename from packages/rocketchat-notifications/client/index.js
rename to app/notifications/client/index.js
diff --git a/packages/rocketchat-notifications/client/lib/Notifications.js b/app/notifications/client/lib/Notifications.js
similarity index 100%
rename from packages/rocketchat-notifications/client/lib/Notifications.js
rename to app/notifications/client/lib/Notifications.js
diff --git a/app/notifications/index.js b/app/notifications/index.js
new file mode 100644
index 000000000000..a67eca871efb
--- /dev/null
+++ b/app/notifications/index.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+if (Meteor.isClient) {
+ module.exports = require('./client/index.js');
+}
+if (Meteor.isServer) {
+ module.exports = require('./server/index.js');
+}
diff --git a/packages/rocketchat-notifications/server/index.js b/app/notifications/server/index.js
similarity index 100%
rename from packages/rocketchat-notifications/server/index.js
rename to app/notifications/server/index.js
diff --git a/app/notifications/server/lib/Notifications.js b/app/notifications/server/lib/Notifications.js
new file mode 100644
index 000000000000..ab9ff5d8531c
--- /dev/null
+++ b/app/notifications/server/lib/Notifications.js
@@ -0,0 +1,206 @@
+import { Meteor } from 'meteor/meteor';
+import { DDPCommon } from 'meteor/ddp-common';
+import { Subscriptions, Rooms } from '../../../models';
+import { settings } from '../../../settings';
+
+const changedPayload = function(collection, id, fields) {
+ return DDPCommon.stringifyDDP({
+ msg: 'changed',
+ collection,
+ id,
+ fields,
+ });
+};
+const send = function(self, msg) {
+ if (!self.socket) {
+ return;
+ }
+ self.socket.send(msg);
+};
+class RoomStreamer extends Meteor.Streamer {
+ _publish(publication, eventName, options) {
+ super._publish(publication, eventName, options);
+ const uid = Meteor.userId();
+ if (/rooms-changed/.test(eventName)) {
+ const roomEvent = (...args) => send(publication._session, changedPayload(this.subscriptionName, 'id', {
+ eventName: `${ uid }/rooms-changed`,
+ args,
+ }));
+ const rooms = Subscriptions.find({ 'u._id': uid }, { fields: { rid: 1 } }).fetch();
+ rooms.forEach(({ rid }) => {
+ this.on(rid, roomEvent);
+ });
+
+ const userEvent = (clientAction, { rid }) => {
+ switch (clientAction) {
+ case 'inserted':
+ rooms.push({ rid });
+ this.on(rid, roomEvent);
+ break;
+
+ case 'removed':
+ this.removeListener(rid, roomEvent);
+ break;
+ }
+ };
+ this.on(uid, userEvent);
+
+ publication.onStop(() => {
+ this.removeListener(uid, userEvent);
+ rooms.forEach(({ rid }) => this.removeListener(rid, roomEvent));
+ });
+ }
+ }
+}
+
+class Notifications {
+ constructor() {
+ const self = this;
+ this.debug = false;
+ this.notifyUser = this.notifyUser.bind(this);
+ this.streamAll = new Meteor.Streamer('notify-all');
+ this.streamLogged = new Meteor.Streamer('notify-logged');
+ this.streamRoom = new Meteor.Streamer('notify-room');
+ this.streamRoomUsers = new Meteor.Streamer('notify-room-users');
+ this.streamUser = new RoomStreamer('notify-user');
+ this.streamAll.allowWrite('none');
+ this.streamLogged.allowWrite('none');
+ this.streamRoom.allowWrite('none');
+ this.streamRoomUsers.allowWrite(function(eventName, ...args) {
+ const [roomId, e] = eventName.split('/');
+ // const user = Meteor.users.findOne(this.userId, {
+ // fields: {
+ // username: 1
+ // }
+ // });
+ if (Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) {
+ const subscriptions = Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch();
+ subscriptions.forEach((subscription) => self.notifyUser(subscription.u._id, e, ...args));
+ }
+ return false;
+ });
+ this.streamUser.allowWrite('logged');
+ this.streamAll.allowRead('all');
+ this.streamLogged.allowRead('logged');
+ this.streamRoom.allowRead(function(eventName, extraData) {
+ const [roomId] = eventName.split('/');
+ const room = Rooms.findOneById(roomId);
+ if (!room) {
+ console.warn(`Invalid streamRoom eventName: "${ eventName }"`);
+ return false;
+ }
+ if (room.t === 'l' && extraData && extraData.token && room.v.token === extraData.token) {
+ return true;
+ }
+ if (this.userId == null) {
+ return false;
+ }
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { fields: { _id: 1 } });
+ return subscription != null;
+ });
+ this.streamRoomUsers.allowRead('none');
+ this.streamUser.allowRead(function(eventName) {
+ const [userId] = eventName.split('/');
+ return (this.userId != null) && this.userId === userId;
+ });
+ }
+
+ notifyAll(eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyAll', [eventName, ...args]);
+ }
+ args.unshift(eventName);
+ return this.streamAll.emit.apply(this.streamAll, args);
+ }
+
+ notifyLogged(eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyLogged', [eventName, ...args]);
+ }
+ args.unshift(eventName);
+ return this.streamLogged.emit.apply(this.streamLogged, args);
+ }
+
+ notifyRoom(room, eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyRoom', [room, eventName, ...args]);
+ }
+ args.unshift(`${ room }/${ eventName }`);
+ return this.streamRoom.emit.apply(this.streamRoom, args);
+ }
+
+ notifyUser(userId, eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyUser', [userId, eventName, ...args]);
+ }
+ args.unshift(`${ userId }/${ eventName }`);
+ return this.streamUser.emit.apply(this.streamUser, args);
+ }
+
+ notifyAllInThisInstance(eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyAll', [eventName, ...args]);
+ }
+ args.unshift(eventName);
+ return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args);
+ }
+
+ notifyLoggedInThisInstance(eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyLogged', [eventName, ...args]);
+ }
+ args.unshift(eventName);
+ return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args);
+ }
+
+ notifyRoomInThisInstance(room, eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyRoomAndBroadcast', [room, eventName, ...args]);
+ }
+ args.unshift(`${ room }/${ eventName }`);
+ return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args);
+ }
+
+ notifyUserInThisInstance(userId, eventName, ...args) {
+ if (this.debug === true) {
+ console.log('notifyUserAndBroadcast', [userId, eventName, ...args]);
+ }
+ args.unshift(`${ userId }/${ eventName }`);
+ return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args);
+ }
+}
+
+const notifications = new Notifications();
+
+notifications.streamRoom.allowWrite(function(eventName, username, typing, extraData) {
+ const [roomId, e] = eventName.split('/');
+
+ if (e === 'webrtc') {
+ return true;
+ }
+ if (e === 'typing') {
+ const key = settings.get('UI_Use_Real_Name') ? 'name' : 'username';
+ // typing from livechat widget
+ if (extraData && extraData.token) {
+ const room = Rooms.findOneById(roomId);
+ if (room && room.t === 'l' && room.v.token === extraData.token) {
+ return true;
+ }
+ }
+
+ const user = Meteor.users.findOne(this.userId, {
+ fields: {
+ [key]: 1,
+ },
+ });
+
+ if (!user) {
+ return false;
+ }
+
+ return user[key] === username;
+ }
+ return false;
+});
+
+export default notifications;
diff --git a/packages/rocketchat-nrr/README.md b/app/nrr/README.md
similarity index 100%
rename from packages/rocketchat-nrr/README.md
rename to app/nrr/README.md
diff --git a/app/nrr/client/index.js b/app/nrr/client/index.js
new file mode 100644
index 000000000000..613d0630705e
--- /dev/null
+++ b/app/nrr/client/index.js
@@ -0,0 +1 @@
+import './nrr';
diff --git a/packages/rocketchat-nrr/nrr.js b/app/nrr/client/nrr.js
similarity index 79%
rename from packages/rocketchat-nrr/nrr.js
rename to app/nrr/client/nrr.js
index 8376a6425869..00c1cdba2d95 100644
--- a/packages/rocketchat-nrr/nrr.js
+++ b/app/nrr/client/nrr.js
@@ -7,27 +7,24 @@ import { HTML } from 'meteor/htmljs';
import { Spacebars } from 'meteor/spacebars';
import { Tracker } from 'meteor/tracker';
+const makeCursorReactive = function(obj) {
+ if (obj instanceof Meteor.Collection.Cursor) {
+ return obj._depend({
+ added: true,
+ removed: true,
+ changed: true,
+ });
+ }
+};
+
Blaze.toHTMLWithDataNonReactive = function(content, data) {
- const makeCursorReactive = function(obj) {
- if (obj instanceof Meteor.Collection.Cursor) {
- return obj._depend({
- added: true,
- removed: true,
- changed: true,
- });
- }
- };
makeCursorReactive(data);
if (data instanceof Spacebars.kw && Object.keys(data.hash).length > 0) {
- Object.keys(data.hash).forEach((key) => {
- makeCursorReactive(data.hash[key]);
- });
-
- data = data.hash;
+ Object.entries(data.hash).forEach(([, value]) => makeCursorReactive(value));
+ return Tracker.nonreactive(() => Blaze.toHTMLWithData(content, data.hash));
}
-
return Tracker.nonreactive(() => Blaze.toHTMLWithData(content, data));
};
diff --git a/app/nrr/index.js b/app/nrr/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/nrr/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/packages/rocketchat-oauth2-server-config/.gitignore b/app/oauth2-server-config/.gitignore
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/.gitignore
rename to app/oauth2-server-config/.gitignore
diff --git a/packages/rocketchat-oauth2-server-config/client/admin/collection.js b/app/oauth2-server-config/client/admin/collection.js
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/client/admin/collection.js
rename to app/oauth2-server-config/client/admin/collection.js
diff --git a/app/oauth2-server-config/client/admin/route.js b/app/oauth2-server-config/client/admin/route.js
new file mode 100644
index 000000000000..229074dfd0af
--- /dev/null
+++ b/app/oauth2-server-config/client/admin/route.js
@@ -0,0 +1,25 @@
+import { FlowRouter } from 'meteor/kadira:flow-router' ;
+import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { t } from '../../../utils';
+
+FlowRouter.route('/admin/oauth-apps', {
+ name: 'admin-oauth-apps',
+ action() {
+ return BlazeLayout.render('main', {
+ center: 'oauthApps',
+ pageTitle: t('OAuth_Applications'),
+ });
+ },
+});
+
+FlowRouter.route('/admin/oauth-app/:id?', {
+ name: 'admin-oauth-app',
+ action(params) {
+ return BlazeLayout.render('main', {
+ center: 'pageSettingsContainer',
+ pageTitle: t('OAuth_Application'),
+ pageTemplate: 'oauthApp',
+ params,
+ });
+ },
+});
diff --git a/app/oauth2-server-config/client/admin/startup.js b/app/oauth2-server-config/client/admin/startup.js
new file mode 100644
index 000000000000..cf3e4fb08663
--- /dev/null
+++ b/app/oauth2-server-config/client/admin/startup.js
@@ -0,0 +1,11 @@
+import { AdminBox } from '../../../ui-utils';
+import { hasAllPermission } from '../../../authorization';
+
+AdminBox.addOption({
+ href: 'admin-oauth-apps',
+ i18nLabel: 'OAuth Apps',
+ icon: 'discover',
+ permissionGranted() {
+ return hasAllPermission('manage-oauth-apps');
+ },
+});
diff --git a/packages/rocketchat-oauth2-server-config/client/admin/views/oauthApp.html b/app/oauth2-server-config/client/admin/views/oauthApp.html
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/client/admin/views/oauthApp.html
rename to app/oauth2-server-config/client/admin/views/oauthApp.html
diff --git a/packages/rocketchat-oauth2-server-config/client/admin/views/oauthApp.js b/app/oauth2-server-config/client/admin/views/oauthApp.js
similarity index 87%
rename from packages/rocketchat-oauth2-server-config/client/admin/views/oauthApp.js
rename to app/oauth2-server-config/client/admin/views/oauthApp.js
index 706f6d73a50e..3b59a9bfcfa9 100644
--- a/packages/rocketchat-oauth2-server-config/client/admin/views/oauthApp.js
+++ b/app/oauth2-server-config/client/admin/views/oauthApp.js
@@ -3,9 +3,10 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat, handleError } from 'meteor/rocketchat:lib';
-import { modal } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { Tracker } from 'meteor/tracker';
+import { hasAllPermission } from '../../../../authorization';
+import { modal, SideNav } from '../../../../ui-utils/client';
+import { t, handleError } from '../../../../utils';
import { ChatOAuthApps } from '../collection';
import toastr from 'toastr';
@@ -18,7 +19,7 @@ Template.oauthApp.onCreated(function() {
Template.oauthApp.helpers({
hasPermission() {
- return RocketChat.authz.hasAllPermission('manage-oauth-apps');
+ return hasAllPermission('manage-oauth-apps');
},
data() {
const instance = Template.instance();
@@ -100,3 +101,11 @@ Template.oauthApp.events({
});
},
});
+
+Template.oauthApp.onRendered(() => {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+});
+
diff --git a/app/oauth2-server-config/client/admin/views/oauthApps.html b/app/oauth2-server-config/client/admin/views/oauthApps.html
new file mode 100644
index 000000000000..99a142127802
--- /dev/null
+++ b/app/oauth2-server-config/client/admin/views/oauthApps.html
@@ -0,0 +1,38 @@
+
+
+ {{# header sectionName=pageTitle}}
+
+ {{/header}}
+
+ {{#if hasPermission}}
+
+ {{else}}
+ {{_ "Not_authorized"}}
+ {{/if}}
+
+
+
diff --git a/app/oauth2-server-config/client/admin/views/oauthApps.js b/app/oauth2-server-config/client/admin/views/oauthApps.js
new file mode 100644
index 000000000000..701367de9364
--- /dev/null
+++ b/app/oauth2-server-config/client/admin/views/oauthApps.js
@@ -0,0 +1,30 @@
+import { Template } from 'meteor/templating';
+import { Tracker } from 'meteor/tracker';
+import { hasAllPermission } from '../../../../authorization';
+import { ChatOAuthApps } from '../collection';
+import moment from 'moment';
+import { SideNav } from '../../../../ui-utils/client';
+
+Template.oauthApps.onCreated(function() {
+ this.subscribe('oauthApps');
+});
+
+Template.oauthApps.helpers({
+ hasPermission() {
+ return hasAllPermission('manage-oauth-apps');
+ },
+ applications() {
+ return ChatOAuthApps.find();
+ },
+ dateFormated(date) {
+ return moment(date).format('L LT');
+ },
+});
+
+Template.oauthApps.onRendered(() => {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+});
+
diff --git a/packages/rocketchat-oauth2-server-config/client/index.js b/app/oauth2-server-config/client/index.js
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/client/index.js
rename to app/oauth2-server-config/client/index.js
diff --git a/packages/rocketchat-oauth2-server-config/client/oauth/oauth2-client.html b/app/oauth2-server-config/client/oauth/oauth2-client.html
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/client/oauth/oauth2-client.html
rename to app/oauth2-server-config/client/oauth/oauth2-client.html
diff --git a/packages/rocketchat-oauth2-server-config/client/oauth/oauth2-client.js b/app/oauth2-server-config/client/oauth/oauth2-client.js
similarity index 92%
rename from packages/rocketchat-oauth2-server-config/client/oauth/oauth2-client.js
rename to app/oauth2-server-config/client/oauth/oauth2-client.js
index e6b02c2adffb..f1d82952a8a1 100644
--- a/packages/rocketchat-oauth2-server-config/client/oauth/oauth2-client.js
+++ b/app/oauth2-server-config/client/oauth/oauth2-client.js
@@ -3,6 +3,7 @@ import { FlowRouter } from 'meteor/kadira:flow-router' ;
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { Template } from 'meteor/templating';
import { ChatOAuthApps } from '../admin/collection';
+import { Accounts } from 'meteor/accounts-base';
FlowRouter.route('/oauth/authorize', {
action(params, queryParams) {
@@ -34,7 +35,7 @@ Template.authorize.onCreated(function() {
Template.authorize.helpers({
getToken() {
- return localStorage.getItem('Meteor.loginToken');
+ return localStorage.getItem(Accounts.LOGIN_TOKEN_KEY);
},
getClient() {
return ChatOAuthApps.findOne();
diff --git a/packages/rocketchat-oauth2-server-config/client/oauth/stylesheets/oauth2.css b/app/oauth2-server-config/client/oauth/stylesheets/oauth2.css
similarity index 100%
rename from packages/rocketchat-oauth2-server-config/client/oauth/stylesheets/oauth2.css
rename to app/oauth2-server-config/client/oauth/stylesheets/oauth2.css
diff --git a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js
new file mode 100644
index 000000000000..3a3614dbaf1a
--- /dev/null
+++ b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js
@@ -0,0 +1,28 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { hasPermission } from '../../../../authorization';
+import { Users, OAuthApps } from '../../../../models';
+import _ from 'underscore';
+
+Meteor.methods({
+ addOAuthApp(application) {
+ if (!hasPermission(this.userId, 'manage-oauth-apps')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' });
+ }
+ if (!_.isString(application.name) || application.name.trim() === '') {
+ throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'addOAuthApp' });
+ }
+ if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') {
+ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'addOAuthApp' });
+ }
+ if (!_.isBoolean(application.active)) {
+ throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'addOAuthApp' });
+ }
+ application.clientId = Random.id();
+ application.clientSecret = Random.secret();
+ application._createdAt = new Date;
+ application._createdBy = Users.findOne(this.userId, { fields: { username: 1 } });
+ application._id = OAuthApps.insert(application);
+ return application;
+ },
+});
diff --git a/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js
new file mode 100644
index 000000000000..e08bbfdfb15d
--- /dev/null
+++ b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js
@@ -0,0 +1,17 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../../authorization';
+import { OAuthApps } from '../../../../models';
+
+Meteor.methods({
+ deleteOAuthApp(applicationId) {
+ if (!hasPermission(this.userId, 'manage-oauth-apps')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' });
+ }
+ const application = OAuthApps.findOne(applicationId);
+ if (application == null) {
+ throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp' });
+ }
+ OAuthApps.remove({ _id: applicationId });
+ return true;
+ },
+});
diff --git a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js
new file mode 100644
index 000000000000..50292d4bc06e
--- /dev/null
+++ b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../../authorization';
+import { OAuthApps, Users } from '../../../../models';
+import _ from 'underscore';
+
+Meteor.methods({
+ updateOAuthApp(applicationId, application) {
+ if (!hasPermission(this.userId, 'manage-oauth-apps')) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' });
+ }
+ if (!_.isString(application.name) || application.name.trim() === '') {
+ throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' });
+ }
+ if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') {
+ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' });
+ }
+ if (!_.isBoolean(application.active)) {
+ throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'updateOAuthApp' });
+ }
+ const currentApplication = OAuthApps.findOne(applicationId);
+ if (currentApplication == null) {
+ throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'updateOAuthApp' });
+ }
+ OAuthApps.update(applicationId, {
+ $set: {
+ name: application.name,
+ active: application.active,
+ redirectUri: application.redirectUri,
+ _updatedAt: new Date,
+ _updatedBy: Users.findOne(this.userId, {
+ fields: {
+ username: 1,
+ },
+ }),
+ },
+ });
+ return OAuthApps.findOne(applicationId);
+ },
+});
diff --git a/app/oauth2-server-config/server/admin/publications/oauthApps.js b/app/oauth2-server-config/server/admin/publications/oauthApps.js
new file mode 100644
index 000000000000..498155b1e279
--- /dev/null
+++ b/app/oauth2-server-config/server/admin/publications/oauthApps.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../../authorization';
+import { OAuthApps } from '../../../../models';
+
+Meteor.publish('oauthApps', function() {
+ if (!this.userId) {
+ return this.ready();
+ }
+ if (!hasPermission(this.userId, 'manage-oauth-apps')) {
+ this.error(Meteor.Error('error-not-allowed', 'Not allowed', { publish: 'oauthApps' }));
+ }
+ return OAuthApps.find();
+});
diff --git a/app/oauth2-server-config/server/index.js b/app/oauth2-server-config/server/index.js
new file mode 100644
index 000000000000..4fe04df84ed2
--- /dev/null
+++ b/app/oauth2-server-config/server/index.js
@@ -0,0 +1,6 @@
+import './oauth/oauth2-server';
+import './oauth/default-services';
+import './admin/publications/oauthApps';
+import './admin/methods/addOAuthApp';
+import './admin/methods/updateOAuthApp';
+import './admin/methods/deleteOAuthApp';
diff --git a/app/oauth2-server-config/server/oauth/default-services.js b/app/oauth2-server-config/server/oauth/default-services.js
new file mode 100644
index 000000000000..4925ed314cae
--- /dev/null
+++ b/app/oauth2-server-config/server/oauth/default-services.js
@@ -0,0 +1,17 @@
+import { OAuthApps } from '../../../models';
+
+if (!OAuthApps.findOne('zapier')) {
+ OAuthApps.insert({
+ _id: 'zapier',
+ name: 'Zapier',
+ active: true,
+ clientId: 'zapier',
+ clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr',
+ redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/',
+ _createdAt: new Date,
+ _createdBy: {
+ _id: 'system',
+ username: 'system',
+ },
+ });
+}
diff --git a/packages/rocketchat-oauth2-server-config/server/oauth/oauth2-server.js b/app/oauth2-server-config/server/oauth/oauth2-server.js
similarity index 85%
rename from packages/rocketchat-oauth2-server-config/server/oauth/oauth2-server.js
rename to app/oauth2-server-config/server/oauth/oauth2-server.js
index ec1da72899c6..fbc6fed4e63d 100644
--- a/packages/rocketchat-oauth2-server-config/server/oauth/oauth2-server.js
+++ b/app/oauth2-server-config/server/oauth/oauth2-server.js
@@ -1,16 +1,20 @@
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { OAuthApps, Users } from '../../../models';
import { OAuth2Server } from 'meteor/rocketchat:oauth2-server';
+import { API } from '../../../api';
const oauth2server = new OAuth2Server({
accessTokensCollectionName: 'rocketchat_oauth_access_tokens',
refreshTokensCollectionName: 'rocketchat_oauth_refresh_tokens',
authCodesCollectionName: 'rocketchat_oauth_auth_codes',
- clientsCollection: RocketChat.models.OAuthApps.model,
+ clientsCollection: OAuthApps.model,
debug: true,
});
+oauth2server.app.disable('x-powered-by');
+oauth2server.routes.disable('x-powered-by');
+
WebApp.connectHandlers.use(oauth2server.app);
oauth2server.routes.get('/oauth/userinfo', function(req, res) {
@@ -24,7 +28,7 @@ oauth2server.routes.get('/oauth/userinfo', function(req, res) {
if (token == null) {
return res.sendStatus(401).send('Invalid Token');
}
- const user = RocketChat.models.Users.findOneById(token.userId);
+ const user = Users.findOneById(token.userId);
if (user == null) {
return res.sendStatus(401).send('Invalid Token');
}
@@ -45,7 +49,7 @@ Meteor.publish('oauthClient', function(clientId) {
if (!this.userId) {
return this.ready();
}
- return RocketChat.models.OAuthApps.find({
+ return OAuthApps.find({
clientId,
active: true,
}, {
@@ -55,7 +59,7 @@ Meteor.publish('oauthClient', function(clientId) {
});
});
-RocketChat.API.v1.addAuthMethod(function() {
+API.v1.addAuthMethod(function() {
let headerToken = this.request.headers.authorization;
const getToken = this.request.query.access_token;
if (headerToken != null) {
@@ -78,7 +82,7 @@ RocketChat.API.v1.addAuthMethod(function() {
if ((accessToken.expires != null) && accessToken.expires !== 0 && accessToken.expires < new Date()) {
return;
}
- const user = RocketChat.models.Users.findOne(accessToken.userId);
+ const user = Users.findOne(accessToken.userId);
if (user == null) {
return;
}
diff --git a/packages/rocketchat-oembed/client/baseWidget.html b/app/oembed/client/baseWidget.html
similarity index 100%
rename from packages/rocketchat-oembed/client/baseWidget.html
rename to app/oembed/client/baseWidget.html
diff --git a/packages/rocketchat-oembed/client/baseWidget.js b/app/oembed/client/baseWidget.js
similarity index 88%
rename from packages/rocketchat-oembed/client/baseWidget.js
rename to app/oembed/client/baseWidget.js
index 66263dcf5992..004169306bd6 100644
--- a/packages/rocketchat-oembed/client/baseWidget.js
+++ b/app/oembed/client/baseWidget.js
@@ -22,9 +22,6 @@ Template.oembedBaseWidget.helpers({
if (this.meta && this.meta.oembedHtml) {
return 'oembedFrameWidget';
}
- if (this.meta && this.meta.sandstorm && this.meta.sandstorm.grain) {
- return 'oembedSandstormGrain';
- }
return 'oembedUrlWidget';
},
});
diff --git a/app/oembed/client/index.js b/app/oembed/client/index.js
new file mode 100644
index 000000000000..c5b326243e89
--- /dev/null
+++ b/app/oembed/client/index.js
@@ -0,0 +1,14 @@
+import './baseWidget.html';
+import './baseWidget';
+import './oembedImageWidget.html';
+import './oembedImageWidget';
+import './oembedAudioWidget.html';
+import './oembedAudioWidget';
+import './oembedVideoWidget.html';
+import './oembedVideoWidget';
+import './oembedYoutubeWidget.html';
+import './oembedYoutubeWidget';
+import './oembedUrlWidget.html';
+import './oembedUrlWidget';
+import './oembedFrameWidget.html';
+import './oembedFrameWidget';
diff --git a/packages/rocketchat-oembed/client/oembedAudioWidget.html b/app/oembed/client/oembedAudioWidget.html
similarity index 88%
rename from packages/rocketchat-oembed/client/oembedAudioWidget.html
rename to app/oembed/client/oembedAudioWidget.html
index 9d4117485189..62c1924b4491 100644
--- a/packages/rocketchat-oembed/client/oembedAudioWidget.html
+++ b/app/oembed/client/oembedAudioWidget.html
@@ -5,7 +5,7 @@
{{else}}
-
+
Your browser does not support the audio element.
diff --git a/app/oembed/client/oembedAudioWidget.js b/app/oembed/client/oembedAudioWidget.js
new file mode 100644
index 000000000000..f1eae8cada3d
--- /dev/null
+++ b/app/oembed/client/oembedAudioWidget.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { getUserPreference } from '../../utils';
+
+Template.oembedAudioWidget.helpers({
+ collapsed() {
+ if (this.collapsed) {
+ return this.collapsed;
+ } else {
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ }
+ },
+});
diff --git a/packages/rocketchat-oembed/client/oembedFrameWidget.html b/app/oembed/client/oembedFrameWidget.html
similarity index 93%
rename from packages/rocketchat-oembed/client/oembedFrameWidget.html
rename to app/oembed/client/oembedFrameWidget.html
index 7a88ca046c92..c11a82b79b38 100644
--- a/packages/rocketchat-oembed/client/oembedFrameWidget.html
+++ b/app/oembed/client/oembedFrameWidget.html
@@ -1,6 +1,6 @@
{{#if parsedUrl}}
-
+
{{#if meta.oembedProviderName}}
{{#if meta.oembedProviderUrl}}
{{meta.oembedProviderName}}
diff --git a/app/oembed/client/oembedFrameWidget.js b/app/oembed/client/oembedFrameWidget.js
new file mode 100644
index 000000000000..dee89ca5ef80
--- /dev/null
+++ b/app/oembed/client/oembedFrameWidget.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { getUserPreference } from '../../utils';
+
+Template.oembedFrameWidget.helpers({
+ collapsed() {
+ if (this.collapsed) {
+ return this.collapsed;
+ } else {
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ }
+ },
+});
diff --git a/packages/rocketchat-oembed/client/oembedImageWidget.html b/app/oembed/client/oembedImageWidget.html
similarity index 100%
rename from packages/rocketchat-oembed/client/oembedImageWidget.html
rename to app/oembed/client/oembedImageWidget.html
diff --git a/app/oembed/client/oembedImageWidget.js b/app/oembed/client/oembedImageWidget.js
new file mode 100644
index 000000000000..fc7dcdd1d20d
--- /dev/null
+++ b/app/oembed/client/oembedImageWidget.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { getUserPreference } from '../../utils';
+
+Template.oembedImageWidget.helpers({
+ loadImage() {
+ if (getUserPreference(Meteor.userId(), 'autoImageLoad') === false && this.downloadImages == null) {
+ return false;
+ }
+ if (Meteor.Device.isPhone() && getUserPreference(Meteor.userId(), 'saveMobileBandwidth') && this.downloadImages == null) {
+ return false;
+ }
+ return true;
+ },
+ collapsed() {
+ if (this.collapsed != null) {
+ return this.collapsed;
+ } else {
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ }
+ },
+});
diff --git a/packages/rocketchat-oembed/client/oembedUrlWidget.html b/app/oembed/client/oembedUrlWidget.html
similarity index 93%
rename from packages/rocketchat-oembed/client/oembedUrlWidget.html
rename to app/oembed/client/oembedUrlWidget.html
index a464f5f32a52..3d411cdebb37 100644
--- a/packages/rocketchat-oembed/client/oembedUrlWidget.html
+++ b/app/oembed/client/oembedUrlWidget.html
@@ -1,6 +1,6 @@
{{#if show}}
-
+
{{#if image}}
{{#if meta.ogImageUserGenerated}}
diff --git a/packages/rocketchat-oembed/client/oembedUrlWidget.js b/app/oembed/client/oembedUrlWidget.js
similarity index 92%
rename from packages/rocketchat-oembed/client/oembedUrlWidget.js
rename to app/oembed/client/oembedUrlWidget.js
index d231f6a35e80..943d33b44a30 100644
--- a/packages/rocketchat-oembed/client/oembedUrlWidget.js
+++ b/app/oembed/client/oembedUrlWidget.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { getUserPreference } from '../../utils';
import _ from 'underscore';
const getTitle = function(self) {
@@ -66,7 +66,7 @@ Template.oembedUrlWidget.helpers({
if (this.collapsed != null) {
return this.collapsed;
} else {
- return RocketChat.getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
}
},
});
diff --git a/packages/rocketchat-oembed/client/oembedVideoWidget.html b/app/oembed/client/oembedVideoWidget.html
similarity index 89%
rename from packages/rocketchat-oembed/client/oembedVideoWidget.html
rename to app/oembed/client/oembedVideoWidget.html
index 20b2189dca3a..7cbc8e658c78 100644
--- a/packages/rocketchat-oembed/client/oembedVideoWidget.html
+++ b/app/oembed/client/oembedVideoWidget.html
@@ -1,6 +1,6 @@
{{#if parsedUrl}}
-
+
{{title}}
{{#if collapsed}}
diff --git a/packages/rocketchat-oembed/client/oembedVideoWidget.js b/app/oembed/client/oembedVideoWidget.js
similarity index 84%
rename from packages/rocketchat-oembed/client/oembedVideoWidget.js
rename to app/oembed/client/oembedVideoWidget.js
index 0c0d21d40d89..be7764b45104 100644
--- a/packages/rocketchat-oembed/client/oembedVideoWidget.js
+++ b/app/oembed/client/oembedVideoWidget.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { getUserPreference } from '../../utils';
const getTitle = function(self) {
if (self.meta == null) {
@@ -31,7 +31,7 @@ Template.oembedVideoWidget.helpers({
if (this.collapsed) {
return this.collapsed;
} else {
- return RocketChat.getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
+ return getUserPreference(Meteor.userId(), 'collapseMediaByDefault') === true;
}
},
diff --git a/packages/rocketchat-oembed/client/oembedYoutubeWidget.html b/app/oembed/client/oembedYoutubeWidget.html
similarity index 79%
rename from packages/rocketchat-oembed/client/oembedYoutubeWidget.html
rename to app/oembed/client/oembedYoutubeWidget.html
index 0c7cbbb5bfe3..a30b1c1eb2d0 100644
--- a/packages/rocketchat-oembed/client/oembedYoutubeWidget.html
+++ b/app/oembed/client/oembedYoutubeWidget.html
@@ -1,12 +1,12 @@
{{#if parsedUrl}}
-
+
{{parsedUrl.host}}
{{#if collapsed}}
{{else}}
-
+
{{{meta.description}}}
{{/if}}
diff --git a/app/oembed/client/oembedYoutubeWidget.js b/app/oembed/client/oembedYoutubeWidget.js
new file mode 100644
index 000000000000..2e8d2557c620
--- /dev/null
+++ b/app/oembed/client/oembedYoutubeWidget.js
@@ -0,0 +1,14 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { getUserPreference } from '../../utils';
+
+Template.oembedYoutubeWidget.helpers({
+ collapsed() {
+ if (this.collapsed) {
+ return this.collapsed;
+ } else {
+ const user = Meteor.user();
+ return getUserPreference(user, 'collapseMediaByDefault') === true;
+ }
+ },
+});
diff --git a/app/oembed/server/index.js b/app/oembed/server/index.js
new file mode 100644
index 000000000000..86617752ce43
--- /dev/null
+++ b/app/oembed/server/index.js
@@ -0,0 +1,7 @@
+import './jumpToMessage';
+import './providers';
+import { OEmbed } from './server';
+
+export {
+ OEmbed,
+};
diff --git a/app/oembed/server/jumpToMessage.js b/app/oembed/server/jumpToMessage.js
new file mode 100644
index 000000000000..5158913cd4b4
--- /dev/null
+++ b/app/oembed/server/jumpToMessage.js
@@ -0,0 +1,55 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages } from '../../models';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
+import _ from 'underscore';
+import URL from 'url';
+import QueryString from 'querystring';
+
+const recursiveRemove = (message, deep = 1) => {
+ if (message) {
+ if ('attachments' in message && message.attachments !== null && deep < settings.get('Message_QuoteChainLimit')) {
+ message.attachments.map((msg) => recursiveRemove(msg, deep + 1));
+ } else {
+ delete(message.attachments);
+ }
+ }
+ return message;
+};
+
+callbacks.add('beforeSaveMessage', (msg) => {
+ if (msg && msg.urls) {
+ msg.urls.forEach((item) => {
+ if (item.url.indexOf(Meteor.absoluteUrl()) === 0) {
+ const urlObj = URL.parse(item.url);
+ if (urlObj.query) {
+ const queryString = QueryString.parse(urlObj.query);
+ if (_.isString(queryString.msg)) { // Jump-to query param
+ const jumpToMessage = recursiveRemove(Messages.findOneById(queryString.msg));
+ if (jumpToMessage) {
+ msg.attachments = msg.attachments || [];
+
+ const index = msg.attachments.findIndex((a) => a.message_link === item.url);
+ if (index > -1) {
+ msg.attachments.splice(index, 1);
+ }
+
+ msg.attachments.push({
+ text: jumpToMessage.msg,
+ translations: jumpToMessage.translations,
+ author_name: jumpToMessage.alias || jumpToMessage.u.username,
+ author_icon: getUserAvatarURL(jumpToMessage.u.username),
+ message_link: item.url,
+ attachments: jumpToMessage.attachments || [],
+ ts: jumpToMessage.ts,
+ });
+ item.ignoreParse = true;
+ }
+ }
+ }
+ }
+ });
+ }
+ return msg;
+}, callbacks.priority.LOW, 'jumpToMessage');
diff --git a/packages/rocketchat-oembed/server/providers.js b/app/oembed/server/providers.js
similarity index 88%
rename from packages/rocketchat-oembed/server/providers.js
rename to app/oembed/server/providers.js
index 60e18cc8144b..b5cacfd5305a 100644
--- a/packages/rocketchat-oembed/server/providers.js
+++ b/app/oembed/server/providers.js
@@ -1,5 +1,5 @@
import { changeCase } from 'meteor/konecty:change-case';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { callbacks } from '../../callbacks';
import _ from 'underscore';
import URL from 'url';
import QueryString from 'querystring';
@@ -66,11 +66,11 @@ providers.registerProvider({
endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200',
});
-RocketChat.oembed = {};
+export const oembed = {};
-RocketChat.oembed.providers = providers;
+oembed.providers = providers;
-RocketChat.callbacks.add('oembed:beforeGetUrlContent', function(data) {
+callbacks.add('oembed:beforeGetUrlContent', function(data) {
if (data.parsedUrl != null) {
const url = URL.format(data.parsedUrl);
const provider = providers.getProviderForUrl(url);
@@ -87,9 +87,9 @@ RocketChat.callbacks.add('oembed:beforeGetUrlContent', function(data) {
}
}
return data;
-}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-before');
+}, callbacks.priority.MEDIUM, 'oembed-providers-before');
-RocketChat.callbacks.add('oembed:afterParseContent', function(data) {
+callbacks.add('oembed:afterParseContent', function(data) {
if (data.parsedUrl && data.parsedUrl.query) {
let queryString = data.parsedUrl.query;
if (_.isString(data.parsedUrl.query)) {
@@ -116,4 +116,4 @@ RocketChat.callbacks.add('oembed:afterParseContent', function(data) {
}
}
return data;
-}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-after');
+}, callbacks.priority.MEDIUM, 'oembed-providers-after');
diff --git a/app/oembed/server/server.js b/app/oembed/server/server.js
new file mode 100644
index 000000000000..37a3c4deaf1c
--- /dev/null
+++ b/app/oembed/server/server.js
@@ -0,0 +1,295 @@
+import { Meteor } from 'meteor/meteor';
+import { HTTPInternals } from 'meteor/http';
+import { changeCase } from 'meteor/konecty:change-case';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { OEmbedCache, Messages } from '../../models';
+import _ from 'underscore';
+import URL from 'url';
+import querystring from 'querystring';
+import iconv from 'iconv-lite';
+import ipRangeCheck from 'ip-range-check';
+import he from 'he';
+import jschardet from 'jschardet';
+import { isURL } from '../../utils/lib/isURL';
+
+const request = HTTPInternals.NpmModules.request.module;
+const OEmbed = {};
+
+// Detect encoding
+// Priority:
+// Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8)
+// See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer
+const getCharset = function(contentType, body) {
+ let detectedCharset;
+ let httpHeaderCharset;
+ let htmlMetaCharset;
+ let result;
+
+ contentType = contentType || '';
+
+ const binary = body.toString('binary');
+ const detected = jschardet.detect(binary);
+ if (detected.confidence > 0.8) {
+ detectedCharset = detected.encoding.toLowerCase();
+ }
+ const m1 = contentType.match(/charset=([\w\-]+)/i);
+ if (m1) {
+ httpHeaderCharset = m1[1].toLowerCase();
+ }
+ const m2 = binary.match(/ ]*charset=["']?([\w\-]+)/i);
+ if (m2) {
+ htmlMetaCharset = m2[1].toLowerCase();
+ }
+ if (detectedCharset) {
+ if (detectedCharset === httpHeaderCharset) {
+ result = httpHeaderCharset;
+ } else if (detectedCharset === htmlMetaCharset) {
+ result = htmlMetaCharset;
+ }
+ }
+ if (!result) {
+ result = httpHeaderCharset || htmlMetaCharset || detectedCharset;
+ }
+ return result || 'utf-8';
+};
+
+const toUtf8 = function(contentType, body) {
+ return iconv.decode(body, getCharset(contentType, body));
+};
+
+const getUrlContent = function(urlObj, redirectCount = 5, callback) {
+
+ if (_.isString(urlObj)) {
+ urlObj = URL.parse(urlObj);
+ }
+
+ const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']);
+ const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || [];
+ if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) {
+ return callback();
+ }
+
+ const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || [];
+ if (parsedUrl.port && safePorts.length > 0 && (!safePorts.includes(parsedUrl.port))) {
+ return callback();
+ }
+
+ const data = callbacks.run('oembed:beforeGetUrlContent', {
+ urlObj,
+ parsedUrl,
+ });
+ if (data.attachments != null) {
+ return callback(null, data);
+ }
+ const url = URL.format(data.urlObj);
+ const opts = {
+ url,
+ strictSSL: !settings.get('Allow_Invalid_SelfSigned_Certs'),
+ gzip: true,
+ maxRedirects: redirectCount,
+ headers: {
+ 'User-Agent': settings.get('API_Embed_UserAgent'),
+ },
+ };
+ let headers = null;
+ let statusCode = null;
+ let error = null;
+ const chunks = [];
+ let chunksTotalLength = 0;
+ const stream = request(opts);
+ stream.on('response', function(response) {
+ statusCode = response.statusCode;
+ headers = response.headers;
+ if (response.statusCode !== 200) {
+ return stream.abort();
+ }
+ });
+ stream.on('data', function(chunk) {
+ chunks.push(chunk);
+ chunksTotalLength += chunk.length;
+ if (chunksTotalLength > 250000) {
+ return stream.abort();
+ }
+ });
+ stream.on('end', Meteor.bindEnvironment(function() {
+ if (error != null) {
+ return callback(null, {
+ error,
+ parsedUrl,
+ });
+ }
+ const buffer = Buffer.concat(chunks);
+ return callback(null, {
+ headers,
+ body: toUtf8(headers['content-type'], buffer),
+ parsedUrl,
+ statusCode,
+ });
+ }));
+ return stream.on('error', function(err) {
+ return error = err;
+ });
+};
+
+OEmbed.getUrlMeta = function(url, withFragment) {
+ const getUrlContentSync = Meteor.wrapAsync(getUrlContent);
+ const urlObj = URL.parse(url);
+ if (withFragment != null) {
+ const queryStringObj = querystring.parse(urlObj.query);
+ queryStringObj._escaped_fragment_ = '';
+ urlObj.query = querystring.stringify(queryStringObj);
+ let path = urlObj.pathname;
+ if (urlObj.query != null) {
+ path += `?${ urlObj.query }`;
+ urlObj.search = `?${ urlObj.query }`;
+ }
+ urlObj.path = path;
+ }
+ const content = getUrlContentSync(urlObj, 5);
+ if (!content) {
+ return;
+ }
+ if (content.attachments != null) {
+ return content;
+ }
+ let metas = undefined;
+ if (content && content.body) {
+ metas = {};
+ content.body.replace(/]*>([^<]*)<\/title>/gmi, function(meta, title) {
+ return metas.pageTitle != null ? metas.pageTitle : metas.pageTitle = he.unescape(title);
+ });
+ content.body.replace(/ ]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gmi, function(meta, name, value) {
+ let name1;
+ return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
+ });
+ content.body.replace(/ ]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gmi, function(meta, name, value) {
+ let name1;
+ return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
+ });
+ content.body.replace(/ ]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, function(meta, value, name) {
+ let name1;
+ return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
+ });
+ content.body.replace(/ ]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, function(meta, value, name) {
+ let name1;
+ return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
+ });
+ if (metas.fragment === '!' && (withFragment == null)) {
+ return OEmbed.getUrlMeta(url, true);
+ }
+ }
+ let headers = undefined;
+ let data = undefined;
+
+
+ if (content && content.headers) {
+ headers = {};
+ const headerObj = content.headers;
+ Object.keys(headerObj).forEach((header) => {
+ headers[changeCase.camelCase(header)] = headerObj[header];
+ });
+ }
+ if (content && content.statusCode !== 200) {
+ return data;
+ }
+ data = callbacks.run('oembed:afterParseContent', {
+ meta: metas,
+ headers,
+ parsedUrl: content.parsedUrl,
+ content,
+ });
+ return data;
+};
+
+OEmbed.getUrlMetaWithCache = function(url, withFragment) {
+ const cache = OEmbedCache.findOneById(url);
+ if (cache != null) {
+ return cache.data;
+ }
+ const data = OEmbed.getUrlMeta(url, withFragment);
+ if (data != null) {
+ try {
+ OEmbedCache.createWithIdAndData(url, data);
+ } catch (_error) {
+ console.error('OEmbed duplicated record', url);
+ }
+ return data;
+ }
+};
+
+const getRelevantHeaders = function(headersObj) {
+ const headers = {};
+ Object.keys(headersObj).forEach((key) => {
+ const value = headersObj[key];
+ const lowerCaseKey = key.toLowerCase();
+ if ((lowerCaseKey === 'contenttype' || lowerCaseKey === 'contentlength') && (value && value.trim() !== '')) {
+ headers[key] = value;
+ }
+ });
+
+ if (Object.keys(headers).length > 0) {
+ return headers;
+ }
+};
+
+const getRelevantMetaTags = function(metaObj) {
+ const tags = {};
+ Object.keys(metaObj).forEach((key) => {
+ const value = metaObj[key];
+ if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && (value && value.trim() !== '')) {
+ tags[key] = value;
+ }
+ });
+
+ if (Object.keys(tags).length > 0) {
+ return tags;
+ }
+};
+
+OEmbed.rocketUrlParser = function(message) {
+ if (Array.isArray(message.urls)) {
+ let attachments = [];
+ let changed = false;
+ message.urls.forEach(function(item) {
+ if (item.ignoreParse === true) {
+ return;
+ }
+ if (!isURL(item.url)) {
+ return;
+ }
+ const data = OEmbed.getUrlMetaWithCache(item.url);
+ if (data != null) {
+ if (data.attachments) {
+ return attachments = _.union(attachments, data.attachments);
+ } else {
+ if (data.meta != null) {
+ item.meta = getRelevantMetaTags(data.meta);
+ }
+ if (data.headers != null) {
+ item.headers = getRelevantHeaders(data.headers);
+ }
+ item.parsedUrl = data.parsedUrl;
+ return changed = true;
+ }
+ }
+ });
+ if (attachments.length) {
+ Messages.setMessageAttachments(message._id, attachments);
+ }
+ if (changed === true) {
+ Messages.setUrlsById(message._id, message.urls);
+ }
+ }
+ return message;
+};
+
+settings.get('API_Embed', function(key, value) {
+ if (value) {
+ return callbacks.add('afterSaveMessage', OEmbed.rocketUrlParser, callbacks.priority.LOW, 'API_Embed');
+ } else {
+ return callbacks.remove('afterSaveMessage', 'API_Embed');
+ }
+});
+
+export { OEmbed };
diff --git a/app/otr/client/index.js b/app/otr/client/index.js
new file mode 100644
index 000000000000..5a2ca16a74a7
--- /dev/null
+++ b/app/otr/client/index.js
@@ -0,0 +1,5 @@
+import './rocketchat.otr.room';
+import './rocketchat.otr';
+import './views/otrFlexTab.html';
+import './views/otrFlexTab';
+import './tabBar';
diff --git a/app/otr/client/rocketchat.otr.js b/app/otr/client/rocketchat.otr.js
new file mode 100644
index 000000000000..54d088259976
--- /dev/null
+++ b/app/otr/client/rocketchat.otr.js
@@ -0,0 +1,112 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Tracker } from 'meteor/tracker';
+import { Subscriptions } from '../../models';
+import { promises } from '../../promises/client';
+import { Notifications } from '../../notifications';
+import { t } from '../../utils';
+
+class OTRClass {
+ constructor() {
+ this.enabled = new ReactiveVar(false);
+ this.instancesByRoomId = {};
+ }
+
+ isEnabled() {
+ return this.enabled.get();
+ }
+
+ getInstanceByRoomId(roomId) {
+ if (!this.enabled.get()) {
+ return;
+ }
+
+ if (this.instancesByRoomId[roomId]) {
+ return this.instancesByRoomId[roomId];
+ }
+
+ const subscription = Subscriptions.findOne({
+ rid: roomId,
+ });
+
+ if (!subscription || subscription.t !== 'd') {
+ return;
+ }
+
+ this.instancesByRoomId[roomId] = new OTR.Room(Meteor.userId(), roomId); // eslint-disable-line no-use-before-define
+ return this.instancesByRoomId[roomId];
+ }
+}
+
+export const OTR = new OTRClass();
+
+Meteor.startup(function() {
+ Tracker.autorun(function() {
+ if (Meteor.userId()) {
+ Notifications.onUser('otr', (type, data) => {
+ if (!data.roomId || !data.userId || data.userId === Meteor.userId()) {
+ return;
+ } else {
+ OTR.getInstanceByRoomId(data.roomId).onUserStream(type, data);
+ }
+ });
+ }
+ });
+
+ promises.add('onClientBeforeSendMessage', function(message) {
+ if (message.rid && OTR.getInstanceByRoomId(message.rid) && OTR.getInstanceByRoomId(message.rid).established.get()) {
+ return OTR.getInstanceByRoomId(message.rid).encrypt(message)
+ .then((msg) => {
+ message.msg = msg;
+ message.t = 'otr';
+ return message;
+ });
+ } else {
+ return Promise.resolve(message);
+ }
+ }, promises.priority.HIGH);
+
+ promises.add('onClientMessageReceived', function(message) {
+ if (message.rid && OTR.getInstanceByRoomId(message.rid) && OTR.getInstanceByRoomId(message.rid).established.get()) {
+ if (message.notification) {
+ message.msg = t('Encrypted_message');
+ return Promise.resolve(message);
+ } else {
+ const otrRoom = OTR.getInstanceByRoomId(message.rid);
+ return otrRoom.decrypt(message.msg)
+ .then((data) => {
+ const { _id, text, ack } = data;
+ message._id = _id;
+ message.msg = text;
+
+ if (data.ts) {
+ message.ts = data.ts;
+ }
+
+ if (message.otrAck) {
+ return otrRoom.decrypt(message.otrAck)
+ .then((data) => {
+ if (ack === data.text) {
+ message.t = 'otr-ack';
+ }
+ return message;
+ });
+ } else if (data.userId !== Meteor.userId()) {
+ return otrRoom.encryptText(ack)
+ .then((ack) => {
+ Meteor.call('updateOTRAck', message._id, ack);
+ return message;
+ });
+ } else {
+ return message;
+ }
+ });
+ }
+ } else {
+ if (message.t === 'otr') {
+ message.msg = '';
+ }
+ return Promise.resolve(message);
+ }
+ }, promises.priority.HIGH);
+});
diff --git a/packages/rocketchat-otr/client/rocketchat.otr.room.js b/app/otr/client/rocketchat.otr.room.js
similarity index 84%
rename from packages/rocketchat-otr/client/rocketchat.otr.room.js
rename to app/otr/client/rocketchat.otr.room.js
index a70e7b893ebb..4383f1deb3a6 100644
--- a/packages/rocketchat-otr/client/rocketchat.otr.room.js
+++ b/app/otr/client/rocketchat.otr.room.js
@@ -6,12 +6,13 @@ import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { TAPi18n } from 'meteor/tap:i18n';
import { TimeSync } from 'meteor/mizzao:timesync';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import { modal } from 'meteor/rocketchat:ui';
+import { Notifications } from '../../notifications';
+import { modal } from '../../ui-utils';
+import { OTR } from './rocketchat.otr';
import _ from 'underscore';
import toastr from 'toastr';
-RocketChat.OTR.Room = class {
+OTR.Room = class {
constructor(userId, roomId) {
this.userId = userId;
this.roomId = roomId;
@@ -30,22 +31,22 @@ RocketChat.OTR.Room = class {
this.establishing.set(true);
this.firstPeer = true;
this.generateKeyPair().then(() => {
- RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'handshake', { roomId: this.roomId, userId: this.userId, publicKey: EJSON.stringify(this.exportedPublicKey), refresh });
+ Notifications.notifyUser(this.peerId, 'otr', 'handshake', { roomId: this.roomId, userId: this.userId, publicKey: EJSON.stringify(this.exportedPublicKey), refresh });
});
}
acknowledge() {
- RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'acknowledge', { roomId: this.roomId, userId: this.userId, publicKey: EJSON.stringify(this.exportedPublicKey) });
+ Notifications.notifyUser(this.peerId, 'otr', 'acknowledge', { roomId: this.roomId, userId: this.userId, publicKey: EJSON.stringify(this.exportedPublicKey) });
}
deny() {
this.reset();
- RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'deny', { roomId: this.roomId, userId: this.userId });
+ Notifications.notifyUser(this.peerId, 'otr', 'deny', { roomId: this.roomId, userId: this.userId });
}
end() {
this.reset();
- RocketChat.Notifications.notifyUser(this.peerId, 'otr', 'end', { roomId: this.roomId, userId: this.userId });
+ Notifications.notifyUser(this.peerId, 'otr', 'end', { roomId: this.roomId, userId: this.userId });
}
reset() {
@@ -79,13 +80,13 @@ RocketChat.OTR.Room = class {
});
// Generate an ephemeral key pair.
- return RocketChat.OTR.crypto.generateKey({
+ return OTR.crypto.generateKey({
name: 'ECDH',
namedCurve: 'P-256',
}, false, ['deriveKey', 'deriveBits'])
.then((keyPair) => {
this.keyPair = keyPair;
- return RocketChat.OTR.crypto.exportKey('jwk', keyPair.publicKey);
+ return OTR.crypto.exportKey('jwk', keyPair.publicKey);
})
.then((exportedPublicKey) => {
this.exportedPublicKey = exportedPublicKey;
@@ -99,19 +100,19 @@ RocketChat.OTR.Room = class {
}
importPublicKey(publicKey) {
- return RocketChat.OTR.crypto.importKey('jwk', EJSON.parse(publicKey), {
+ return OTR.crypto.importKey('jwk', EJSON.parse(publicKey), {
name: 'ECDH',
namedCurve: 'P-256',
- }, false, []).then((peerPublicKey) => RocketChat.OTR.crypto.deriveBits({
+ }, false, []).then((peerPublicKey) => OTR.crypto.deriveBits({
name: 'ECDH',
namedCurve: 'P-256',
public: peerPublicKey,
- }, this.keyPair.privateKey, 256)).then((bits) => RocketChat.OTR.crypto.digest({
+ }, this.keyPair.privateKey, 256)).then((bits) => OTR.crypto.digest({
name: 'SHA-256',
}, bits)).then((hashedBits) => {
// We truncate the hash to 128 bits.
const sessionKeyData = new Uint8Array(hashedBits).slice(0, 16);
- return RocketChat.OTR.crypto.importKey('raw', sessionKeyData, {
+ return OTR.crypto.importKey('raw', sessionKeyData, {
name: 'AES-GCM',
}, false, ['encrypt', 'decrypt']);
}).then((sessionKey) => {
@@ -126,7 +127,7 @@ RocketChat.OTR.Room = class {
}
const iv = crypto.getRandomValues(new Uint8Array(12));
- return RocketChat.OTR.crypto.encrypt({
+ return OTR.crypto.encrypt({
name: 'AES-GCM',
iv,
}, this.sessionKey, data).then((cipherText) => {
@@ -164,7 +165,7 @@ RocketChat.OTR.Room = class {
const iv = cipherText.slice(0, 12);
cipherText = cipherText.slice(12);
- return RocketChat.OTR.crypto.decrypt({
+ return OTR.crypto.decrypt({
name: 'AES-GCM',
iv,
}, this.sessionKey, cipherText)
diff --git a/packages/rocketchat-otr/client/stylesheets/otr.css b/app/otr/client/stylesheets/otr.css
similarity index 100%
rename from packages/rocketchat-otr/client/stylesheets/otr.css
rename to app/otr/client/stylesheets/otr.css
diff --git a/app/otr/client/tabBar.js b/app/otr/client/tabBar.js
new file mode 100644
index 000000000000..b5fa4bf30c80
--- /dev/null
+++ b/app/otr/client/tabBar.js
@@ -0,0 +1,25 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { settings } from '../../settings';
+import { TabBar } from '../../ui-utils';
+import { OTR } from './rocketchat.otr';
+
+Meteor.startup(function() {
+ Tracker.autorun(function() {
+ if (settings.get('OTR_Enable') && window.crypto) {
+ OTR.crypto = window.crypto.subtle || window.crypto.webkitSubtle;
+ OTR.enabled.set(true);
+ TabBar.addButton({
+ groups: ['direct'],
+ id: 'otr',
+ i18nTitle: 'OTR',
+ icon: 'key',
+ template: 'otrFlexTab',
+ order: 11,
+ });
+ } else {
+ OTR.enabled.set(false);
+ TabBar.removeButton('otr');
+ }
+ });
+});
diff --git a/packages/rocketchat-otr/client/views/otrFlexTab.html b/app/otr/client/views/otrFlexTab.html
similarity index 100%
rename from packages/rocketchat-otr/client/views/otrFlexTab.html
rename to app/otr/client/views/otrFlexTab.html
diff --git a/app/otr/client/views/otrFlexTab.js b/app/otr/client/views/otrFlexTab.js
new file mode 100644
index 000000000000..09420e7ecccb
--- /dev/null
+++ b/app/otr/client/views/otrFlexTab.js
@@ -0,0 +1,85 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { OTR } from '../rocketchat.otr';
+import { modal } from '../../../ui-utils';
+import { t } from '../../../utils';
+
+Template.otrFlexTab.helpers({
+ otrAvailable() {
+ return OTR && OTR.isEnabled();
+ },
+ userIsOnline() {
+ // I have to appear online for the other user
+ if (Meteor.user().status === 'offline') {
+ return false;
+ }
+
+ if (this.rid) {
+ const peerId = this.rid.replace(Meteor.userId(), '');
+ if (peerId) {
+ const user = Meteor.users.findOne(peerId);
+ const online = user && user.status !== 'offline';
+ return online;
+ }
+ }
+ },
+ established() {
+ const otr = OTR.getInstanceByRoomId(this.rid);
+ return otr && otr.established.get();
+ },
+ establishing() {
+ const otr = OTR.getInstanceByRoomId(this.rid);
+ return otr && otr.establishing.get();
+ },
+});
+
+Template.otrFlexTab.events({
+ 'click button.start'(e, instance) {
+ e.preventDefault();
+ const otr = OTR.getInstanceByRoomId(this.rid);
+ if (otr) {
+ otr.handshake();
+ instance.timeout = Meteor.setTimeout(() => {
+ modal.open({
+ title: t('Timeout'),
+ type: 'error',
+ timer: 2000,
+ });
+ otr.establishing.set(false);
+ }, 10000);
+ }
+ },
+ 'click button.refresh'(e, instance) {
+ e.preventDefault();
+ const otr = OTR.getInstanceByRoomId(this.rid);
+ if (otr) {
+ otr.reset();
+ otr.handshake(true);
+ instance.timeout = Meteor.setTimeout(() => {
+ modal.open({
+ title: t('Timeout'),
+ type: 'error',
+ timer: 2000,
+ });
+ otr.establishing.set(false);
+ }, 10000);
+ }
+ },
+ 'click button.end'(e/* , t*/) {
+ e.preventDefault();
+ const otr = OTR.getInstanceByRoomId(this.rid);
+ if (otr) {
+ otr.end();
+ }
+ },
+});
+
+Template.otrFlexTab.onCreated(function() {
+ this.timeout = null;
+ this.autorun(() => {
+ const otr = OTR.getInstanceByRoomId(this.data.rid);
+ if (otr && otr.established.get()) {
+ Meteor.clearTimeout(this.timeout);
+ }
+ });
+});
diff --git a/app/otr/server/index.js b/app/otr/server/index.js
new file mode 100644
index 000000000000..1df0c14f3b15
--- /dev/null
+++ b/app/otr/server/index.js
@@ -0,0 +1,3 @@
+import './settings';
+import './methods/deleteOldOTRMessages';
+import './methods/updateOTRAck';
diff --git a/app/otr/server/methods/deleteOldOTRMessages.js b/app/otr/server/methods/deleteOldOTRMessages.js
new file mode 100644
index 000000000000..6f560ba0fc06
--- /dev/null
+++ b/app/otr/server/methods/deleteOldOTRMessages.js
@@ -0,0 +1,18 @@
+import { Meteor } from 'meteor/meteor';
+import { Subscriptions, Messages } from '../../../models';
+
+Meteor.methods({
+ deleteOldOTRMessages(roomId) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOldOTRMessages' });
+ }
+
+ const now = new Date();
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, Meteor.userId());
+ if (subscription && subscription.t === 'd') {
+ Messages.deleteOldOTRMessages(roomId, now);
+ } else {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'deleteOldOTRMessages' });
+ }
+ },
+});
diff --git a/app/otr/server/methods/updateOTRAck.js b/app/otr/server/methods/updateOTRAck.js
new file mode 100644
index 000000000000..fafb36fa3d24
--- /dev/null
+++ b/app/otr/server/methods/updateOTRAck.js
@@ -0,0 +1,11 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages } from '../../../models';
+
+Meteor.methods({
+ updateOTRAck(_id, ack) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' });
+ }
+ Messages.updateOTRAck(_id, ack);
+ },
+});
diff --git a/app/otr/server/settings.js b/app/otr/server/settings.js
new file mode 100644
index 000000000000..fbecb9c7519f
--- /dev/null
+++ b/app/otr/server/settings.js
@@ -0,0 +1,9 @@
+import { settings } from '../../settings';
+
+settings.addGroup('OTR', function() {
+ this.add('OTR_Enable', true, {
+ type: 'boolean',
+ i18nLabel: 'Enabled',
+ public: true,
+ });
+});
diff --git a/packages/rocketchat-promises/client/index.js b/app/promises/client/index.js
similarity index 100%
rename from packages/rocketchat-promises/client/index.js
rename to app/promises/client/index.js
diff --git a/packages/rocketchat-promises/lib/promises.js b/app/promises/lib/promises.js
similarity index 100%
rename from packages/rocketchat-promises/lib/promises.js
rename to app/promises/lib/promises.js
diff --git a/packages/rocketchat-promises/server/index.js b/app/promises/server/index.js
similarity index 100%
rename from packages/rocketchat-promises/server/index.js
rename to app/promises/server/index.js
diff --git a/packages/rocketchat-push-notifications/client/index.js b/app/push-notifications/client/index.js
similarity index 100%
rename from packages/rocketchat-push-notifications/client/index.js
rename to app/push-notifications/client/index.js
diff --git a/packages/rocketchat-push-notifications/client/stylesheets/pushNotifications.css b/app/push-notifications/client/stylesheets/pushNotifications.css
similarity index 100%
rename from packages/rocketchat-push-notifications/client/stylesheets/pushNotifications.css
rename to app/push-notifications/client/stylesheets/pushNotifications.css
diff --git a/app/push-notifications/client/tabBar.js b/app/push-notifications/client/tabBar.js
new file mode 100644
index 000000000000..41e728ac1a94
--- /dev/null
+++ b/app/push-notifications/client/tabBar.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { TabBar } from '../../ui-utils';
+
+Meteor.startup(function() {
+ TabBar.addButton({
+ groups: ['channel', 'group', 'direct'],
+ id: 'push-notifications',
+ i18nTitle: 'Notifications_Preferences',
+ icon: 'bell',
+ template: 'pushNotificationsFlexTab',
+ order: 100,
+ });
+});
diff --git a/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.html b/app/push-notifications/client/views/pushNotificationsFlexTab.html
similarity index 100%
rename from packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.html
rename to app/push-notifications/client/views/pushNotificationsFlexTab.html
diff --git a/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js b/app/push-notifications/client/views/pushNotificationsFlexTab.js
similarity index 96%
rename from packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
rename to app/push-notifications/client/views/pushNotificationsFlexTab.js
index 8f89fc0650dd..f255ce84e67f 100644
--- a/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
+++ b/app/push-notifications/client/views/pushNotificationsFlexTab.js
@@ -2,11 +2,11 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
-import { settings } from 'meteor/rocketchat:settings';
-import { getUserPreference, handleError, t } from 'meteor/rocketchat:utils';
-import { popover } from 'meteor/rocketchat:ui-utils';
-import { CustomSounds } from 'meteor/rocketchat:custom-sounds';
-import { ChatSubscription } from 'meteor/rocketchat:models';
+import { settings } from '../../../settings';
+import { getUserPreference, handleError, t } from '../../../utils';
+import { popover } from '../../../ui-utils';
+import { CustomSounds } from '../../../custom-sounds/client';
+import { ChatSubscription } from '../../../models';
const notificationLabels = {
all: 'All_messages',
@@ -159,8 +159,8 @@ Template.pushNotificationsFlexTab.onCreated(function() {
muteGroupMentions: new ReactiveVar(muteGroupMentions),
};
- this.saveSetting = async() => {
- Object.keys(this.original).forEach(async(field) => {
+ this.saveSetting = async () => {
+ Object.keys(this.original).forEach(async (field) => {
if (this.original[field].get() === this.form[field].get()) {
return;
}
diff --git a/app/push-notifications/server/index.js b/app/push-notifications/server/index.js
new file mode 100644
index 000000000000..c7499a51ee6a
--- /dev/null
+++ b/app/push-notifications/server/index.js
@@ -0,0 +1,6 @@
+import './methods/saveNotificationSettings';
+import PushNotification from './lib/PushNotification';
+
+export {
+ PushNotification,
+};
diff --git a/app/push-notifications/server/lib/PushNotification.js b/app/push-notifications/server/lib/PushNotification.js
new file mode 100644
index 000000000000..c8195835ff55
--- /dev/null
+++ b/app/push-notifications/server/lib/PushNotification.js
@@ -0,0 +1,58 @@
+import { Push } from 'meteor/rocketchat:push';
+import { settings } from '../../../settings';
+import { metrics } from '../../../metrics';
+import { RocketChatAssets } from '../../../assets';
+
+export class PushNotification {
+ getNotificationId(roomId) {
+ const serverId = settings.get('uniqueID');
+ return this.hash(`${ serverId }|${ roomId }`); // hash
+ }
+
+ hash(str) {
+ let hash = 0;
+ let i = str.length;
+
+ while (i) {
+ hash = ((hash << 5) - hash) + str.charCodeAt(--i);
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return hash;
+ }
+
+ send({ roomName, roomId, username, message, usersTo, payload, badge = 1, category }) {
+ let title;
+ if (roomName && roomName !== '') {
+ title = `${ roomName }`;
+ message = `${ username }: ${ message }`;
+ } else {
+ title = `${ username }`;
+ }
+ const config = {
+ from: 'push',
+ badge,
+ sound: 'default',
+ title,
+ text: message,
+ payload,
+ query: usersTo,
+ notId: this.getNotificationId(roomId),
+ gcm: {
+ style: 'inbox',
+ summaryText: '%n% new messages',
+ image: RocketChatAssets.getURL('Assets_favicon_192'),
+ },
+ };
+
+ if (category !== '') {
+ config.apn = {
+ category,
+ };
+ }
+
+ metrics.notificationsSent.inc({ notification_type: 'mobile' });
+ return Push.send(config);
+ }
+}
+
+export default new PushNotification();
diff --git a/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js b/app/push-notifications/server/methods/saveNotificationSettings.js
similarity index 97%
rename from packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js
rename to app/push-notifications/server/methods/saveNotificationSettings.js
index 653d74844a4d..d63d9af941f3 100644
--- a/packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js
+++ b/app/push-notifications/server/methods/saveNotificationSettings.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { Subscriptions } from 'meteor/rocketchat:models';
-import { getUserNotificationPreference } from 'meteor/rocketchat:utils';
+import { Subscriptions } from '../../../models';
+import { getUserNotificationPreference } from '../../../utils';
Meteor.methods({
saveNotificationSettings(roomId, field, value) {
diff --git a/packages/rocketchat-reactions/README.md b/app/reactions/README.md
similarity index 100%
rename from packages/rocketchat-reactions/README.md
rename to app/reactions/README.md
diff --git a/app/reactions/client/index.js b/app/reactions/client/index.js
new file mode 100644
index 000000000000..8d85a264dd7d
--- /dev/null
+++ b/app/reactions/client/index.js
@@ -0,0 +1,2 @@
+import './init';
+import './methods/setReaction';
diff --git a/app/reactions/client/init.js b/app/reactions/client/init.js
new file mode 100644
index 000000000000..7eb39bcd4c8a
--- /dev/null
+++ b/app/reactions/client/init.js
@@ -0,0 +1,83 @@
+import { Meteor } from 'meteor/meteor';
+import { Blaze } from 'meteor/blaze';
+import { Template } from 'meteor/templating';
+import { Rooms, Subscriptions } from '../../models';
+import { MessageAction } from '../../ui-utils';
+import { messageArgs } from '../../ui-utils/client/lib/messageArgs';
+
+import { EmojiPicker } from '../../emoji';
+import { tooltip } from '../../tooltip';
+
+Template.room.events({
+ 'click .add-reaction, click [data-message-action="reaction-message"]'(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ const data = Blaze.getData(event.currentTarget);
+ const { msg:{ rid, _id: mid } } = messageArgs(data);
+ const user = Meteor.user();
+ const room = Rooms.findOne({ _id: rid });
+
+ if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) {
+ return false;
+ }
+
+ EmojiPicker.open(event.currentTarget, (emoji) => {
+ Meteor.call('setReaction', `:${ emoji }:`, mid);
+ });
+ },
+
+ 'click .reactions > li:not(.add-reaction)'(event) {
+ event.preventDefault();
+
+ const data = Blaze.getData(event.currentTarget);
+ const { msg:{ _id: mid } } = messageArgs(data);
+ Meteor.call('setReaction', $(event.currentTarget).data('emoji'), mid, () => {
+ tooltip.hide();
+ });
+ },
+
+ 'mouseenter .reactions > li:not(.add-reaction)'(event) {
+ event.stopPropagation();
+ tooltip.showElement($(event.currentTarget).find('.people').get(0), event.currentTarget);
+ },
+
+ 'mouseleave .reactions > li:not(.add-reaction)'(event) {
+ event.stopPropagation();
+ tooltip.hide();
+ },
+});
+
+Meteor.startup(function() {
+ MessageAction.addButton({
+ id: 'reaction-message',
+ icon: 'add-reaction',
+ label: 'Reactions',
+ context: [
+ 'message',
+ 'message-mobile',
+ ],
+ action(event) {
+ event.stopPropagation();
+ const { msg } = messageArgs(this);
+ EmojiPicker.open(event.currentTarget, (emoji) => Meteor.call('setReaction', `:${ emoji }:`, msg._id));
+ },
+ condition(message) {
+ const room = Rooms.findOne({ _id: message.rid });
+ const user = Meteor.user();
+
+ if (!room) {
+ return false;
+ } else if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) {
+ return false;
+ } else if (!Subscriptions.findOne({ rid: message.rid })) {
+ return false;
+ } else if (message.private) {
+ return false;
+ }
+
+ return true;
+ },
+ order: 22,
+ group: 'message',
+ });
+});
diff --git a/app/reactions/client/methods/setReaction.js b/app/reactions/client/methods/setReaction.js
new file mode 100644
index 000000000000..e411dcac92f0
--- /dev/null
+++ b/app/reactions/client/methods/setReaction.js
@@ -0,0 +1,60 @@
+import { Meteor } from 'meteor/meteor';
+import { Messages, Rooms, Subscriptions, EmojiCustom } from '../../../models';
+import { callbacks } from '../../../callbacks';
+import { emoji } from '../../../emoji';
+import _ from 'underscore';
+
+Meteor.methods({
+ setReaction(reaction, messageId) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error(203, 'User_logged_out');
+ }
+
+ const user = Meteor.user();
+
+ const message = Messages.findOne({ _id: messageId });
+ const room = Rooms.findOne({ _id: message.rid });
+
+ if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) {
+ return false;
+ } else if (!Subscriptions.findOne({ rid: message.rid })) {
+ return false;
+ } else if (message.private) {
+ return false;
+ } else if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) {
+ return false;
+ }
+
+ if (message.reactions && message.reactions[reaction] && message.reactions[reaction].usernames.indexOf(user.username) !== -1) {
+ message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(user.username), 1);
+
+ if (message.reactions[reaction].usernames.length === 0) {
+ delete message.reactions[reaction];
+ }
+
+ if (_.isEmpty(message.reactions)) {
+ delete message.reactions;
+ Messages.unsetReactions(messageId);
+ callbacks.run('unsetReaction', messageId, reaction);
+ } else {
+ Messages.setReactions(messageId, message.reactions);
+ callbacks.run('setReaction', messageId, reaction);
+ }
+ } else {
+ if (!message.reactions) {
+ message.reactions = {};
+ }
+ if (!message.reactions[reaction]) {
+ message.reactions[reaction] = {
+ usernames: [],
+ };
+ }
+ message.reactions[reaction].usernames.push(user.username);
+
+ Messages.setReactions(messageId, message.reactions);
+ callbacks.run('setReaction', messageId, reaction);
+ }
+
+ return;
+ },
+});
diff --git a/packages/rocketchat-reactions/client/stylesheets/reaction.css b/app/reactions/client/stylesheets/reaction.css
similarity index 100%
rename from packages/rocketchat-reactions/client/stylesheets/reaction.css
rename to app/reactions/client/stylesheets/reaction.css
diff --git a/app/reactions/server/index.js b/app/reactions/server/index.js
new file mode 100644
index 000000000000..e7490bab5827
--- /dev/null
+++ b/app/reactions/server/index.js
@@ -0,0 +1 @@
+export { setReaction } from './setReaction';
diff --git a/app/reactions/server/setReaction.js b/app/reactions/server/setReaction.js
new file mode 100644
index 000000000000..38cad11bc4bf
--- /dev/null
+++ b/app/reactions/server/setReaction.js
@@ -0,0 +1,108 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Messages, EmojiCustom, Subscriptions, Rooms } from '../../models';
+import { Notifications } from '../../notifications';
+import { callbacks } from '../../callbacks';
+import { emoji } from '../../emoji';
+import { isTheLastMessage, msgStream } from '../../lib';
+import _ from 'underscore';
+
+const removeUserReaction = (message, reaction, username) => {
+ message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(username), 1);
+ if (message.reactions[reaction].usernames.length === 0) {
+ delete message.reactions[reaction];
+ }
+ return message;
+};
+
+export function setReaction(room, user, message, reaction, shouldReact) {
+ reaction = `:${ reaction.replace(/:/g, '') }:`;
+
+ if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) {
+ throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction' });
+ }
+
+ if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: room._id,
+ ts: new Date(),
+ msg: TAPi18n.__('You_have_been_muted', {}, user.language),
+ });
+ return false;
+ } else if (!Subscriptions.findOne({ rid: message.rid })) {
+ return false;
+ }
+
+ const userAlreadyReacted = Boolean(message.reactions) && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.indexOf(user.username) !== -1;
+ // When shouldReact was not informed, toggle the reaction.
+ if (shouldReact === undefined) {
+ shouldReact = !userAlreadyReacted;
+ }
+
+ if (userAlreadyReacted === shouldReact) {
+ return;
+ }
+ if (userAlreadyReacted) {
+ removeUserReaction(message, reaction, user.username);
+ if (_.isEmpty(message.reactions)) {
+ delete message.reactions;
+ if (isTheLastMessage(room, message)) {
+ Rooms.unsetReactionsInLastMessage(room._id);
+ }
+ Messages.unsetReactions(message._id);
+ } else {
+ Messages.setReactions(message._id, message.reactions);
+ if (isTheLastMessage(room, message)) {
+ Rooms.setReactionsInLastMessage(room._id, message);
+ }
+ }
+ callbacks.run('unsetReaction', message._id, reaction);
+ callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact });
+ } else {
+ if (!message.reactions) {
+ message.reactions = {};
+ }
+ if (!message.reactions[reaction]) {
+ message.reactions[reaction] = {
+ usernames: [],
+ };
+ }
+ message.reactions[reaction].usernames.push(user.username);
+ Messages.setReactions(message._id, message.reactions);
+ if (isTheLastMessage(room, message)) {
+ Rooms.setReactionsInLastMessage(room._id, message);
+ }
+ callbacks.run('setReaction', message._id, reaction);
+ callbacks.run('afterSetReaction', message, { user, reaction, shouldReact });
+ }
+
+ msgStream.emit(message.rid, message);
+}
+
+Meteor.methods({
+ setReaction(reaction, messageId, shouldReact) {
+ const user = Meteor.user();
+
+ const message = Messages.findOneById(messageId);
+
+ const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId());
+
+ if (!user) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' });
+ }
+
+ if (!message) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' });
+ }
+
+ if (!room) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' });
+ }
+
+ setReaction(room, user, message, reaction, shouldReact);
+
+ return;
+ },
+});
diff --git a/packages/rocketchat-retention-policy/README.md b/app/retention-policy/README.md
similarity index 100%
rename from packages/rocketchat-retention-policy/README.md
rename to app/retention-policy/README.md
diff --git a/app/retention-policy/index.js b/app/retention-policy/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/retention-policy/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/app/retention-policy/server/cronPruneMessages.js b/app/retention-policy/server/cronPruneMessages.js
new file mode 100644
index 000000000000..3d057a6dd128
--- /dev/null
+++ b/app/retention-policy/server/cronPruneMessages.js
@@ -0,0 +1,131 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { Rooms, Settings } from '../../models';
+import { cleanRoomHistory } from '../../lib';
+import { SyncedCron } from 'meteor/littledata:synced-cron';
+
+let types = [];
+
+const oldest = new Date('0001-01-01T00:00:00Z');
+
+let lastPrune = oldest;
+
+const maxTimes = {
+ c: 0,
+ p: 0,
+ d: 0,
+};
+const toDays = 1000 * 60 * 60 * 24;
+const gracePeriod = 5000;
+function job() {
+ const now = new Date();
+ const filesOnly = settings.get('RetentionPolicy_FilesOnly');
+ const excludePinned = settings.get('RetentionPolicy_ExcludePinned');
+ const ignoreDiscussion = settings.get('RetentionPolicy_DoNotExcludeDiscussion');
+
+ // get all rooms with default values
+ types.forEach((type) => {
+ const maxAge = maxTimes[type] || 0;
+ const latest = new Date(now.getTime() - maxAge * toDays);
+
+ Rooms.find({
+ t: type,
+ _updatedAt: { $gte: latest },
+ $or: [
+ { 'retention.enabled': { $eq: true } },
+ { 'retention.enabled': { $exists: false } },
+ ],
+ 'retention.overrideGlobal': { $ne: true },
+ }, { fields : { _id: 1 } }).forEach(({ _id: rid }) => {
+ cleanRoomHistory({ rid, latest, oldest, filesOnly, excludePinned, ignoreDiscussion });
+ });
+ });
+
+ Rooms.find({
+ 'retention.enabled': { $eq: true },
+ 'retention.overrideGlobal': { $eq: true },
+ 'retention.maxAge': { $gte: 0 },
+ _updatedAt: { $gte: lastPrune },
+ }).forEach((room) => {
+ const { maxAge = 30, filesOnly, excludePinned } = room.retention;
+ const latest = new Date(now.getTime() - maxAge * toDays);
+ cleanRoomHistory({ rid: room._id, latest, oldest, filesOnly, excludePinned, ignoreDiscussion });
+ });
+ lastPrune = new Date(now.getTime() - gracePeriod);
+}
+
+function getSchedule(precision) {
+ switch (precision) {
+ case '0':
+ return '0 */30 * * * *';
+ case '1':
+ return '0 0 * * * *';
+ case '2':
+ return '0 0 */6 * * *';
+ case '3':
+ return '0 0 0 * * *';
+ }
+}
+
+const pruneCronName = 'Prune old messages by retention policy';
+
+function deployCron(precision) {
+ const schedule = (parser) => parser.cron(getSchedule(precision), true);
+
+ SyncedCron.remove(pruneCronName);
+ SyncedCron.add({
+ name: pruneCronName,
+ schedule,
+ job,
+ });
+}
+
+function reloadPolicy() {
+ types = [];
+
+ if (settings.get('RetentionPolicy_Enabled')) {
+ if (settings.get('RetentionPolicy_AppliesToChannels')) {
+ types.push('c');
+ }
+
+ if (settings.get('RetentionPolicy_AppliesToGroups')) {
+ types.push('p');
+ }
+
+ if (settings.get('RetentionPolicy_AppliesToDMs')) {
+ types.push('d');
+ }
+
+ maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels');
+ maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups');
+ maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs');
+
+ return deployCron(settings.get('RetentionPolicy_Precision'));
+ }
+ return SyncedCron.remove(pruneCronName);
+}
+
+Meteor.startup(function() {
+ Meteor.defer(function() {
+ Settings.find({
+ _id: {
+ $in: [
+ 'RetentionPolicy_Enabled',
+ 'RetentionPolicy_Precision',
+ 'RetentionPolicy_AppliesToChannels',
+ 'RetentionPolicy_AppliesToGroups',
+ 'RetentionPolicy_AppliesToDMs',
+ 'RetentionPolicy_MaxAge_Channels',
+ 'RetentionPolicy_MaxAge_Groups',
+ 'RetentionPolicy_MaxAge_DMs',
+ ],
+ },
+ }).observe({
+ changed() {
+ reloadPolicy();
+ },
+ });
+
+ reloadPolicy();
+ });
+});
diff --git a/packages/rocketchat-retention-policy/server/index.js b/app/retention-policy/server/index.js
similarity index 100%
rename from packages/rocketchat-retention-policy/server/index.js
rename to app/retention-policy/server/index.js
diff --git a/app/retention-policy/server/startup/settings.js b/app/retention-policy/server/startup/settings.js
new file mode 100644
index 000000000000..2280709c4a42
--- /dev/null
+++ b/app/retention-policy/server/startup/settings.js
@@ -0,0 +1,109 @@
+import { settings } from '../../../settings';
+
+settings.addGroup('RetentionPolicy', function() {
+
+ this.add('RetentionPolicy_Enabled', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_Enabled',
+ alert: 'Watch out! Tweaking these settings without utmost care can destroy all message history. Please read the documentation before turning the feature on at rocket.chat/docs/administrator-guides/retention-policies/',
+ });
+
+ this.add('RetentionPolicy_Precision', '0', {
+ type: 'select',
+ values: [
+ {
+ key: '0',
+ i18nLabel: 'every_30_minutes',
+ }, {
+ key: '1',
+ i18nLabel: 'every_hour',
+ }, {
+ key: '2',
+ i18nLabel: 'every_six_hours',
+ }, {
+ key: '3',
+ i18nLabel: 'every_day',
+ },
+ ],
+ public: true,
+ i18nLabel: 'RetentionPolicy_Precision',
+ i18nDescription: 'RetentionPolicy_Precision_Description',
+ enableQuery: {
+ _id: 'RetentionPolicy_Enabled',
+ value: true,
+ },
+ });
+
+ this.section('Global Policy', function() {
+ const globalQuery = {
+ _id: 'RetentionPolicy_Enabled',
+ value: true,
+ };
+
+ this.add('RetentionPolicy_AppliesToChannels', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_AppliesToChannels',
+ enableQuery: globalQuery,
+ });
+ this.add('RetentionPolicy_MaxAge_Channels', 30, {
+ type: 'int',
+ public: true,
+ i18nLabel: 'RetentionPolicy_MaxAge_Channels',
+ i18nDescription: 'RetentionPolicy_MaxAge_Description',
+ enableQuery: [{
+ _id: 'RetentionPolicy_AppliesToChannels',
+ value: true,
+ }, globalQuery],
+ });
+
+ this.add('RetentionPolicy_AppliesToGroups', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_AppliesToGroups',
+ enableQuery: globalQuery,
+ });
+ this.add('RetentionPolicy_MaxAge_Groups', 30, {
+ type: 'int',
+ public: true,
+ i18nLabel: 'RetentionPolicy_MaxAge_Groups',
+ i18nDescription: 'RetentionPolicy_MaxAge_Description',
+ enableQuery: [{
+ _id: 'RetentionPolicy_AppliesToGroups',
+ value: true,
+ }, globalQuery],
+ });
+
+ this.add('RetentionPolicy_AppliesToDMs', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_AppliesToDMs',
+ enableQuery: globalQuery,
+ });
+ this.add('RetentionPolicy_MaxAge_DMs', 30, {
+ type: 'int',
+ public: true,
+ i18nLabel: 'RetentionPolicy_MaxAge_DMs',
+ i18nDescription: 'RetentionPolicy_MaxAge_Description',
+ enableQuery: [{
+ _id: 'RetentionPolicy_AppliesToDMs',
+ value: true,
+ }, globalQuery],
+ });
+
+ this.add('RetentionPolicy_ExcludePinned', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_ExcludePinned',
+ enableQuery: globalQuery,
+ });
+ this.add('RetentionPolicy_FilesOnly', false, {
+ type: 'boolean',
+ public: true,
+ i18nLabel: 'RetentionPolicy_FilesOnly',
+ i18nDescription: 'RetentionPolicy_FilesOnly_Description',
+ enableQuery: globalQuery,
+ });
+ });
+});
diff --git a/packages/rocketchat-search/README.md b/app/search/README.md
similarity index 100%
rename from packages/rocketchat-search/README.md
rename to app/search/README.md
diff --git a/packages/rocketchat-search/client/index.js b/app/search/client/index.js
similarity index 100%
rename from packages/rocketchat-search/client/index.js
rename to app/search/client/index.js
diff --git a/app/search/client/provider/result.html b/app/search/client/provider/result.html
new file mode 100644
index 000000000000..566738ff0c73
--- /dev/null
+++ b/app/search/client/provider/result.html
@@ -0,0 +1,31 @@
+
+
+
+ {{#if globalSearchEnabled}}
+
+ {{_ "Global_Search"}}
+
+ {{/if}}
+
+
+
+ {{#if result}}
+ {{#if $and result.message result.message.docs}}
+
+
+ {{# with messageContext}}
+ {{#each msg in result.message.docs}}{{#nrr nrrargs 'message' msg=(message msg) room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
+ {{/with}}
+
+
+ {{else}}
+
{{_ "No_results_found"}}
+ {{/if}}
+ {{/if}}
+
+ {{#if searching}}
+ {{> loading}}
+ {{/if}}
+
+
+
diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js
new file mode 100644
index 000000000000..5a477adb1792
--- /dev/null
+++ b/app/search/client/provider/result.js
@@ -0,0 +1,94 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Session } from 'meteor/session';
+import { Template } from 'meteor/templating';
+
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
+import { MessageAction, RoomHistoryManager } from '../../../ui-utils';
+
+import { messageArgs } from '../../../ui-utils/client/lib/messageArgs';
+import _ from 'underscore';
+
+Meteor.startup(function() {
+ MessageAction.addButton({
+ id: 'jump-to-search-message',
+ icon: 'jump',
+ label: 'Jump_to_message',
+ context: ['search'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ if (Session.get('openedRoom') === message.rid) {
+ return RoomHistoryManager.getSurroundingMessages(message, 50);
+ }
+
+ FlowRouter.goToRoomById(message.rid);
+ // RocketChat.MessageAction.hideDropDown();
+
+ if (window.matchMedia('(max-width: 500px)').matches) {
+ Template.instance().tabBar.close();
+ }
+
+ window.setTimeout(() => {
+ RoomHistoryManager.getSurroundingMessages(message, 50);
+ }, 400);
+ // 400ms is popular among game devs as a good delay before transition starts
+ // ie. 50, 100, 200, 400, 800 are the favored timings
+ },
+ order: 100,
+ group: 'menu',
+ });
+});
+
+Template.DefaultSearchResultTemplate.onCreated(function() {
+ const self = this;
+
+ // paging
+ this.pageSize = this.data.settings.PageSize;
+
+ // global search
+ this.globalSearchEnabled = this.data.settings.GlobalSearchEnabled;
+ this.data.parentPayload.searchAll = this.globalSearchEnabled;
+
+ this.hasMore = new ReactiveVar(true);
+
+ this.autorun(() => {
+ const result = this.data.result.get();
+ self.hasMore.set(!(result && result.message.docs.length < (self.data.payload.limit || self.pageSize)));
+ });
+});
+
+Template.DefaultSearchResultTemplate.events({
+ 'change #global-search'(e, t) {
+ t.data.parentPayload.searchAll = e.target.checked;
+ t.data.payload.limit = t.pageSize;
+ t.data.result.set(undefined);
+ t.data.search();
+
+ },
+ 'scroll .rocket-default-search-results': _.throttle(function(e, t) {
+ if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight) && t.hasMore.get()) {
+ t.data.payload.limit = (t.data.payload.limit || t.pageSize) + t.pageSize;
+ t.data.search();
+ }
+ }, 200),
+});
+
+Template.DefaultSearchResultTemplate.helpers({
+ result() {
+ return Template.instance().data.result.get();
+ },
+ globalSearchEnabled() {
+ return Template.instance().globalSearchEnabled;
+ },
+ searching() {
+ return Template.instance().data.searching.get();
+ },
+ hasMore() {
+ return Template.instance().hasMore.get();
+ },
+ message(msg) {
+ return { customClass: 'search', actionContext: 'search', ...msg };
+ },
+ messageContext,
+});
diff --git a/packages/rocketchat-search/client/provider/suggestion.html b/app/search/client/provider/suggestion.html
similarity index 100%
rename from packages/rocketchat-search/client/provider/suggestion.html
rename to app/search/client/provider/suggestion.html
diff --git a/packages/rocketchat-search/client/search/search.html b/app/search/client/search/search.html
similarity index 100%
rename from packages/rocketchat-search/client/search/search.html
rename to app/search/client/search/search.html
diff --git a/packages/rocketchat-search/client/search/search.js b/app/search/client/search/search.js
similarity index 100%
rename from packages/rocketchat-search/client/search/search.js
rename to app/search/client/search/search.js
diff --git a/packages/rocketchat-search/client/style/style.css b/app/search/client/style/style.css
similarity index 100%
rename from packages/rocketchat-search/client/style/style.css
rename to app/search/client/style/style.css
diff --git a/app/search/server/events/events.js b/app/search/server/events/events.js
new file mode 100644
index 000000000000..b739b81ddf62
--- /dev/null
+++ b/app/search/server/events/events.js
@@ -0,0 +1,65 @@
+import { callbacks } from '../../../callbacks';
+import { Users, Rooms } from '../../../models';
+import { searchProviderService } from '../service/providerService';
+import SearchLogger from '../logger/logger';
+
+class EventService {
+
+ /* eslint no-unused-vars: [2, { "args": "none" }]*/
+ _pushError(name, value, payload) {
+ // TODO implement a (performant) cache
+ SearchLogger.debug(`Error on event '${ name }' with id '${ value }'`);
+ }
+
+ promoteEvent(name, value, payload) {
+ if (!(searchProviderService.activeProvider && searchProviderService.activeProvider.on(name, value, payload))) {
+ this._pushError(name, value, payload);
+ }
+ }
+}
+
+const eventService = new EventService();
+
+/**
+ * Listen to message changes via Hooks
+ */
+callbacks.add('afterSaveMessage', function(m) {
+ eventService.promoteEvent('message.save', m._id, m);
+});
+
+callbacks.add('afterDeleteMessage', function(m) {
+ eventService.promoteEvent('message.delete', m._id);
+});
+
+/**
+ * Listen to user and room changes via cursor
+ */
+
+
+Users.on('change', ({ clientAction, id, data }) => {
+ switch (clientAction) {
+ case 'updated':
+ case 'inserted':
+ const user = data || Users.findOneById(id);
+ eventService.promoteEvent('user.save', id, user);
+ break;
+
+ case 'removed':
+ eventService.promoteEvent('user.delete', id);
+ break;
+ }
+});
+
+Rooms.on('change', ({ clientAction, id, data }) => {
+ switch (clientAction) {
+ case 'updated':
+ case 'inserted':
+ const room = data || Rooms.findOneById(id);
+ eventService.promoteEvent('room.save', id, room);
+ break;
+
+ case 'removed':
+ eventService.promoteEvent('room.delete', id);
+ break;
+ }
+});
diff --git a/app/search/server/index.js b/app/search/server/index.js
new file mode 100644
index 000000000000..12edd159209c
--- /dev/null
+++ b/app/search/server/index.js
@@ -0,0 +1,12 @@
+import SearchProvider from './model/provider';
+import { searchProviderService } from './service/providerService.js';
+import './service/validationService.js';
+import './events/events.js';
+import './provider/defaultProvider.js';
+
+
+
+export {
+ searchProviderService,
+ SearchProvider,
+};
diff --git a/app/search/server/logger/logger.js b/app/search/server/logger/logger.js
new file mode 100644
index 000000000000..86464e1c6acd
--- /dev/null
+++ b/app/search/server/logger/logger.js
@@ -0,0 +1,4 @@
+import { Logger } from '../../../logger';
+
+const SearchLogger = new Logger('Search Logger', {});
+export default SearchLogger;
diff --git a/app/search/server/model/provider.js b/app/search/server/model/provider.js
new file mode 100644
index 000000000000..6e8c8716f91a
--- /dev/null
+++ b/app/search/server/model/provider.js
@@ -0,0 +1,177 @@
+/* eslint no-unused-vars: [2, { "args": "none" }]*/
+import SearchLogger from '../logger/logger';
+import { settings } from '../../../settings';
+
+/**
+ * Setting Object in order to manage settings loading for providers and admin ui display
+ */
+class Setting {
+ constructor(basekey, key, type, defaultValue, options = {}) {
+ this._basekey = basekey;
+ this.key = key;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ this.options = options;
+ this._value = undefined;
+ }
+
+ get value() {
+ return this._value;
+ }
+
+ /**
+ * Id is generated based on baseKey and key
+ * @returns {string}
+ */
+ get id() {
+ return `Search.${ this._basekey }.${ this.key }`;
+ }
+
+ load() {
+ this._value = settings.get(this.id);
+
+ if (this._value === undefined) { this._value = this.defaultValue; }
+ }
+
+}
+
+/**
+ * Settings Object allows to manage Setting Objects
+ */
+class Settings {
+ constructor(basekey) {
+ this.basekey = basekey;
+ this.settings = {};
+ }
+
+ add(key, type, defaultValue, options) {
+ this.settings[key] = new Setting(this.basekey, key, type, defaultValue, options);
+ }
+
+ list() {
+ return Object.keys(this.settings).map((key) => this.settings[key]);
+ }
+
+ map() {
+ return this.settings;
+ }
+
+ /**
+ * return the value for key
+ * @param key
+ */
+ get(key) {
+ if (!this.settings[key]) { throw new Error('Setting is not set'); }
+ return this.settings[key].value;
+ }
+
+ /**
+ * load currently stored values of all settings
+ */
+ load() {
+ Object.keys(this.settings).forEach((key) => {
+ this.settings[key].load();
+ });
+ }
+}
+
+export default class SearchProvider {
+
+ /**
+ * Create search provider, key must match /^[a-z0-9]+$/
+ * @param key
+ */
+ constructor(key) {
+
+ if (!key.match(/^[A-z0-9]+$/)) { throw new Error(`cannot instantiate provider: ${ key } does not match key-pattern`); }
+
+ SearchLogger.info(`create search provider ${ key }`);
+
+ this._key = key;
+ this._settings = new Settings(key);
+ }
+
+ /* --- basic params ---*/
+ get key() {
+ return this._key;
+ }
+
+ get i18nLabel() {
+ return undefined;
+ }
+
+ get i18nDescription() {
+ return undefined;
+ }
+
+ get iconName() {
+ return 'magnifier';
+ }
+
+ get settings() {
+ return this._settings.list();
+ }
+
+ get settingsAsMap() {
+ return this._settings.map();
+ }
+
+ /* --- templates ---*/
+ get resultTemplate() {
+ return 'DefaultSearchResultTemplate';
+ }
+
+ get suggestionItemTemplate() {
+ return 'DefaultSuggestionItemTemplate';
+ }
+
+ /* --- search functions ---*/
+ /**
+ * Search using the current search provider and check if results are valid for the user. The search result has
+ * the format {messages:{start:0,numFound:1,docs:[{...}]},users:{...},rooms:{...}}
+ * @param text the search text
+ * @param context the context (uid, rid)
+ * @param payload custom payload (e.g. for paging)
+ * @param callback is used to return result an can be called with (error,result)
+ */
+ search(text, context, payload, callback) {
+ throw new Error('Function search has to be implemented');
+ }
+
+ /**
+ * Returns an ordered list of suggestions. The result should have at least the form [{text:string}]
+ * @param text
+ * @param context
+ * @param payload
+ * @param callback
+ */
+ suggest(text, context, payload, callback) {
+ callback(null, []);
+ }
+
+ get supportsSuggestions() {
+ return false;
+ }
+
+ /* --- triggers ---*/
+ on(name, value) {
+ return true;
+ }
+
+ /* --- livecycle ---*/
+ run(reason, callback) {
+ return new Promise((resolve, reject) => {
+ this._settings.load();
+ this.start(reason, resolve, reject);
+ });
+ }
+
+ start(reason, resolve) {
+ resolve();
+ }
+
+ stop(resolve) {
+ resolve();
+ }
+}
+
diff --git a/packages/rocketchat-search/server/provider/defaultProvider.js b/app/search/server/provider/defaultProvider.js
similarity index 100%
rename from packages/rocketchat-search/server/provider/defaultProvider.js
rename to app/search/server/provider/defaultProvider.js
diff --git a/packages/rocketchat-search/server/service/providerService.js b/app/search/server/service/providerService.js
similarity index 94%
rename from packages/rocketchat-search/server/service/providerService.js
rename to app/search/server/service/providerService.js
index 64590845ac15..04ee935f8b81 100644
--- a/packages/rocketchat-search/server/service/providerService.js
+++ b/app/search/server/service/providerService.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../settings';
import _ from 'underscore';
-import { validationService } from '../service/validationService';
+import { validationService } from './validationService';
import SearchLogger from '../logger/logger';
class SearchProviderService {
@@ -78,7 +78,7 @@ class SearchProviderService {
const { providers } = this;
// add settings for admininistration
- RocketChat.settings.addGroup('Search', function() {
+ settings.addGroup('Search', function() {
const self = this;
@@ -115,7 +115,7 @@ class SearchProviderService {
// add listener to react on setting changes
const configProvider = _.debounce(Meteor.bindEnvironment(() => {
- const providerId = RocketChat.settings.get('Search.Provider');
+ const providerId = settings.get('Search.Provider');
if (providerId) {
this.use(providerId);// TODO do something with success and errors
@@ -123,7 +123,7 @@ class SearchProviderService {
}), 1000);
- RocketChat.settings.get(/^Search\./, configProvider);
+ settings.get(/^Search\./, configProvider);
}
}
diff --git a/packages/rocketchat-search/server/service/validationService.js b/app/search/server/service/validationService.js
similarity index 93%
rename from packages/rocketchat-search/server/service/validationService.js
rename to app/search/server/service/validationService.js
index bd9b2ba4fa6f..a872eda5056b 100644
--- a/packages/rocketchat-search/server/service/validationService.js
+++ b/app/search/server/service/validationService.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import SearchLogger from '../logger/logger';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Users } from '../../../models';
class ValidationService {
constructor() {}
@@ -22,7 +22,7 @@ class ValidationService {
const getUsername = (uid) => {
if (!userCache.hasOwnProperty(uid)) {
try {
- userCache[uid] = RocketChat.models.Users.findById(uid).fetch()[0].username;
+ userCache[uid] = Users.findById(uid).fetch()[0].username;
} catch (e) {
userCache[uid] = undefined;
}
diff --git a/packages/rocketchat-settings/client/index.js b/app/settings/client/index.js
similarity index 100%
rename from packages/rocketchat-settings/client/index.js
rename to app/settings/client/index.js
diff --git a/app/settings/client/lib/settings.js b/app/settings/client/lib/settings.js
new file mode 100644
index 000000000000..3e49f44a8261
--- /dev/null
+++ b/app/settings/client/lib/settings.js
@@ -0,0 +1,47 @@
+import { Meteor } from 'meteor/meteor';
+import { ReactiveDict } from 'meteor/reactive-dict';
+import { CachedCollection } from '../../../ui-cached-collection';
+import { settings } from '../../lib/settings';
+
+settings.cachedCollection = new CachedCollection({
+ name: 'public-settings',
+ eventType: 'onAll',
+ userRelated: false,
+ listenChangesForLoggedUsersOnly: true,
+});
+
+settings.collection = settings.cachedCollection.collection;
+
+settings.cachedCollection.init();
+
+settings.dict = new ReactiveDict('settings');
+
+settings.get = function(_id) {
+ return settings.dict.get(_id);
+};
+
+settings.init = function() {
+ let initialLoad = true;
+ settings.collection.find().observe({
+ added(record) {
+ Meteor.settings[record._id] = record.value;
+ settings.dict.set(record._id, record.value);
+ settings.load(record._id, record.value, initialLoad);
+ },
+ changed(record) {
+ Meteor.settings[record._id] = record.value;
+ settings.dict.set(record._id, record.value);
+ settings.load(record._id, record.value, initialLoad);
+ },
+ removed(record) {
+ delete Meteor.settings[record._id];
+ settings.dict.set(record._id, null);
+ settings.load(record._id, null, initialLoad);
+ },
+ });
+ initialLoad = false;
+};
+
+settings.init();
+
+export { settings };
diff --git a/app/settings/index.js b/app/settings/index.js
new file mode 100644
index 000000000000..a67eca871efb
--- /dev/null
+++ b/app/settings/index.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+if (Meteor.isClient) {
+ module.exports = require('./client/index.js');
+}
+if (Meteor.isServer) {
+ module.exports = require('./server/index.js');
+}
diff --git a/packages/rocketchat-settings/lib/settings.js b/app/settings/lib/settings.js
similarity index 100%
rename from packages/rocketchat-settings/lib/settings.js
rename to app/settings/lib/settings.js
diff --git a/app/settings/server/functions/settings.js b/app/settings/server/functions/settings.js
new file mode 100644
index 000000000000..a0a53a69ceba
--- /dev/null
+++ b/app/settings/server/functions/settings.js
@@ -0,0 +1,301 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../lib/settings';
+import Settings from '../../../models/server/models/Settings';
+import _ from 'underscore';
+
+const blockedSettings = {};
+
+if (process.env.SETTINGS_BLOCKED) {
+ process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings[settingId] = 1);
+}
+
+const hiddenSettings = {};
+if (process.env.SETTINGS_HIDDEN) {
+ process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings[settingId] = 1);
+}
+
+settings._sorter = {};
+
+
+/*
+* Add a setting
+* @param {String} _id
+* @param {Mixed} value
+* @param {Object} setting
+*/
+
+settings.add = function(_id, value, options = {}) {
+ if (options == null) {
+ options = {};
+ }
+ if (!_id || value == null) {
+ return false;
+ }
+ if (settings._sorter[options.group] == null) {
+ settings._sorter[options.group] = 0;
+ }
+ options.packageValue = value;
+ options.valueSource = 'packageValue';
+ options.hidden = options.hidden || false;
+ options.blocked = options.blocked || false;
+ if (options.sorter == null) {
+ options.sorter = settings._sorter[options.group]++;
+ }
+ if (options.enableQuery != null) {
+ options.enableQuery = JSON.stringify(options.enableQuery);
+ }
+ if (options.i18nDefaultQuery != null) {
+ options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery);
+ }
+ if (typeof process !== 'undefined' && process.env && process.env[_id]) {
+ value = process.env[_id];
+ if (value.toLowerCase() === 'true') {
+ value = true;
+ } else if (value.toLowerCase() === 'false') {
+ value = false;
+ } else if (options.type === 'int') {
+ value = parseInt(value);
+ }
+ options.processEnvValue = value;
+ options.valueSource = 'processEnvValue';
+ } else if (Meteor.settings && typeof Meteor.settings[_id] !== 'undefined') {
+ if (Meteor.settings[_id] == null) {
+ return false;
+ }
+
+ value = Meteor.settings[_id];
+ options.meteorSettingsValue = value;
+ options.valueSource = 'meteorSettingsValue';
+ }
+ if (options.i18nLabel == null) {
+ options.i18nLabel = _id;
+ }
+ if (options.i18nDescription == null) {
+ options.i18nDescription = `${ _id }_Description`;
+ }
+ if (blockedSettings[_id] != null) {
+ options.blocked = true;
+ }
+ if (hiddenSettings[_id] != null) {
+ options.hidden = true;
+ }
+ if (options.autocomplete == null) {
+ options.autocomplete = true;
+ }
+ if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) {
+ let value = process.env[`OVERWRITE_SETTING_${ _id }`];
+ if (value.toLowerCase() === 'true') {
+ value = true;
+ } else if (value.toLowerCase() === 'false') {
+ value = false;
+ } else if (options.type === 'int') {
+ value = parseInt(value);
+ }
+ options.value = value;
+ options.processEnvValue = value;
+ options.valueSource = 'processEnvValue';
+ }
+ const updateOperations = {
+ $set: options,
+ $setOnInsert: {
+ createdAt: new Date,
+ },
+ };
+ if (options.editor != null) {
+ updateOperations.$setOnInsert.editor = options.editor;
+ delete options.editor;
+ }
+ if (options.value == null) {
+ if (options.force === true) {
+ updateOperations.$set.value = options.packageValue;
+ } else {
+ updateOperations.$setOnInsert.value = value;
+ }
+ }
+ const query = _.extend({
+ _id,
+ }, updateOperations.$set);
+ if (options.section == null) {
+ updateOperations.$unset = {
+ section: 1,
+ };
+ query.section = {
+ $exists: false,
+ };
+ }
+ const existantSetting = Settings.db.findOne(query);
+ if (existantSetting != null) {
+ if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) {
+ updateOperations.$set.editor = updateOperations.$setOnInsert.editor;
+ delete updateOperations.$setOnInsert.editor;
+ }
+ } else {
+ updateOperations.$set.ts = new Date;
+ }
+ return Settings.upsert({
+ _id,
+ }, updateOperations);
+};
+
+
+/*
+* Add a setting group
+* @param {String} _id
+*/
+
+settings.addGroup = function(_id, options = {}, cb) {
+ if (!_id) {
+ return false;
+ }
+ if (_.isFunction(options)) {
+ cb = options;
+ options = {};
+ }
+ if (options.i18nLabel == null) {
+ options.i18nLabel = _id;
+ }
+ if (options.i18nDescription == null) {
+ options.i18nDescription = `${ _id }_Description`;
+ }
+ options.ts = new Date;
+ options.blocked = false;
+ options.hidden = false;
+ if (blockedSettings[_id] != null) {
+ options.blocked = true;
+ }
+ if (hiddenSettings[_id] != null) {
+ options.hidden = true;
+ }
+ Settings.upsert({
+ _id,
+ }, {
+ $set: options,
+ $setOnInsert: {
+ type: 'group',
+ createdAt: new Date,
+ },
+ });
+ if (cb != null) {
+ cb.call({
+ add(id, value, options) {
+ if (options == null) {
+ options = {};
+ }
+ options.group = _id;
+ return settings.add(id, value, options);
+ },
+ section(section, cb) {
+ return cb.call({
+ add(id, value, options) {
+ if (options == null) {
+ options = {};
+ }
+ options.group = _id;
+ options.section = section;
+ return settings.add(id, value, options);
+ },
+ });
+ },
+ });
+ }
+};
+
+
+/*
+* Remove a setting by id
+* @param {String} _id
+*/
+
+settings.removeById = function(_id) {
+ if (!_id) {
+ return false;
+ }
+ return Settings.removeById(_id);
+};
+
+
+/*
+* Update a setting by id
+* @param {String} _id
+*/
+
+settings.updateById = function(_id, value, editor) {
+ if (!_id || value == null) {
+ return false;
+ }
+ if (editor != null) {
+ return Settings.updateValueAndEditorById(_id, value, editor);
+ }
+ return Settings.updateValueById(_id, value);
+};
+
+
+/*
+* Update options of a setting by id
+* @param {String} _id
+*/
+
+settings.updateOptionsById = function(_id, options) {
+ if (!_id || options == null) {
+ return false;
+ }
+ return Settings.updateOptionsById(_id, options);
+};
+
+
+/*
+* Update a setting by id
+* @param {String} _id
+*/
+
+settings.clearById = function(_id) {
+ if (_id == null) {
+ return false;
+ }
+ return Settings.updateValueById(_id, undefined);
+};
+
+
+/*
+* Update a setting by id
+*/
+
+settings.init = function() {
+ settings.initialLoad = true;
+ Settings.find().observe({
+ added(record) {
+ Meteor.settings[record._id] = record.value;
+ if (record.env === true) {
+ process.env[record._id] = record.value;
+ }
+ return settings.load(record._id, record.value, settings.initialLoad);
+ },
+ changed(record) {
+ Meteor.settings[record._id] = record.value;
+ if (record.env === true) {
+ process.env[record._id] = record.value;
+ }
+ return settings.load(record._id, record.value, settings.initialLoad);
+ },
+ removed(record) {
+ delete Meteor.settings[record._id];
+ if (record.env === true) {
+ delete process.env[record._id];
+ }
+ return settings.load(record._id, undefined, settings.initialLoad);
+ },
+ });
+ settings.initialLoad = false;
+ settings.afterInitialLoad.forEach((fn) => fn(Meteor.settings));
+};
+
+settings.afterInitialLoad = [];
+
+settings.onAfterInitialLoad = function(fn) {
+ settings.afterInitialLoad.push(fn);
+ if (settings.initialLoad === false) {
+ return fn(Meteor.settings);
+ }
+};
+
+export { settings };
diff --git a/packages/rocketchat-settings/server/index.js b/app/settings/server/index.js
similarity index 100%
rename from packages/rocketchat-settings/server/index.js
rename to app/settings/server/index.js
diff --git a/packages/rocketchat-setup-wizard/client/final.html b/app/setup-wizard/client/final.html
similarity index 100%
rename from packages/rocketchat-setup-wizard/client/final.html
rename to app/setup-wizard/client/final.html
diff --git a/app/setup-wizard/client/final.js b/app/setup-wizard/client/final.js
new file mode 100644
index 000000000000..a0de3155524c
--- /dev/null
+++ b/app/setup-wizard/client/final.js
@@ -0,0 +1,58 @@
+import { Meteor } from 'meteor/meteor';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Template } from 'meteor/templating';
+import { settings } from '../../settings';
+import { Users } from '../../models';
+import { hasRole } from '../../authorization';
+
+Template.setupWizardFinal.onCreated(function() {
+ const isSetupWizardDone = localStorage.getItem('wizardFinal');
+ if (isSetupWizardDone === null) {
+ FlowRouter.go('setup-wizard');
+ }
+
+ this.autorun((c) => {
+ const showSetupWizard = settings.get('Show_Setup_Wizard');
+ if (!showSetupWizard) {
+ // Setup Wizard state is not defined yet
+ return;
+ }
+
+ const userId = Meteor.userId();
+ const user = userId && Users.findOne(userId, { fields: { status: true } });
+ if (userId && (!user || !user.status)) {
+ // User and its status are not defined yet
+ return;
+ }
+
+ c.stop();
+
+ const isComplete = showSetupWizard === 'completed';
+ const noUserLoggedInAndIsNotPending = !userId && showSetupWizard !== 'pending';
+ const userIsLoggedButIsNotAdmin = userId && !hasRole(userId, 'admin');
+ if (isComplete || noUserLoggedInAndIsNotPending || userIsLoggedButIsNotAdmin) {
+ FlowRouter.go('home');
+ return;
+ }
+ });
+});
+
+Template.setupWizardFinal.onRendered(function() {
+ $('#initial-page-loading').remove();
+});
+
+Template.setupWizardFinal.events({
+ 'click .js-finish'() {
+ settings.set('Show_Setup_Wizard', 'completed', function() {
+ localStorage.removeItem('wizard');
+ localStorage.removeItem('wizardFinal');
+ FlowRouter.go('home');
+ });
+ },
+});
+
+Template.setupWizardFinal.helpers({
+ siteUrl() {
+ return settings.get('Site_Url');
+ },
+});
diff --git a/packages/rocketchat-setup-wizard/client/index.js b/app/setup-wizard/client/index.js
similarity index 100%
rename from packages/rocketchat-setup-wizard/client/index.js
rename to app/setup-wizard/client/index.js
diff --git a/packages/rocketchat-setup-wizard/client/setupWizard.html b/app/setup-wizard/client/setupWizard.html
similarity index 100%
rename from packages/rocketchat-setup-wizard/client/setupWizard.html
rename to app/setup-wizard/client/setupWizard.html
diff --git a/packages/rocketchat-setup-wizard/client/setupWizard.js b/app/setup-wizard/client/setupWizard.js
similarity index 89%
rename from packages/rocketchat-setup-wizard/client/setupWizard.js
rename to app/setup-wizard/client/setupWizard.js
index d79dd8288f50..0d6d8dd1e32b 100644
--- a/packages/rocketchat-setup-wizard/client/setupWizard.js
+++ b/app/setup-wizard/client/setupWizard.js
@@ -5,19 +5,22 @@ import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat, handleError } from 'meteor/rocketchat:lib';
-import { t } from 'meteor/rocketchat:utils';
+import { settings } from '../../settings';
+import { callbacks } from '../../callbacks';
+import { hasRole } from '../../authorization';
+import { Users } from '../../models';
+import { t, handleError } from '../../utils';
import toastr from 'toastr';
const cannotSetup = () => {
- const showSetupWizard = RocketChat.settings.get('Show_Setup_Wizard');
+ const showSetupWizard = settings.get('Show_Setup_Wizard');
if (!showSetupWizard) {
// Setup Wizard state is not defined yet
return;
}
const userId = Meteor.userId();
- const user = userId && RocketChat.models.Users.findOne(userId, { fields: { status: true } });
+ const user = userId && Users.findOne(userId, { fields: { status: true } });
if (userId && (!user || !user.status)) {
// User and its status are not defined yet
return;
@@ -25,7 +28,7 @@ const cannotSetup = () => {
const isComplete = showSetupWizard === 'completed';
const noUserLoggedInAndIsNotPending = !userId && showSetupWizard !== 'pending';
- const userIsLoggedButIsNotAdmin = userId && !RocketChat.authz.hasRole(userId, 'admin');
+ const userIsLoggedButIsNotAdmin = userId && !hasRole(userId, 'admin');
return isComplete || noUserLoggedInAndIsNotPending || userIsLoggedButIsNotAdmin;
};
@@ -41,7 +44,7 @@ const registerAdminUser = (state, callback) => {
return handleError(error);
}
- RocketChat.callbacks.run('userRegistered');
+ callbacks.run('userRegistered');
Meteor.loginWithPassword(registrationData.email, registrationData.pass, (error) => {
if (error) {
if (error.error === 'error-invalid-email') {
@@ -58,7 +61,7 @@ const registerAdminUser = (state, callback) => {
return handleError(error);
}
- RocketChat.callbacks.run('usernameSet');
+ callbacks.run('usernameSet');
callback && callback();
});
});
@@ -66,7 +69,7 @@ const registerAdminUser = (state, callback) => {
};
const persistSettings = (state, callback) => {
- const settings = Object.entries(state)
+ const setupSettings = Object.entries(state)
.filter(([key]) => !/registration-|registerServer|optIn|currentStep|invalidUsername|invalidEmail/.test(key))
.map(([_id, value]) => ({ _id, value }))
.concat([
@@ -88,7 +91,7 @@ const persistSettings = (state, callback) => {
},
]);
- RocketChat.settings.batchSet(settings, (error) => {
+ settings.batchSet(setupSettings, (error) => {
if (error) {
return handleError(error);
}
@@ -164,7 +167,7 @@ Template.setupWizard.events({
switch (t.state.get('currentStep')) {
case 1: {
const usernameValue = t.state.get('registration-username');
- const usernameRegex = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`);
+ const usernameRegex = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
t.state.set('invalidUsername', !usernameRegex.test(usernameValue));
const emailValue = t.state.get('registration-email');
@@ -190,7 +193,19 @@ Template.setupWizard.events({
persistSettings(t.state.all(), () => {
localStorage.removeItem('wizard');
localStorage.setItem('wizardFinal', true);
- FlowRouter.go('setup-wizard-final');
+
+ if (t.state.get('registerServer')) {
+ Meteor.call('cloud:registerWorkspace', (error) => {
+ if (error) {
+ console.warn(error);
+ return;
+ }
+
+ FlowRouter.go('setup-wizard-final');
+ });
+ } else {
+ FlowRouter.go('setup-wizard-final');
+ }
});
return false;
}
@@ -256,7 +271,7 @@ Template.setupWizard.helpers({
formLoadStateClass() {
switch (Template.instance().state.get('currentStep')) {
case 1:
- return RocketChat.settings.get('Show_Setup_Wizard') === 'pending' && 'setup-wizard-forms__box--loaded';
+ return settings.get('Show_Setup_Wizard') === 'pending' && 'setup-wizard-forms__box--loaded';
case 2:
case 3:
return Template.instance().wizardSettings.get().length > 0 && 'setup-wizard-forms__box--loaded';
diff --git a/app/setup-wizard/server/getSetupWizardParameters.js b/app/setup-wizard/server/getSetupWizardParameters.js
new file mode 100644
index 000000000000..9124d6ed16d6
--- /dev/null
+++ b/app/setup-wizard/server/getSetupWizardParameters.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { hasRole } from '../../authorization';
+import { Settings } from '../../models';
+
+Meteor.methods({
+ getSetupWizardParameters() {
+ const userId = Meteor.userId();
+ const userHasAdminRole = userId && hasRole(userId, 'admin');
+
+ if (!userHasAdminRole) {
+ throw new Meteor.Error('error-not-allowed');
+ }
+
+ const settings = Settings.findSetupWizardSettings().fetch();
+ const allowStandaloneServer = process.env.DEPLOY_PLATFORM !== 'rocket-cloud';
+
+ return {
+ settings,
+ allowStandaloneServer,
+ };
+ },
+});
diff --git a/packages/rocketchat-setup-wizard/server/index.js b/app/setup-wizard/server/index.js
similarity index 100%
rename from packages/rocketchat-setup-wizard/server/index.js
rename to app/setup-wizard/server/index.js
diff --git a/packages/rocketchat-slackbridge/README.md b/app/slackbridge/README.md
similarity index 100%
rename from packages/rocketchat-slackbridge/README.md
rename to app/slackbridge/README.md
diff --git a/packages/rocketchat-slackbridge/client/index.js b/app/slackbridge/client/index.js
similarity index 100%
rename from packages/rocketchat-slackbridge/client/index.js
rename to app/slackbridge/client/index.js
diff --git a/app/slackbridge/client/slackbridge_import.client.js b/app/slackbridge/client/slackbridge_import.client.js
new file mode 100644
index 000000000000..1441701f92d6
--- /dev/null
+++ b/app/slackbridge/client/slackbridge_import.client.js
@@ -0,0 +1,12 @@
+import { settings } from '../../settings';
+import { slashCommands } from '../../utils';
+
+settings.onload('SlackBridge_Enabled', (key, value) => {
+ if (value) {
+ slashCommands.add('slackbridge-import', null, {
+ description: 'Import_old_messages_from_slackbridge',
+ });
+ } else {
+ delete slashCommands.commands['slackbridge-import'];
+ }
+});
diff --git a/app/slackbridge/server/RocketAdapter.js b/app/slackbridge/server/RocketAdapter.js
new file mode 100644
index 000000000000..b51e2cf9f950
--- /dev/null
+++ b/app/slackbridge/server/RocketAdapter.js
@@ -0,0 +1,526 @@
+import _ from 'underscore';
+import util from 'util';
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+import { Random } from 'meteor/random';
+import { callbacks } from '../../callbacks';
+import { settings } from '../../settings';
+import { Messages, Rooms, Users } from '../../models';
+import { createRoom, sendMessage, setUserAvatar } from '../../lib';
+import { logger } from './logger';
+
+export default class RocketAdapter {
+ constructor(slackBridge) {
+ logger.rocket.debug('constructor');
+ this.slackBridge = slackBridge;
+ this.util = util;
+ this.userTags = {};
+ this.slackAdapters = [];
+ }
+
+ connect() {
+ this.registerForEvents();
+ }
+
+ disconnect() {
+ this.unregisterForEvents();
+ }
+
+ addSlack(slack) {
+ if (this.slackAdapters.indexOf(slack) < 0) {
+ this.slackAdapters.push(slack);
+ }
+ }
+
+ clearSlackAdapters() {
+ this.slackAdapters = [];
+ }
+
+ registerForEvents() {
+ logger.rocket.debug('Register for events');
+ callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out');
+ callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete');
+ callbacks.add('setReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction');
+ callbacks.add('unsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction');
+ }
+
+ unregisterForEvents() {
+ logger.rocket.debug('Unregister for events');
+ callbacks.remove('afterSaveMessage', 'SlackBridge_Out');
+ callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete');
+ callbacks.remove('setReaction', 'SlackBridge_SetReaction');
+ callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction');
+ }
+
+ onMessageDelete(rocketMessageDeleted) {
+ this.slackAdapters.forEach((slack) => {
+ try {
+ if (!slack.getSlackChannel(rocketMessageDeleted.rid)) {
+ // This is on a channel that the rocket bot is not subscribed on this slack server
+ return;
+ }
+ logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted);
+ slack.postDeleteMessage(rocketMessageDeleted);
+ } catch (err) {
+ logger.rocket.error('Unhandled error onMessageDelete', err);
+ }
+ });
+ }
+
+ onSetReaction(rocketMsgID, reaction) {
+ try {
+ if (!this.slackBridge.isReactionsEnabled) {
+ return;
+ }
+
+ logger.rocket.debug('onRocketSetReaction');
+
+ if (rocketMsgID && reaction) {
+ if (this.slackBridge.reactionsMap.delete(`set${ rocketMsgID }${ reaction }`)) {
+ // This was a Slack reaction, we don't need to tell Slack about it
+ return;
+ }
+ const rocketMsg = Messages.findOneById(rocketMsgID);
+ if (rocketMsg) {
+ this.slackAdapters.forEach((slack) => {
+ const slackChannel = slack.getSlackChannel(rocketMsg.rid);
+ if (null != slackChannel) {
+ const slackTS = slack.getTimeStamp(rocketMsg);
+ slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
+ }
+ });
+ }
+ }
+ } catch (err) {
+ logger.rocket.error('Unhandled error onSetReaction', err);
+ }
+ }
+
+ onUnSetReaction(rocketMsgID, reaction) {
+ try {
+ if (!this.slackBridge.isReactionsEnabled) {
+ return;
+ }
+
+ logger.rocket.debug('onRocketUnSetReaction');
+
+ if (rocketMsgID && reaction) {
+ if (this.slackBridge.reactionsMap.delete(`unset${ rocketMsgID }${ reaction }`)) {
+ // This was a Slack unset reaction, we don't need to tell Slack about it
+ return;
+ }
+
+ const rocketMsg = Messages.findOneById(rocketMsgID);
+ if (rocketMsg) {
+ this.slackAdapters.forEach((slack) => {
+ const slackChannel = slack.getSlackChannel(rocketMsg.rid);
+ if (slackChannel != null) {
+ const slackTS = slack.getTimeStamp(rocketMsg);
+ slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
+ }
+ });
+ }
+ }
+ } catch (err) {
+ logger.rocket.error('Unhandled error onUnSetReaction', err);
+ }
+ }
+
+ onMessage(rocketMessage) {
+ this.slackAdapters.forEach((slack) => {
+ try {
+ if (!slack.getSlackChannel(rocketMessage.rid)) {
+ // This is on a channel that the rocket bot is not subscribed
+ return;
+ }
+ logger.rocket.debug('onRocketMessage', rocketMessage);
+
+ if (rocketMessage.editedAt) {
+ // This is an Edit Event
+ this.processMessageChanged(rocketMessage, slack);
+ return rocketMessage;
+ }
+ // Ignore messages originating from Slack
+ if (rocketMessage._id.indexOf('slack-') === 0) {
+ return rocketMessage;
+ }
+
+ if (rocketMessage.file) {
+ return this.processFileShare(rocketMessage, slack);
+ }
+
+ // A new message from Rocket.Chat
+ this.processSendMessage(rocketMessage, slack);
+
+ } catch (err) {
+ logger.rocket.error('Unhandled error onMessage', err);
+ }
+ });
+
+ return rocketMessage;
+ }
+
+ processSendMessage(rocketMessage, slack) {
+ // Since we got this message, SlackBridge_Out_Enabled is true
+ if (settings.get('SlackBridge_Out_All') === true) {
+ slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
+ } else {
+ // They want to limit to certain groups
+ const outSlackChannels = _.pluck(settings.get('SlackBridge_Out_Channels'), '_id') || [];
+ // logger.rocket.debug('Out SlackChannels: ', outSlackChannels);
+ if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) {
+ slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
+ }
+ }
+ }
+
+ getMessageAttachment(rocketMessage) {
+ if (!rocketMessage.file) {
+ return;
+ }
+
+ if (!rocketMessage.attachments || !rocketMessage.attachments.length) {
+ return;
+ }
+
+ const fileId = rocketMessage.file._id;
+ return rocketMessage.attachments.find((attachment) => attachment.title_link && attachment.title_link.indexOf(`/${ fileId }/`) >= 0);
+ }
+
+ processFileShare(rocketMessage, slack) {
+ if (!settings.get('SlackBridge_FileUpload_Enabled')) {
+ return;
+ }
+
+ if (rocketMessage.file.name) {
+ let fileName = rocketMessage.file.name;
+ let text = rocketMessage.msg;
+
+ const attachment = this.getMessageAttachment(rocketMessage);
+ if (attachment) {
+ fileName = Meteor.absoluteUrl(attachment.title_link);
+ if (!text) {
+ text = attachment.description;
+ }
+ }
+
+ const message = `${ text } ${ fileName }`;
+
+ rocketMessage.msg = message;
+ slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
+ }
+ }
+
+ processMessageChanged(rocketMessage, slack) {
+ if (rocketMessage) {
+ if (rocketMessage.updatedBySlack) {
+ // We have already processed this
+ delete rocketMessage.updatedBySlack;
+ return;
+ }
+
+ // This was a change from Rocket.Chat
+ const slackChannel = slack.getSlackChannel(rocketMessage.rid);
+ slack.postMessageUpdate(slackChannel, rocketMessage);
+ }
+ }
+
+ getChannel(slackMessage) {
+ return slackMessage.channel ? this.findChannel(slackMessage.channel) || this.addChannel(slackMessage.channel) : null;
+ }
+
+ getUser(slackUser) {
+ return slackUser ? this.findUser(slackUser) || this.addUser(slackUser) : null;
+ }
+
+ createRocketID(slackChannel, ts) {
+ return `slack-${ slackChannel }-${ ts.replace(/\./g, '-') }`;
+ }
+
+ findChannel(slackChannelId) {
+ return Rooms.findOneByImportId(slackChannelId);
+ }
+
+ getRocketUsers(members, slackChannel) {
+ const rocketUsers = [];
+ for (const member of members) {
+ if (member !== slackChannel.creator) {
+ const rocketUser = this.findUser(member) || this.addUser(member);
+ if (rocketUser && rocketUser.username) {
+ rocketUsers.push(rocketUser.username);
+ }
+ }
+ }
+ return rocketUsers;
+ }
+
+ getRocketUserCreator(slackChannel) {
+ return slackChannel.creator ? this.findUser(slackChannel.creator) || this.addUser(slackChannel.creator) : null;
+ }
+
+ addChannel(slackChannelID, hasRetried = false) {
+ logger.rocket.debug('Adding Rocket.Chat channel from Slack', slackChannelID);
+ let addedRoom;
+
+ this.slackAdapters.forEach((slack) => {
+ if (addedRoom) {
+ return;
+ }
+
+ const slackChannel = slack.slackAPI.getRoomInfo(slackChannelID);
+ if (slackChannel) {
+ const members = slack.slackAPI.getMembers(slackChannelID);
+ if (!members) {
+ logger.rocket.error('Could not fetch room members');
+ return;
+ }
+
+ const rocketRoom = Rooms.findOneByName(slackChannel.name);
+
+ if (rocketRoom || slackChannel.is_general) {
+ slackChannel.rocketId = slackChannel.is_general ? 'GENERAL' : rocketRoom._id;
+ Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
+ } else {
+ const rocketUsers = this.getRocketUsers(members, slackChannel);
+ const rocketUserCreator = this.getRocketUserCreator(slackChannel);
+
+ if (!rocketUserCreator) {
+ logger.rocket.error('Could not fetch room creator information', slackChannel.creator);
+ return;
+ }
+
+ try {
+ const isPrivate = slackChannel.is_private;
+ const rocketChannel = createRoom(isPrivate ? 'p' : 'c', slackChannel.name, slackChannel.username, rocketUsers);
+ rocketChannel.rocketId = rocketChannel.rid;
+ } catch (e) {
+ if (!hasRetried) {
+ logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message);
+ // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s.
+ Meteor._sleepForMs(1000);
+ return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true);
+ } else {
+ console.log(e.message);
+ }
+ }
+
+ const roomUpdate = {
+ ts: new Date(slackChannel.created * 1000),
+ };
+
+ let lastSetTopic = 0;
+ if (slackChannel.topic && slackChannel.topic.value) {
+ roomUpdate.topic = slackChannel.topic.value;
+ lastSetTopic = slackChannel.topic.last_set;
+ }
+
+ if (slackChannel.purpose && slackChannel.purpose.value && slackChannel.purpose.last_set > lastSetTopic) {
+ roomUpdate.topic = slackChannel.purpose.value;
+ }
+
+ Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
+ slack.addSlackChannel(slackChannel.rocketId, slackChannelID);
+ }
+
+ addedRoom = Rooms.findOneById(slackChannel.rocketId);
+ }
+ });
+
+ if (!addedRoom) {
+ logger.rocket.debug('Channel not added');
+ }
+ return addedRoom;
+ }
+
+ findUser(slackUserID) {
+ const rocketUser = Users.findOneByImportId(slackUserID);
+ if (rocketUser && !this.userTags[slackUserID]) {
+ this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUser.username }` };
+ }
+ return rocketUser;
+ }
+
+ addUser(slackUserID) {
+ logger.rocket.debug('Adding Rocket.Chat user from Slack', slackUserID);
+ let addedUser;
+ this.slackAdapters.forEach((slack) => {
+ if (addedUser) {
+ return;
+ }
+
+ const user = slack.slackAPI.getUser(slackUserID);
+ if (user) {
+ const rocketUserData = user;
+ const isBot = rocketUserData.is_bot === true;
+ const email = (rocketUserData.profile && rocketUserData.profile.email) || '';
+ let existingRocketUser;
+ if (!isBot) {
+ existingRocketUser = Users.findOneByEmailAddress(email) || Users.findOneByUsername(rocketUserData.name);
+ } else {
+ existingRocketUser = Users.findOneByUsername(rocketUserData.name);
+ }
+
+ if (existingRocketUser) {
+ rocketUserData.rocketId = existingRocketUser._id;
+ rocketUserData.name = existingRocketUser.username;
+ } else {
+ const newUser = {
+ password: Random.id(),
+ username: rocketUserData.name,
+ };
+
+ if (!isBot && email) {
+ newUser.email = email;
+ }
+
+ if (isBot) {
+ newUser.joinDefaultChannels = false;
+ }
+
+ rocketUserData.rocketId = Accounts.createUser(newUser);
+ const userUpdate = {
+ utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600,
+ roles: isBot ? ['bot'] : ['user'],
+ };
+
+ if (rocketUserData.profile && rocketUserData.profile.real_name) {
+ userUpdate.name = rocketUserData.profile.real_name;
+ }
+
+ if (rocketUserData.deleted) {
+ userUpdate.active = false;
+ userUpdate['services.resume.loginTokens'] = [];
+ }
+
+ Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate });
+
+ const user = Users.findOneById(rocketUserData.rocketId);
+
+ let url = null;
+ if (rocketUserData.profile) {
+ if (rocketUserData.profile.image_original) {
+ url = rocketUserData.profile.image_original;
+ } else if (rocketUserData.profile.image_512) {
+ url = rocketUserData.profile.image_512;
+ }
+ }
+ if (url) {
+ try {
+ setUserAvatar(user, url, null, 'url');
+ } catch (error) {
+ logger.rocket.debug('Error setting user avatar', error.message);
+ }
+ }
+ }
+
+ const importIds = [rocketUserData.id];
+ if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) {
+ importIds.push(rocketUserData.profile.bot_id);
+ }
+ Users.addImportIds(rocketUserData.rocketId, importIds);
+ if (!this.userTags[slackUserID]) {
+ this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUserData.name }` };
+ }
+ addedUser = Users.findOneById(rocketUserData.rocketId);
+ }
+ });
+
+ if (!addedUser) {
+ logger.rocket.debug('User not added');
+ }
+
+ return addedUser;
+ }
+
+ addAliasToMsg(rocketUserName, rocketMsgObj) {
+ const aliasFormat = settings.get('SlackBridge_AliasFormat');
+ if (aliasFormat) {
+ const alias = this.util.format(aliasFormat, rocketUserName);
+
+ if (alias !== rocketUserName) {
+ rocketMsgObj.alias = alias;
+ }
+ }
+
+ return rocketMsgObj;
+ }
+
+ createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting, slack) {
+ if (slackMessage.type === 'message') {
+ let rocketMsgObj = {};
+ if (!_.isEmpty(slackMessage.subtype)) {
+ rocketMsgObj = slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting);
+ if (!rocketMsgObj) {
+ return;
+ }
+ } else {
+ rocketMsgObj = {
+ msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text),
+ rid: rocketChannel._id,
+ u: {
+ _id: rocketUser._id,
+ username: rocketUser.username,
+ },
+ };
+
+ this.addAliasToMsg(rocketUser.username, rocketMsgObj);
+ }
+ _.extend(rocketMsgObj, rocketMsgDataDefaults);
+ if (slackMessage.edited) {
+ rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000);
+ }
+ if (slackMessage.subtype === 'bot_message') {
+ rocketUser = Users.findOneById('rocket.cat', { fields: { username: 1 } });
+ }
+
+ if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) {
+ rocketMsgObj.pinned = true;
+ rocketMsgObj.pinnedAt = Date.now;
+ rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username');
+ }
+ if (slackMessage.subtype === 'bot_message') {
+ Meteor.setTimeout(() => {
+ if (slackMessage.bot_id && slackMessage.ts) {
+ // Make sure that a message with the same bot_id and timestamp doesn't already exists
+ if (!Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) {
+ sendMessage(rocketUser, rocketMsgObj, rocketChannel, true);
+ }
+ }
+ }, 500);
+ } else {
+ logger.rocket.debug('Send message to Rocket.Chat');
+ sendMessage(rocketUser, rocketMsgObj, rocketChannel, true);
+ }
+ }
+ }
+
+ convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) {
+ if (!_.isEmpty(slackMsgTxt)) {
+ slackMsgTxt = slackMsgTxt.replace(//g, '@all');
+ slackMsgTxt = slackMsgTxt.replace(//g, '@all');
+ slackMsgTxt = slackMsgTxt.replace(//g, '@here');
+ slackMsgTxt = slackMsgTxt.replace(/>/g, '>');
+ slackMsgTxt = slackMsgTxt.replace(/</g, '<');
+ slackMsgTxt = slackMsgTxt.replace(/&/g, '&');
+ slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:');
+ slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:');
+ slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:');
+ slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:');
+ slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1');
+
+ slackMsgTxt.replace(/(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g, (match, userId) => {
+ if (!this.userTags[userId]) {
+ this.findUser(userId) || this.addUser(userId); // This adds userTags for the userId
+ }
+ const userTags = this.userTags[userId];
+ if (userTags) {
+ slackMsgTxt = slackMsgTxt.replace(userTags.slack, userTags.rocket);
+ }
+ });
+ } else {
+ slackMsgTxt = '';
+ }
+ return slackMsgTxt;
+ }
+
+}
diff --git a/app/slackbridge/server/SlackAPI.js b/app/slackbridge/server/SlackAPI.js
new file mode 100644
index 000000000000..a4825cfd05ae
--- /dev/null
+++ b/app/slackbridge/server/SlackAPI.js
@@ -0,0 +1,118 @@
+import { HTTP } from 'meteor/http';
+
+export class SlackAPI {
+
+ constructor(apiToken) {
+ this.apiToken = apiToken;
+ }
+
+ getChannels() {
+ const response = HTTP.get('https://slack.com/api/conversations.list', {
+ params: {
+ token: this.apiToken,
+ types: 'public_channel',
+ },
+ });
+ return response && response.data && Array.isArray(response.data.channels) && response.data.channels.length > 0
+ ? response.data.channels
+ : [];
+ }
+
+ getGroups() {
+ const response = HTTP.get('https://slack.com/api/conversations.list', {
+ params: {
+ token: this.apiToken,
+ types: 'private_channel',
+ },
+ });
+ return response && response.data && Array.isArray(response.data.channels) && response.data.channels.length > 0
+ ? response.data.channels
+ : [];
+ }
+
+ getRoomInfo(roomId) {
+ const response = HTTP.get('https://slack.com/api/conversations.info', {
+ params: {
+ token: this.apiToken,
+ channel: roomId,
+ include_num_members: true,
+ },
+ });
+ return response && response.data && response.statusCode === 200 && response.data.ok && response.data.channel;
+ }
+
+ getMembers(channelId) {
+ const { num_members } = this.getRoomInfo(channelId);
+ const MAX_MEMBERS_PER_CALL = 100;
+ let members = [];
+ let currentCursor = '';
+ for (let index = 0; index < num_members; index += MAX_MEMBERS_PER_CALL) {
+ const response = HTTP.get('https://slack.com/api/conversations.members', {
+ params: {
+ token: this.apiToken,
+ channel: channelId,
+ limit: MAX_MEMBERS_PER_CALL,
+ cursor: currentCursor,
+ },
+ });
+ if (response && response.data && response.statusCode === 200 && response.data.ok && Array.isArray(response.data.members)) {
+ members = members.concat(response.data.members);
+ const hasMoreItems = response.data.response_metadata && response.data.response_metadata.next_cursor;
+ if (hasMoreItems) {
+ currentCursor = response.data.response_metadata.next_cursor;
+ }
+ }
+ }
+ return members;
+ }
+
+ react(data) {
+ const response = HTTP.post('https://slack.com/api/reactions.add', { params: data });
+ return response && response.statusCode === 200 && response.data && response.data.ok;
+ }
+
+ removeReaction(data) {
+ const response = HTTP.post('https://slack.com/api/reactions.remove', { params: data });
+ return response && response.statusCode === 200 && response.data && response.data.ok;
+ }
+
+ removeMessage(data) {
+ const response = HTTP.post('https://slack.com/api/chat.delete', { params: data });
+ return response && response.statusCode === 200 && response.data && response.data.ok;
+ }
+
+ sendMessage(data) {
+ return HTTP.post('https://slack.com/api/chat.postMessage', { params: data });
+ }
+
+ updateMessage(data) {
+ const response = HTTP.post('https://slack.com/api/chat.update', { params: data });
+ return response && response.statusCode === 200 && response.data && response.data.ok;
+ }
+
+ getHistory(family, options) {
+ const response = HTTP.get(`https://slack.com/api/${ family }.history`, { params: Object.assign({ token: this.apiToken }, options) });
+ return response && response.data;
+ }
+
+ getPins(channelId) {
+ const response = HTTP.get('https://slack.com/api/pins.list', {
+ params: {
+ token: this.apiToken,
+ channel: channelId,
+ },
+ });
+ return response && response.data && response.statusCode === 200 && response.data.ok && response.data.items;
+ }
+
+ getUser(userId) {
+ const response = HTTP.get('https://slack.com/api/users.info', {
+ params: {
+ token: this.apiToken,
+ user: userId,
+ },
+ });
+ return response && response.data && response.statusCode === 200 && response.data.ok && response.data.user;
+ }
+
+}
diff --git a/packages/rocketchat-slackbridge/server/SlackAdapter.js b/app/slackbridge/server/SlackAdapter.js
similarity index 78%
rename from packages/rocketchat-slackbridge/server/SlackAdapter.js
rename to app/slackbridge/server/SlackAdapter.js
index 7889b8277926..46fa69f694c6 100644
--- a/packages/rocketchat-slackbridge/server/SlackAdapter.js
+++ b/app/slackbridge/server/SlackAdapter.js
@@ -1,25 +1,39 @@
-import { Meteor } from 'meteor/meteor';
-import { HTTP } from 'meteor/http';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import { getAvatarUrlFromUsername } from 'meteor/rocketchat:utils';
-import { FileUpload } from 'meteor/rocketchat:file-upload';
-import { logger } from './logger';
-import _ from 'underscore';
import url from 'url';
import http from 'http';
import https from 'https';
+import { RTMClient } from '@slack/client';
+import { Meteor } from 'meteor/meteor';
+import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
+import { Messages, Rooms, Users } from '../../models';
+import { settings } from '../../settings';
+import {
+ deleteMessage,
+ updateMessage,
+ addUserToRoom,
+ removeUserFromRoom,
+ archiveRoom,
+ unarchiveRoom,
+ sendMessage,
+} from '../../lib';
+import { saveRoomName, saveRoomTopic } from '../../channel-settings';
+import { FileUpload } from '../../file-upload';
+import { logger } from './logger';
+import { SlackAPI } from './SlackAPI';
export default class SlackAdapter {
constructor(slackBridge) {
logger.slack.debug('constructor');
this.slackBridge = slackBridge;
- this.slackClient = require('@slack/client');
this.rtm = {}; // slack-client Real Time Messaging API
this.apiToken = {}; // Slack API Token passed in via Connect
// On Slack, a rocket integration bot will be added to slack channels, this is the list of those channels, key is Rocket Ch ID
this.slackChannelRocketBotMembershipMap = new Map(); // Key=RocketChannelID, Value=SlackChannel
this.rocket = {};
+ this.messagesBeingSent = [];
+ this.slackBotId = false;
+
+ this.slackAPI = {};
}
/**
@@ -29,10 +43,10 @@ export default class SlackAdapter {
connect(apiToken) {
this.apiToken = apiToken;
- const { RTMClient } = this.slackClient;
if (RTMClient != null) {
RTMClient.disconnect;
}
+ this.slackAPI = new SlackAPI(this.apiToken);
this.rtm = new RTMClient(this.apiToken);
this.rtm.start();
this.registerForEvents();
@@ -51,7 +65,7 @@ export default class SlackAdapter {
* Unregister for slack events and disconnect from Slack
*/
disconnect() {
- this.rtm.disconnect && this.rtm.disconnect;
+ this.rtm.disconnect && this.rtm.disconnect();
}
setRocket(rocket) {
@@ -321,17 +335,17 @@ export default class SlackAdapter {
*/
onReactionRemoved(slackReactionMsg) {
if (slackReactionMsg) {
- if (! this.slackBridge.isReactionsEnabled) {
+ if (!this.slackBridge.isReactionsEnabled) {
return;
}
const rocketUser = this.rocket.getUser(slackReactionMsg.user);
// Lets find our Rocket originated message
- let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts);
+ let rocketMsg = Messages.findOneBySlackTs(slackReactionMsg.item.ts);
if (!rocketMsg) {
// Must have originated from Slack
const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
- rocketMsg = RocketChat.models.Messages.findOneById(rocketID);
+ rocketMsg = Messages.findOneById(rocketID);
}
if (rocketMsg && rocketUser) {
@@ -365,7 +379,7 @@ export default class SlackAdapter {
*/
onReactionAdded(slackReactionMsg) {
if (slackReactionMsg) {
- if (! this.slackBridge.isReactionsEnabled) {
+ if (!this.slackBridge.isReactionsEnabled) {
return;
}
const rocketUser = this.rocket.getUser(slackReactionMsg.user);
@@ -375,12 +389,12 @@ export default class SlackAdapter {
}
// Lets find our Rocket originated message
- let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts);
+ let rocketMsg = Messages.findOneBySlackTs(slackReactionMsg.item.ts);
if (!rocketMsg) {
// Must have originated from Slack
const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
- rocketMsg = RocketChat.models.Messages.findOneById(rocketID);
+ rocketMsg = Messages.findOneById(rocketID);
}
if (rocketMsg && rocketUser) {
@@ -409,11 +423,17 @@ export default class SlackAdapter {
onChannelLeft(channelLeftMsg) {
this.removeSlackChannel(channelLeftMsg.channel);
}
+
/**
* We have received a message from slack and we need to save/delete/update it into rocket
* https://api.slack.com/events/message
*/
onMessage(slackMessage, isImporting) {
+ const isAFileShare = slackMessage && slackMessage.files && Array.isArray(slackMessage.files) && slackMessage.files.length;
+ if (isAFileShare) {
+ this.processFileShare(slackMessage);
+ return;
+ }
if (slackMessage.subtype) {
switch (slackMessage.subtype) {
case 'message_deleted':
@@ -425,9 +445,6 @@ export default class SlackAdapter {
case 'channel_join':
this.processChannelJoin(slackMessage);
break;
- case 'file_share':
- this.processFileShare(slackMessage);
- break;
default:
// Keeping backwards compatability for now, refactor later
this.processNewMessage(slackMessage, isImporting);
@@ -438,27 +455,19 @@ export default class SlackAdapter {
}
}
- postGetChannelInfo(slackChID) {
- logger.slack.debug('Getting slack channel info', slackChID);
- const response = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.apiToken, channel: slackChID } });
- if (response && response.data) {
- return response.data.channel;
- }
- }
-
postFindChannel(rocketChannelName) {
logger.slack.debug('Searching for Slack channel or group', rocketChannelName);
- let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } });
- if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) {
- for (const channel of response.data.channels) {
+ const channels = this.slackAPI.getChannels();
+ if (channels && channels.length > 0) {
+ for (const channel of channels) {
if (channel.name === rocketChannelName && channel.is_member === true) {
return channel;
}
}
}
- response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } });
- if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) {
- for (const group of response.data.groups) {
+ const groups = this.slackAPI.getGroups();
+ if (groups && groups.length > 0) {
+ for (const group of groups) {
if (group.name === rocketChannelName) {
return group;
}
@@ -521,10 +530,10 @@ export default class SlackAdapter {
}
populateMembershipChannelMapByChannels() {
- const response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } });
- if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) {
- for (const slackChannel of response.data.channels) {
- const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } });
+ const channels = this.slackAPI.getChannels();
+ if (channels && channels.length > 0) {
+ for (const slackChannel of channels) {
+ const rocketchat_room = Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } });
if (rocketchat_room) {
if (slackChannel.is_member) {
this.addSlackChannel(rocketchat_room._id, slackChannel.id);
@@ -535,10 +544,10 @@ export default class SlackAdapter {
}
populateMembershipChannelMapByGroups() {
- const response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } });
- if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) {
- for (const slackGroup of response.data.groups) {
- const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } });
+ const groups = this.slackAPI.getGroups();
+ if (groups && groups.length > 0) {
+ for (const slackGroup of groups) {
+ const rocketchat_room = Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } });
if (rocketchat_room) {
if (slackGroup.is_member) {
this.addSlackChannel(rocketchat_room._id, slackGroup.id);
@@ -567,8 +576,8 @@ export default class SlackAdapter {
};
logger.slack.debug('Posting Add Reaction to Slack');
- const postResult = HTTP.post('https://slack.com/api/reactions.add', { params: data });
- if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) {
+ const postResult = this.slackAPI.react(data);
+ if (postResult) {
logger.slack.debug('Reaction added to Slack');
}
}
@@ -587,8 +596,8 @@ export default class SlackAdapter {
};
logger.slack.debug('Posting Remove Reaction to Slack');
- const postResult = HTTP.post('https://slack.com/api/reactions.remove', { params: data });
- if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) {
+ const postResult = this.slackAPI.removeReaction(data);
+ if (postResult) {
logger.slack.debug('Reaction removed from Slack');
}
}
@@ -607,17 +616,46 @@ export default class SlackAdapter {
};
logger.slack.debug('Post Delete Message to Slack', data);
- const postResult = HTTP.post('https://slack.com/api/chat.delete', { params: data });
- if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) {
+ const postResult = this.slackAPI.removeMessage(data);
+ if (postResult) {
logger.slack.debug('Message deleted on Slack');
}
}
}
}
+ storeMessageBeingSent(data) {
+ this.messagesBeingSent.push(data);
+ }
+
+ removeMessageBeingSent(data) {
+ const idx = this.messagesBeingSent.indexOf(data);
+ if (idx >= 0) {
+ this.messagesBeingSent.splice(idx, 1);
+ }
+ }
+
+ isMessageBeingSent(username, channel) {
+ if (!this.messagesBeingSent.length) {
+ return false;
+ }
+
+ return this.messagesBeingSent.some((messageData) => {
+ if (messageData.username !== username) {
+ return false;
+ }
+
+ if (messageData.channel !== channel) {
+ return false;
+ }
+
+ return true;
+ });
+ }
+
postMessage(slackChannel, rocketMessage) {
if (slackChannel && slackChannel.id) {
- let iconUrl = getAvatarUrlFromUsername(rocketMessage.u && rocketMessage.u.username);
+ let iconUrl = getUserAvatarURL(rocketMessage.u && rocketMessage.u.username);
if (iconUrl) {
iconUrl = Meteor.absoluteUrl().replace(/\/$/, '') + iconUrl;
}
@@ -630,9 +668,21 @@ export default class SlackAdapter {
link_names: 1,
};
logger.slack.debug('Post Message To Slack', data);
- const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data });
+
+ // If we don't have the bot id yet and we have multiple slack bridges, we need to keep track of the messages that are being sent
+ if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) {
+ this.storeMessageBeingSent(data);
+ }
+
+ const postResult = this.slackAPI.sendMessage(data);
+
+ if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) {
+ this.removeMessageBeingSent(data);
+ }
+
if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) {
- RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts);
+ this.slackBotId = postResult.data.message.bot_id;
+ Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts);
logger.slack.debug(`RocketMsgID=${ rocketMessage._id } SlackMsgID=${ postResult.data.message.ts } SlackBotID=${ postResult.data.message.bot_id }`);
}
}
@@ -651,8 +701,8 @@ export default class SlackAdapter {
as_user: true,
};
logger.slack.debug('Post UpdateMessage To Slack', data);
- const postResult = HTTP.post('https://slack.com/api/chat.update', { params: data });
- if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) {
+ const postResult = this.slackAPI.updateMessage(data);
+ if (postResult) {
logger.slack.debug('Message updated on Slack');
}
}
@@ -667,11 +717,12 @@ export default class SlackAdapter {
}
processFileShare(slackMessage) {
- if (! RocketChat.settings.get('SlackBridge_FileUpload_Enabled')) {
+ if (!settings.get('SlackBridge_FileUpload_Enabled')) {
return;
}
+ const file = slackMessage.files[0];
- if (slackMessage.file && slackMessage.file.url_private_download !== undefined) {
+ if (file && file.url_private_download !== undefined) {
const rocketChannel = this.rocket.getChannel(slackMessage);
const rocketUser = this.rocket.getUser(slackMessage.user);
@@ -681,8 +732,8 @@ export default class SlackAdapter {
// If the text includes the file link, simply use the same text for the rocket message.
// If the link was not included, then use it instead of the message.
- if (slackMessage.text.indexOf(slackMessage.file.permalink) < 0) {
- slackMessage.text = slackMessage.file.permalink;
+ if (slackMessage.text.indexOf(file.permalink) < 0) {
+ slackMessage.text = file.permalink;
}
const ts = new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000);
@@ -702,21 +753,21 @@ export default class SlackAdapter {
processMessageDeleted(slackMessage) {
if (slackMessage.previous_message) {
const rocketChannel = this.rocket.getChannel(slackMessage);
- const rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } });
+ const rocketUser = Users.findOneById('rocket.cat', { fields: { username: 1 } });
if (rocketChannel && rocketUser) {
// Find the Rocket message to delete
- let rocketMsgObj = RocketChat.models.Messages
+ let rocketMsgObj = Messages
.findOneBySlackBotIdAndSlackTs(slackMessage.previous_message.bot_id, slackMessage.previous_message.ts);
if (!rocketMsgObj) {
// Must have been a Slack originated msg
const _id = this.rocket.createRocketID(slackMessage.channel, slackMessage.previous_message.ts);
- rocketMsgObj = RocketChat.models.Messages.findOneById(_id);
+ rocketMsgObj = Messages.findOneById(_id);
}
if (rocketMsgObj) {
- RocketChat.deleteMessage(rocketMsgObj, rocketUser);
+ deleteMessage(rocketMsgObj, rocketUser);
logger.slack.debug('Rocket message deleted by Slack');
}
}
@@ -728,7 +779,7 @@ export default class SlackAdapter {
*/
processMessageChanged(slackMessage) {
if (slackMessage.previous_message) {
- const currentMsg = RocketChat.models.Messages.findOneById(this.rocket.createRocketID(slackMessage.channel, slackMessage.message.ts));
+ const currentMsg = Messages.findOneById(this.rocket.createRocketID(slackMessage.channel, slackMessage.message.ts));
// Only process this change, if its an actual update (not just Slack repeating back our Rocket original change)
if (currentMsg && (slackMessage.message.text !== currentMsg.msg)) {
@@ -743,7 +794,7 @@ export default class SlackAdapter {
updatedBySlack: true, // We don't want to notify slack about this change since Slack initiated it
};
- RocketChat.updateMessage(rocketMsgObj, rocketUser);
+ updateMessage(rocketMsgObj, rocketUser);
logger.slack.debug('Rocket message updated by Slack');
}
}
@@ -756,7 +807,7 @@ export default class SlackAdapter {
const rocketChannel = this.rocket.getChannel(slackMessage);
let rocketUser = null;
if (slackMessage.subtype === 'bot_message') {
- rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } });
+ rocketUser = Users.findOneById('rocket.cat', { fields: { username: 1 } });
} else {
rocketUser = slackMessage.user ? this.rocket.findUser(slackMessage.user) || this.rocket.addUser(slackMessage.user) : null;
}
@@ -769,7 +820,7 @@ export default class SlackAdapter {
msgDataDefaults.imported = 'slackbridge';
}
try {
- this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting);
+ this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting, this);
} catch (e) {
// http://www.mongodb.org/about/contributors/error-codes/
// 11000 == duplicate key error
@@ -783,11 +834,22 @@ export default class SlackAdapter {
}
processBotMessage(rocketChannel, slackMessage) {
- const excludeBotNames = RocketChat.settings.get('SlackBridge_ExcludeBotnames');
+ const excludeBotNames = settings.get('SlackBridge_ExcludeBotnames');
if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) {
return;
}
+ if (this.slackBotId) {
+ if (slackMessage.bot_id === this.slackBotId) {
+ return;
+ }
+ } else {
+ const slackChannel = this.getSlackChannel(rocketChannel._id);
+ if (this.isMessageBeingSent(slackMessage.username || slackMessage.bot_id, slackChannel.id)) {
+ return;
+ }
+ }
+
const rocketMsgObj = {
msg: this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text),
rid: rocketChannel._id,
@@ -810,9 +872,9 @@ export default class SlackAdapter {
processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
- RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+ Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
- RocketChat.addUserToRoom(rocketChannel._id, rocketUser);
+ addUserToRoom(rocketChannel._id, rocketUser);
}
}
@@ -820,7 +882,7 @@ export default class SlackAdapter {
if (slackMessage.inviter) {
const inviter = slackMessage.inviter ? this.rocket.findUser(slackMessage.inviter) || this.rocket.addUser(slackMessage.inviter) : null;
if (isImporting) {
- RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, {
+ Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, {
ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
u: {
_id: inviter._id,
@@ -829,43 +891,43 @@ export default class SlackAdapter {
imported: 'slackbridge',
});
} else {
- RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter);
+ addUserToRoom(rocketChannel._id, rocketUser, inviter);
}
}
}
processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
- RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, {
+ Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, {
ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
imported: 'slackbridge',
});
} else {
- RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser);
+ removeUserFromRoom(rocketChannel._id, rocketUser);
}
}
processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
- RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+ Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
- RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false);
+ saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false);
}
}
processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
- RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+ Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
- RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false);
+ saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false);
}
}
processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
- RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+ Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
- RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false);
+ saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false);
}
}
@@ -895,13 +957,13 @@ export default class SlackAdapter {
attachments: [{
text : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text),
author_name : slackMessage.attachments[0].author_subname,
- author_icon : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname),
+ author_icon : getUserAvatarURL(slackMessage.attachments[0].author_subname),
ts : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000),
}],
};
if (!isImporting) {
- RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${ slackMessage.attachments[0].channel_id }-${ slackMessage.attachments[0].ts.replace(/\./g, '-') }`, rocketMsgObj.u, true, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000));
+ Messages.setPinnedByIdAndUserId(`slack-${ slackMessage.attachments[0].channel_id }-${ slackMessage.attachments[0].ts.replace(/\./g, '-') }`, rocketMsgObj.u, true, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000));
}
return rocketMsgObj;
@@ -935,13 +997,13 @@ export default class SlackAdapter {
case 'channel_archive':
case 'group_archive':
if (!isImporting) {
- RocketChat.archiveRoom(rocketChannel);
+ archiveRoom(rocketChannel);
}
return;
case 'channel_unarchive':
case 'group_unarchive':
if (!isImporting) {
- RocketChat.unarchiveRoom(rocketChannel);
+ unarchiveRoom(rocketChannel);
}
return;
case 'file_share':
@@ -1022,7 +1084,7 @@ export default class SlackAdapter {
msg._id = details.message_id;
}
- return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true);
+ return sendMessage(rocketUser, msg, rocketChannel, true);
}
});
}));
@@ -1030,10 +1092,10 @@ export default class SlackAdapter {
importFromHistory(family, options) {
logger.slack.debug('Importing messages history');
- const response = HTTP.get(`https://slack.com/api/${ family }.history`, { params: _.extend({ token: this.apiToken }, options) });
- if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) {
+ const data = this.slackAPI.getHistory(family, options);
+ if (Array.isArray(data.messages) && data.messages.length) {
let latest = 0;
- for (const message of response.data.messages.reverse()) {
+ for (const message of data.messages.reverse()) {
logger.slack.debug('MESSAGE: ', message);
if (!latest || message.ts > latest) {
latest = message.ts;
@@ -1041,21 +1103,21 @@ export default class SlackAdapter {
message.channel = options.channel;
this.onMessage(message, true);
}
- return { has_more: response.data.has_more, ts: latest };
+ return { has_more: data.has_more, ts: latest };
}
}
copyChannelInfo(rid, channelMap) {
logger.slack.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid);
- const response = HTTP.get(`https://slack.com/api/${ channelMap.family }.info`, { params: { token: this.apiToken, channel: channelMap.id } });
- if (response && response.data) {
- const data = channelMap.family === 'channels' ? response.data.channel : response.data.group;
- if (data && _.isArray(data.members) && data.members.length > 0) {
- for (const member of data.members) {
+ const channel = this.slackAPI.getRoomInfo(channelMap.id);
+ if (channel) {
+ const members = this.slackAPI.getMembers(channelMap.id);
+ if (members && Array.isArray(members) && members.length) {
+ for (const member of members) {
const user = this.rocket.findUser(member) || this.rocket.addUser(member);
if (user) {
logger.slack.debug('Adding user to room', user.username, rid);
- RocketChat.addUserToRoom(rid, user, null, true);
+ addUserToRoom(rid, user, null, true);
}
}
}
@@ -1063,36 +1125,36 @@ export default class SlackAdapter {
let topic = '';
let topic_last_set = 0;
let topic_creator = null;
- if (data && data.topic && data.topic.value) {
- topic = data.topic.value;
- topic_last_set = data.topic.last_set;
- topic_creator = data.topic.creator;
+ if (channel && channel.topic && channel.topic.value) {
+ topic = channel.topic.value;
+ topic_last_set = channel.topic.last_set;
+ topic_creator = channel.topic.creator;
}
- if (data && data.purpose && data.purpose.value) {
+ if (channel && channel.purpose && channel.purpose.value) {
if (topic_last_set) {
- if (topic_last_set < data.purpose.last_set) {
- topic = data.purpose.topic;
- topic_creator = data.purpose.creator;
+ if (topic_last_set < channel.purpose.last_set) {
+ topic = channel.purpose.topic;
+ topic_creator = channel.purpose.creator;
}
} else {
- topic = data.purpose.topic;
- topic_creator = data.purpose.creator;
+ topic = channel.purpose.topic;
+ topic_creator = channel.purpose.creator;
}
}
if (topic) {
const creator = this.rocket.findUser(topic_creator) || this.rocket.addUser(topic_creator);
logger.slack.debug('Setting room topic', rid, topic, creator.username);
- RocketChat.saveRoomTopic(rid, topic, creator, false);
+ saveRoomTopic(rid, topic, creator, false);
}
}
}
copyPins(rid, channelMap) {
- const response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } });
- if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) {
- for (const pin of response.data.items) {
+ const items = this.slackAPI.getPins(channelMap.id);
+ if (items && Array.isArray(items) && items.length) {
+ for (const pin of items) {
if (pin.message) {
const user = this.rocket.findUser(pin.message.user);
const msgObj = {
@@ -1106,12 +1168,12 @@ export default class SlackAdapter {
attachments: [{
text : this.rocket.convertSlackMsgTxtToRocketTxtFormat(pin.message.text),
author_name : user.username,
- author_icon : getAvatarUrlFromUsername(user.username),
+ author_icon : getUserAvatarURL(user.username),
ts : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000),
}],
};
- RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${ pin.channel }-${ pin.message.ts.replace(/\./g, '-') }`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000));
+ Messages.setPinnedByIdAndUserId(`slack-${ pin.channel }-${ pin.message.ts.replace(/\./g, '-') }`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000));
}
}
}
@@ -1119,7 +1181,7 @@ export default class SlackAdapter {
importMessages(rid, callback) {
logger.slack.info('importMessages: ', rid);
- const rocketchat_room = RocketChat.models.Rooms.findOneById(rid);
+ const rocketchat_room = Rooms.findOneById(rid);
if (rocketchat_room) {
if (this.getSlackChannel(rid)) {
this.copyChannelInfo(rid, this.getSlackChannel(rid));
diff --git a/packages/rocketchat-slackbridge/server/index.js b/app/slackbridge/server/index.js
similarity index 100%
rename from packages/rocketchat-slackbridge/server/index.js
rename to app/slackbridge/server/index.js
diff --git a/app/slackbridge/server/logger.js b/app/slackbridge/server/logger.js
new file mode 100644
index 000000000000..f8cf66078391
--- /dev/null
+++ b/app/slackbridge/server/logger.js
@@ -0,0 +1,11 @@
+import { Logger } from '../../logger';
+
+export const logger = new Logger('SlackBridge', {
+ sections: {
+ connection: 'Connection',
+ events: 'Events',
+ class: 'Class',
+ slack: 'Slack',
+ rocket: 'Rocket',
+ },
+});
diff --git a/app/slackbridge/server/settings.js b/app/slackbridge/server/settings.js
new file mode 100644
index 000000000000..06421b512631
--- /dev/null
+++ b/app/slackbridge/server/settings.js
@@ -0,0 +1,94 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ settings.addGroup('SlackBridge', function() {
+ this.add('SlackBridge_Enabled', false, {
+ type: 'boolean',
+ i18nLabel: 'Enabled',
+ public: true,
+ });
+
+ this.add('SlackBridge_APIToken', '', {
+ type: 'string',
+ multiline: true,
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ i18nLabel: 'SlackBridge_APIToken',
+ i18nDescription: 'SlackBridge_APIToken_Description',
+ });
+
+ this.add('SlackBridge_FileUpload_Enabled', true, {
+ type: 'boolean',
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ i18nLabel: 'FileUpload',
+ });
+
+ this.add('SlackBridge_Out_Enabled', false, {
+ type: 'boolean',
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ });
+
+ this.add('SlackBridge_Out_All', false, {
+ type: 'boolean',
+ enableQuery: [{
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ }, {
+ _id: 'SlackBridge_Out_Enabled',
+ value: true,
+ }],
+ });
+
+ this.add('SlackBridge_Out_Channels', '', {
+ type: 'roomPick',
+ enableQuery: [{
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ }, {
+ _id: 'SlackBridge_Out_Enabled',
+ value: true,
+ }, {
+ _id: 'SlackBridge_Out_All',
+ value: false,
+ }],
+ });
+
+ this.add('SlackBridge_AliasFormat', '', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Alias_Format',
+ i18nDescription: 'Alias_Format_Description',
+ });
+
+ this.add('SlackBridge_ExcludeBotnames', '', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Exclude_Botnames',
+ i18nDescription: 'Exclude_Botnames_Description',
+ });
+
+ this.add('SlackBridge_Reactions_Enabled', true, {
+ type: 'boolean',
+ enableQuery: {
+ _id: 'SlackBridge_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Reactions',
+ });
+ });
+});
diff --git a/app/slackbridge/server/slackbridge.js b/app/slackbridge/server/slackbridge.js
new file mode 100644
index 000000000000..953453b874be
--- /dev/null
+++ b/app/slackbridge/server/slackbridge.js
@@ -0,0 +1,108 @@
+import { settings } from '../../settings';
+import SlackAdapter from './SlackAdapter.js';
+import RocketAdapter from './RocketAdapter.js';
+import { logger } from './logger';
+
+/**
+ * SlackBridge interfaces between this Rocket installation and a remote Slack installation.
+ */
+class SlackBridgeClass {
+
+ constructor() {
+ this.slackAdapters = [];
+ this.rocket = new RocketAdapter(this);
+ this.reactionsMap = new Map(); // Sync object between rocket and slack
+
+ this.connected = false;
+ this.rocket.clearSlackAdapters();
+
+ // Settings that we cache versus looking up at runtime
+ this.apiTokens = false;
+ this.aliasFormat = '';
+ this.excludeBotnames = '';
+ this.isReactionsEnabled = true;
+
+ this.processSettings();
+ }
+
+ connect() {
+ if (this.connected === false) {
+ this.slackAdapters = [];
+ this.rocket.clearSlackAdapters();
+
+ const tokenList = this.apiTokens.split('\n');
+ tokenList.forEach((apiToken) => {
+ const slack = new SlackAdapter(this);
+ slack.setRocket(this.rocket);
+ this.rocket.addSlack(slack);
+ this.slackAdapters.push(slack);
+
+ slack.connect(apiToken);
+ });
+
+ if (settings.get('SlackBridge_Out_Enabled')) {
+ this.rocket.connect();
+ }
+
+ this.connected = true;
+ logger.connection.info('Enabled');
+ }
+ }
+
+ disconnect() {
+ if (this.connected === true) {
+ this.rocket.disconnect();
+ this.slackAdapters.forEach((slack) => {
+ slack.disconnect();
+ });
+ this.slackAdapters = [];
+ this.connected = false;
+ logger.connection.info('Disabled');
+ }
+ }
+
+ processSettings() {
+ // Slack installation API token
+ settings.get('SlackBridge_APIToken', (key, value) => {
+ if (value !== this.apiTokens) {
+ this.apiTokens = value;
+ if (this.connected) {
+ this.disconnect();
+ this.connect();
+ }
+ }
+
+ logger.class.debug(`Setting: ${ key }`, value);
+ });
+
+ // Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used.
+ settings.get('SlackBridge_AliasFormat', (key, value) => {
+ this.aliasFormat = value;
+ logger.class.debug(`Setting: ${ key }`, value);
+ });
+
+ // Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.
+ settings.get('SlackBridge_ExcludeBotnames', (key, value) => {
+ this.excludeBotnames = value;
+ logger.class.debug(`Setting: ${ key }`, value);
+ });
+
+ // Reactions
+ settings.get('SlackBridge_Reactions_Enabled', (key, value) => {
+ this.isReactionsEnabled = value;
+ logger.class.debug(`Setting: ${ key }`, value);
+ });
+
+ // Is this entire SlackBridge enabled
+ settings.get('SlackBridge_Enabled', (key, value) => {
+ if (value && this.apiTokens) {
+ this.connect();
+ } else {
+ this.disconnect();
+ }
+ logger.class.debug(`Setting: ${ key }`, value);
+ });
+ }
+}
+
+export const SlackBridge = new SlackBridgeClass;
diff --git a/packages/rocketchat-slackbridge/server/slackbridge_import.server.js b/app/slackbridge/server/slackbridge_import.server.js
similarity index 82%
rename from packages/rocketchat-slackbridge/server/slackbridge_import.server.js
rename to app/slackbridge/server/slackbridge_import.server.js
index 2b869e0e32da..5693839cfc50 100644
--- a/packages/rocketchat-slackbridge/server/slackbridge_import.server.js
+++ b/app/slackbridge/server/slackbridge_import.server.js
@@ -2,14 +2,17 @@ import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Rooms } from '../../models';
+import { msgStream } from '../../lib';
+import { slashCommands } from '../../utils';
+import { SlackBridge } from './slackbridge';
function SlackBridgeImport(command, params, item) {
if (command !== 'slackbridge-import' || !Match.test(params, String)) {
return;
}
- const room = RocketChat.models.Rooms.findOneById(item.rid);
+ const room = Rooms.findOneById(item.rid);
const channel = room.name;
const user = Meteor.users.findOne(Meteor.userId());
@@ -25,7 +28,7 @@ function SlackBridgeImport(command, params, item) {
});
try {
- RocketChat.SlackBridge.importMessages(item.rid, (error) => {
+ SlackBridge.slack.importMessages(item.rid, (error) => {
if (error) {
msgStream.emit(item.rid, {
_id: Random.id(),
@@ -66,4 +69,4 @@ function SlackBridgeImport(command, params, item) {
return SlackBridgeImport;
}
-RocketChat.slashCommands.add('slackbridge-import', SlackBridgeImport);
+slashCommands.add('slackbridge-import', SlackBridgeImport);
diff --git a/packages/rocketchat-slackbridge/tests/manual-tests.txt b/app/slackbridge/tests/manual-tests.txt
similarity index 100%
rename from packages/rocketchat-slackbridge/tests/manual-tests.txt
rename to app/slackbridge/tests/manual-tests.txt
diff --git a/packages/rocketchat-slashcommand-asciiarts/client/index.js b/app/slashcommand-asciiarts/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommand-asciiarts/client/index.js
rename to app/slashcommand-asciiarts/client/index.js
diff --git a/packages/rocketchat-slashcommand-asciiarts/lib/gimme.js b/app/slashcommand-asciiarts/lib/gimme.js
similarity index 80%
rename from packages/rocketchat-slashcommand-asciiarts/lib/gimme.js
rename to app/slashcommand-asciiarts/lib/gimme.js
index 9add71f70cf5..1b0ece10ed94 100644
--- a/packages/rocketchat-slashcommand-asciiarts/lib/gimme.js
+++ b/app/slashcommand-asciiarts/lib/gimme.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
/*
* Gimme is a named function that will replace /gimme commands
* @param {Object} message - The message object
@@ -14,7 +14,7 @@ function Gimme(command, params, item) {
}
}
-RocketChat.slashCommands.add('gimme', Gimme, {
+slashCommands.add('gimme', Gimme, {
description: 'Slash_Gimme_Description',
params: 'your_message_optional',
});
diff --git a/packages/rocketchat-slashcommand-asciiarts/lib/lenny.js b/app/slashcommand-asciiarts/lib/lenny.js
similarity index 79%
rename from packages/rocketchat-slashcommand-asciiarts/lib/lenny.js
rename to app/slashcommand-asciiarts/lib/lenny.js
index 3fc46b741690..59f39c8c756a 100644
--- a/packages/rocketchat-slashcommand-asciiarts/lib/lenny.js
+++ b/app/slashcommand-asciiarts/lib/lenny.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
/*
* Lenny is a named function that will replace /lenny commands
* @param {Object} message - The message object
@@ -14,7 +14,7 @@ function LennyFace(command, params, item) {
}
}
-RocketChat.slashCommands.add('lennyface', LennyFace, {
+slashCommands.add('lennyface', LennyFace, {
description: 'Slash_LennyFace_Description',
params: 'your_message_optional',
});
diff --git a/packages/rocketchat-slashcommand-asciiarts/lib/shrug.js b/app/slashcommand-asciiarts/lib/shrug.js
similarity index 80%
rename from packages/rocketchat-slashcommand-asciiarts/lib/shrug.js
rename to app/slashcommand-asciiarts/lib/shrug.js
index 38aaca78d498..776f99577e3c 100644
--- a/packages/rocketchat-slashcommand-asciiarts/lib/shrug.js
+++ b/app/slashcommand-asciiarts/lib/shrug.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
/*
* Shrug is a named function that will replace /shrug commands
* @param {Object} message - The message object
@@ -14,7 +14,7 @@ function Shrug(command, params, item) {
}
}
-RocketChat.slashCommands.add('shrug', Shrug, {
+slashCommands.add('shrug', Shrug, {
description: 'Slash_Shrug_Description',
params: 'your_message_optional',
});
diff --git a/packages/rocketchat-slashcommand-asciiarts/lib/tableflip.js b/app/slashcommand-asciiarts/lib/tableflip.js
similarity index 80%
rename from packages/rocketchat-slashcommand-asciiarts/lib/tableflip.js
rename to app/slashcommand-asciiarts/lib/tableflip.js
index ef6a5940d4f6..fc054e18b588 100644
--- a/packages/rocketchat-slashcommand-asciiarts/lib/tableflip.js
+++ b/app/slashcommand-asciiarts/lib/tableflip.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
/*
* Tableflip is a named function that will replace /Tableflip commands
* @param {Object} message - The message object
@@ -14,7 +14,7 @@ function Tableflip(command, params, item) {
}
}
-RocketChat.slashCommands.add('tableflip', Tableflip, {
+slashCommands.add('tableflip', Tableflip, {
description: 'Slash_Tableflip_Description',
params: 'your_message_optional',
});
diff --git a/packages/rocketchat-slashcommand-asciiarts/lib/unflip.js b/app/slashcommand-asciiarts/lib/unflip.js
similarity index 80%
rename from packages/rocketchat-slashcommand-asciiarts/lib/unflip.js
rename to app/slashcommand-asciiarts/lib/unflip.js
index 0b15cc492939..0cc6c3c47e2f 100644
--- a/packages/rocketchat-slashcommand-asciiarts/lib/unflip.js
+++ b/app/slashcommand-asciiarts/lib/unflip.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
/*
* Unflip is a named function that will replace /unflip commands
* @param {Object} message - The message object
@@ -14,7 +14,7 @@ function Unflip(command, params, item) {
}
}
-RocketChat.slashCommands.add('unflip', Unflip, {
+slashCommands.add('unflip', Unflip, {
description: 'Slash_TableUnflip_Description',
params: 'your_message_optional',
});
diff --git a/packages/rocketchat-slashcommand-asciiarts/server/index.js b/app/slashcommand-asciiarts/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommand-asciiarts/server/index.js
rename to app/slashcommand-asciiarts/server/index.js
diff --git a/app/slashcommands-archiveroom/client/client.js b/app/slashcommands-archiveroom/client/client.js
new file mode 100644
index 000000000000..472ec711b2e7
--- /dev/null
+++ b/app/slashcommands-archiveroom/client/client.js
@@ -0,0 +1,6 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('archive', null, {
+ description: 'Archive',
+ params: '#channel',
+});
diff --git a/packages/rocketchat-mentions/client/index.js b/app/slashcommands-archiveroom/client/index.js
similarity index 100%
rename from packages/rocketchat-mentions/client/index.js
rename to app/slashcommands-archiveroom/client/index.js
diff --git a/packages/rocketchat-slashcommands-create/server/index.js b/app/slashcommands-archiveroom/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-create/server/index.js
rename to app/slashcommands-archiveroom/server/index.js
diff --git a/app/slashcommands-archiveroom/server/server.js b/app/slashcommands-archiveroom/server/server.js
new file mode 100644
index 000000000000..f4da898c8db8
--- /dev/null
+++ b/app/slashcommands-archiveroom/server/server.js
@@ -0,0 +1,75 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Messages } from '../../models';
+import { slashCommands } from '../../utils';
+import { Notifications } from '../../notifications';
+
+function Archive(command, params, item) {
+ if (command !== 'archive' || !Match.test(params, String)) {
+ return;
+ }
+
+ let channel = params.trim();
+ let room;
+
+ if (channel === '') {
+ room = Rooms.findOneById(item.rid);
+ channel = room.name;
+ } else {
+ channel = channel.replace('#', '');
+ room = Rooms.findOneByName(channel);
+ }
+
+ const user = Meteor.users.findOne(Meteor.userId());
+
+ if (!room) {
+ return Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ }
+
+ // You can not archive direct messages.
+ if (room.t === 'd') {
+ return;
+ }
+
+ if (room.archived) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Duplicate_archived_channel_name', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ return;
+ }
+ Meteor.call('archiveRoom', room._id);
+
+ Messages.createRoomArchivedByRoomIdAndUser(room._id, Meteor.user());
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_Archived', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+
+ return Archive;
+}
+
+slashCommands.add('archive', Archive, {
+ description: 'Archive',
+ params: '#channel',
+});
diff --git a/app/slashcommands-create/client/client.js b/app/slashcommands-create/client/client.js
new file mode 100644
index 000000000000..020f7687d48c
--- /dev/null
+++ b/app/slashcommands-create/client/client.js
@@ -0,0 +1,6 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('create', null, {
+ description: 'Create_A_New_Channel',
+ params: '#channel',
+});
diff --git a/packages/rocketchat-slashcommands-archiveroom/client/index.js b/app/slashcommands-create/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-archiveroom/client/index.js
rename to app/slashcommands-create/client/index.js
diff --git a/packages/rocketchat-slashcommands-help/server/index.js b/app/slashcommands-create/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-help/server/index.js
rename to app/slashcommands-create/server/index.js
diff --git a/app/slashcommands-create/server/server.js b/app/slashcommands-create/server/server.js
new file mode 100644
index 000000000000..a3fb8fe3b713
--- /dev/null
+++ b/app/slashcommands-create/server/server.js
@@ -0,0 +1,60 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../settings';
+import { Notifications } from '../../notifications';
+import { Rooms } from '../../models';
+import { slashCommands } from '../../utils';
+
+function Create(command, params, item) {
+ function getParams(str) {
+ const regex = /(--(\w+))+/g;
+ const result = [];
+ let m;
+ while ((m = regex.exec(str)) !== null) {
+ if (m.index === regex.lastIndex) {
+ regex.lastIndex++;
+ }
+ result.push(m[2]);
+ }
+ return result;
+ }
+
+ const regexp = new RegExp(settings.get('UTF8_Names_Validation'));
+
+ if (command !== 'create' || !Match.test(params, String)) {
+ return;
+ }
+ let channel = regexp.exec(params.trim());
+ channel = channel ? channel[0] : '';
+ if (channel === '') {
+ return;
+ }
+
+ const user = Meteor.users.findOne(Meteor.userId());
+ const room = Rooms.findOneByName(channel);
+ if (room != null) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_already_exist', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ return;
+ }
+
+ if (getParams(params).indexOf('private') > -1) {
+ return Meteor.call('createPrivateGroup', channel, []);
+ }
+
+ Meteor.call('createChannel', channel, []);
+}
+
+slashCommands.add('create', Create, {
+ description: 'Create_A_New_Channel',
+ params: '#channel',
+});
diff --git a/app/slashcommands-help/index.js b/app/slashcommands-help/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/slashcommands-help/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-slashcommands-invite/server/index.js b/app/slashcommands-help/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-invite/server/index.js
rename to app/slashcommands-help/server/index.js
diff --git a/app/slashcommands-help/server/server.js b/app/slashcommands-help/server/server.js
new file mode 100644
index 000000000000..a8773215e79b
--- /dev/null
+++ b/app/slashcommands-help/server/server.js
@@ -0,0 +1,54 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { slashCommands } from '../../utils';
+import { Notifications } from '../../notifications';
+
+/*
+* Help is a named function that will replace /join commands
+* @param {Object} message - The message object
+*/
+
+
+slashCommands.add('help', function Help(command, params, item) {
+ const user = Meteor.users.findOne(Meteor.userId());
+ const keys = [{
+ Open_channel_user_search: 'Command (or Ctrl) + p OR Command (or Ctrl) + k',
+ },
+ {
+ Mark_all_as_read: 'Shift (or Ctrl) + ESC',
+ },
+ {
+ Edit_previous_message: 'Up Arrow',
+ },
+ {
+ Move_beginning_message: 'Command (or Alt) + Left Arrow',
+ },
+ {
+ Move_beginning_message: 'Command (or Alt) + Up Arrow',
+ },
+ {
+ Move_end_message: 'Command (or Alt) + Right Arrow',
+ },
+ {
+ Move_end_message: 'Command (or Alt) + Down Arrow',
+ },
+ {
+ New_line_message_compose_input: 'Shift + Enter',
+ },
+ ];
+ keys.forEach((key) => {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__(Object.keys(key)[0], {
+ postProcess: 'sprintf',
+ sprintf: [key[Object.keys(key)[0]]],
+ }, user.language),
+ });
+ });
+
+}, {
+ description: 'Show_the_keyboard_shortcut_list',
+});
diff --git a/app/slashcommands-hide/client/hide.js b/app/slashcommands-hide/client/hide.js
new file mode 100644
index 000000000000..a28f9a25c199
--- /dev/null
+++ b/app/slashcommands-hide/client/hide.js
@@ -0,0 +1,6 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('hide', undefined, {
+ description: 'Hide_room',
+ params: '#room',
+});
diff --git a/packages/rocketchat-slashcommands-hide/client/index.js b/app/slashcommands-hide/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-hide/client/index.js
rename to app/slashcommands-hide/client/index.js
diff --git a/app/slashcommands-hide/server/hide.js b/app/slashcommands-hide/server/hide.js
new file mode 100644
index 000000000000..20940e37dcc4
--- /dev/null
+++ b/app/slashcommands-hide/server/hide.js
@@ -0,0 +1,68 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Subscriptions } from '../../models';
+import { Notifications } from '../../notifications';
+import { slashCommands } from '../../utils';
+
+/*
+* Hide is a named function that will replace /hide commands
+* @param {Object} message - The message object
+*/
+function Hide(command, param, item) {
+ if (command !== 'hide' || !Match.test(param, String)) {
+ return;
+ }
+ const room = param.trim();
+ const user = Meteor.user();
+ // if there is not a param, hide the current room
+ let { rid } = item;
+ if (room !== '') {
+ const [strippedRoom] = room.replace(/#|@/, '').split(' ');
+ const [type] = room;
+
+ const roomObject = type === '#' ? Rooms.findOneByName(strippedRoom) : Rooms.findOne({
+ t: 'd',
+ usernames: { $all: [user.username, strippedRoom] },
+ });
+
+ if (!roomObject) {
+ return Notifications.notifyUser(user._id, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Channel_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [room],
+ }, user.language),
+ });
+ }
+
+ if (!Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } })) {
+ return Notifications.notifyUser(user._id, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('error-logged-user-not-in-room', {
+ postProcess: 'sprintf',
+ sprintf: [room],
+ }, user.language),
+ });
+ }
+ rid = roomObject._id;
+ }
+
+ Meteor.call('hideRoom', rid, (error) => {
+ if (error) {
+ return Notifications.notifyUser(user._id, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__(error, null, user.language),
+ });
+ }
+ });
+}
+
+slashCommands.add('hide', Hide, { description: 'Hide_room', params: '#room' });
diff --git a/packages/rocketchat-slashcommands-hide/server/index.js b/app/slashcommands-hide/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-hide/server/index.js
rename to app/slashcommands-hide/server/index.js
diff --git a/app/slashcommands-invite/client/client.js b/app/slashcommands-invite/client/client.js
new file mode 100644
index 000000000000..93665e62c362
--- /dev/null
+++ b/app/slashcommands-invite/client/client.js
@@ -0,0 +1,6 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('invite', undefined, {
+ description: 'Invite_user_to_join_channel',
+ params: '@username',
+});
diff --git a/packages/rocketchat-slashcommands-create/client/index.js b/app/slashcommands-invite/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-create/client/index.js
rename to app/slashcommands-invite/client/index.js
diff --git a/packages/rocketchat-slashcommands-inviteall/server/index.js b/app/slashcommands-invite/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-inviteall/server/index.js
rename to app/slashcommands-invite/server/index.js
diff --git a/app/slashcommands-invite/server/server.js b/app/slashcommands-invite/server/server.js
new file mode 100644
index 000000000000..ceb564626457
--- /dev/null
+++ b/app/slashcommands-invite/server/server.js
@@ -0,0 +1,91 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Notifications } from '../../notifications';
+import { slashCommands } from '../../utils';
+import { Subscriptions } from '../../models';
+
+/*
+* Invite is a named function that will replace /invite commands
+* @param {Object} message - The message object
+*/
+
+
+function Invite(command, params, item) {
+
+ if (command !== 'invite' || !Match.test(params, String)) {
+ return;
+ }
+
+ const usernames = params.split(/[\s,]/).map((username) => username.replace(/(^@)|( @)/, '')).filter((a) => a !== '');
+ if (usernames.length === 0) {
+ return;
+ }
+ let users = Meteor.users.find({
+ username: {
+ $in: usernames,
+ },
+ });
+ const userId = Meteor.userId();
+ const currentUser = Meteor.users.findOne(userId);
+ if (users.count() === 0) {
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('User_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [usernames.join(' @')],
+ }, currentUser.language),
+ });
+ return;
+ }
+ users = users.fetch().filter(function(user) {
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, user._id, { fields: { _id: 1 } });
+ if (subscription == null) {
+ return true;
+ }
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_is_already_in_here', {
+ postProcess: 'sprintf',
+ sprintf: [user.username],
+ }, currentUser.language),
+ });
+ return false;
+ });
+
+ users.forEach(function(user) {
+
+ try {
+ return Meteor.call('addUserToRoom', {
+ rid: item.rid,
+ username: user.username,
+ });
+ } catch ({ error }) {
+ if (error === 'cant-invite-for-direct-room') {
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Cannot_invite_users_to_direct_rooms', null, currentUser.language),
+ });
+ } else {
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__(error, null, currentUser.language),
+ });
+ }
+ }
+ });
+}
+
+slashCommands.add('invite', Invite, {
+ description: 'Invite_user_to_join_channel',
+ params: '@username',
+});
diff --git a/app/slashcommands-inviteall/client/client.js b/app/slashcommands-inviteall/client/client.js
new file mode 100644
index 000000000000..2626561abadd
--- /dev/null
+++ b/app/slashcommands-inviteall/client/client.js
@@ -0,0 +1,10 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('invite-all-to', undefined, {
+ description: 'Invite_user_to_join_channel_all_to',
+ params: '#room',
+});
+slashCommands.add('invite-all-from', undefined, {
+ description: 'Invite_user_to_join_channel_all_from',
+ params: '#room',
+});
diff --git a/packages/rocketchat-slashcommands-invite/client/index.js b/app/slashcommands-inviteall/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-invite/client/index.js
rename to app/slashcommands-inviteall/client/index.js
diff --git a/packages/rocketchat-slashcommands-join/server/index.js b/app/slashcommands-inviteall/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-join/server/index.js
rename to app/slashcommands-inviteall/server/index.js
diff --git a/app/slashcommands-inviteall/server/server.js b/app/slashcommands-inviteall/server/server.js
new file mode 100644
index 000000000000..f6e3fa6076ba
--- /dev/null
+++ b/app/slashcommands-inviteall/server/server.js
@@ -0,0 +1,96 @@
+/*
+ * Invite is a named function that will replace /invite commands
+ * @param {Object} message - The message object
+ */
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Subscriptions } from '../../models';
+import { slashCommands } from '../../utils';
+import { settings } from '../../settings';
+import { Notifications } from '../../notifications';
+
+function inviteAll(type) {
+ return function inviteAll(command, params, item) {
+
+ if (!/invite\-all-(to|from)/.test(command) || !Match.test(params, String)) {
+ return;
+ }
+
+ const regexp = /#?([\d-_\w]+)/g;
+ const [, channel] = regexp.exec(params.trim());
+
+ if (!channel) {
+ return;
+ }
+ const userId = Meteor.userId();
+ const currentUser = Meteor.users.findOne(userId);
+ const baseChannel = type === 'to' ? Rooms.findOneById(item.rid) : Rooms.findOneByName(channel);
+ const targetChannel = type === 'from' ? Rooms.findOneById(item.rid) : Rooms.findOneByName(channel);
+
+ if (!baseChannel) {
+ return Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, currentUser.language),
+ });
+ }
+ const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { fields: { 'u.username': 1 } });
+
+ try {
+ if (cursor.count() > settings.get('API_User_Limit')) {
+ throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', {
+ method: 'addAllToRoom',
+ });
+ }
+ const users = cursor.fetch().map((s) => s.u.username);
+
+ if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) {
+ Meteor.call(baseChannel.t === 'c' ? 'createChannel' : 'createPrivateGroup', channel, users);
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_created', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, currentUser.language),
+ });
+ } else {
+ Meteor.call('addUsersToRoom', {
+ rid: targetChannel._id,
+ users,
+ });
+ }
+ return Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Users_added', null, currentUser.language),
+ });
+ } catch (e) {
+ const msg = e.error === 'cant-invite-for-direct-room' ? 'Cannot_invite_users_to_direct_rooms' : e.error;
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__(msg, null, currentUser.language),
+ });
+ }
+ };
+}
+
+slashCommands.add('invite-all-to', inviteAll('to'), {
+ description: 'Invite_user_to_join_channel_all_to',
+ params: '#room',
+});
+slashCommands.add('invite-all-from', inviteAll('from'), {
+ description: 'Invite_user_to_join_channel_all_from',
+ params: '#room',
+});
+module.exports = inviteAll;
diff --git a/app/slashcommands-join/client/client.js b/app/slashcommands-join/client/client.js
new file mode 100644
index 000000000000..d1d0ab2971fe
--- /dev/null
+++ b/app/slashcommands-join/client/client.js
@@ -0,0 +1,12 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('join', undefined, {
+ description: 'Join_the_given_channel',
+ params: '#channel',
+}, function(err, result, params) {
+ if (err.error === 'error-user-already-in-room') {
+ params.cmd = 'open';
+ params.msg.msg = params.msg.msg.replace('join', 'open');
+ return slashCommands.run('open', params.params, params.msg);
+ }
+});
diff --git a/packages/rocketchat-slashcommands-inviteall/client/index.js b/app/slashcommands-join/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-inviteall/client/index.js
rename to app/slashcommands-join/client/index.js
diff --git a/packages/rocketchat-slashcommands-kick/server/index.js b/app/slashcommands-join/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-kick/server/index.js
rename to app/slashcommands-join/server/index.js
diff --git a/app/slashcommands-join/server/server.js b/app/slashcommands-join/server/server.js
new file mode 100644
index 000000000000..fb43c3cb0d36
--- /dev/null
+++ b/app/slashcommands-join/server/server.js
@@ -0,0 +1,48 @@
+
+/*
+* Join is a named function that will replace /join commands
+* @param {Object} message - The message object
+*/
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Subscriptions } from '../../models';
+import { Notifications } from '../../notifications';
+import { slashCommands } from '../../utils';
+
+slashCommands.add('join', function Join(command, params, item) {
+
+ if (command !== 'join' || !Match.test(params, String)) {
+ return;
+ }
+ let channel = params.trim();
+ if (channel === '') {
+ return;
+ }
+ channel = channel.replace('#', '');
+ const user = Meteor.users.findOne(Meteor.userId());
+ const room = Rooms.findOneByNameAndType(channel, 'c');
+ if (!room) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Channel_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } });
+ if (subscription) {
+ throw new Meteor.Error('error-user-already-in-room', 'You are already in the channel', {
+ method: 'slashCommands',
+ });
+ }
+ Meteor.call('joinRoom', room._id);
+}, {
+ description: 'Join_the_given_channel',
+ params: '#channel',
+});
diff --git a/app/slashcommands-kick/client/client.js b/app/slashcommands-kick/client/client.js
new file mode 100644
index 000000000000..eb0b32c6c60e
--- /dev/null
+++ b/app/slashcommands-kick/client/client.js
@@ -0,0 +1,12 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('kick', function(command, params) {
+ const username = params.trim();
+ if (username === '') {
+ return;
+ }
+ return username.replace('@', '');
+}, {
+ description: 'Remove_someone_from_room',
+ params: '@username',
+});
diff --git a/packages/rocketchat-slashcommands-join/client/index.js b/app/slashcommands-kick/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-join/client/index.js
rename to app/slashcommands-kick/client/index.js
diff --git a/packages/rocketchat-slashcommands-msg/server/index.js b/app/slashcommands-kick/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-msg/server/index.js
rename to app/slashcommands-kick/server/index.js
diff --git a/app/slashcommands-kick/server/server.js b/app/slashcommands-kick/server/server.js
new file mode 100644
index 000000000000..ee73f3bfdee0
--- /dev/null
+++ b/app/slashcommands-kick/server/server.js
@@ -0,0 +1,54 @@
+
+// Kick is a named function that will replace /kick commands
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Notifications } from '../../notifications';
+import { Users, Subscriptions } from '../../models';
+import { slashCommands } from '../../utils';
+
+const Kick = function(command, params, { rid }) {
+ if (command !== 'kick' || !Match.test(params, String)) {
+ return;
+ }
+ const username = params.trim().replace('@', '');
+ if (username === '') {
+ return;
+ }
+ const userId = Meteor.userId();
+ const user = Meteor.users.findOne(userId);
+ const kickedUser = Users.findOneByUsername(username);
+
+ if (kickedUser == null) {
+ return Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { fields: { _id: 1 } });
+ if (!subscription) {
+ return Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_is_not_in_this_room', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ }
+ Meteor.call('removeUserFromRoom', { rid, username });
+};
+
+slashCommands.add('kick', Kick, {
+ description: 'Remove_someone_from_room',
+ params: '@username',
+ permission: 'remove-user',
+});
diff --git a/app/slashcommands-leave/index.js b/app/slashcommands-leave/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/slashcommands-leave/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-slashcommands-leave/server/index.js b/app/slashcommands-leave/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-leave/server/index.js
rename to app/slashcommands-leave/server/index.js
diff --git a/app/slashcommands-leave/server/leave.js b/app/slashcommands-leave/server/leave.js
new file mode 100644
index 000000000000..4e42e13d9174
--- /dev/null
+++ b/app/slashcommands-leave/server/leave.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Notifications } from '../../notifications';
+import { slashCommands } from '../../utils';
+
+/*
+* Leave is a named function that will replace /leave commands
+* @param {Object} message - The message object
+*/
+function Leave(command, params, item) {
+ if (command !== 'leave' && command !== 'part') {
+ return;
+ }
+
+ try {
+ Meteor.call('leaveRoom', item.rid);
+ } catch ({ error }) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__(error, null, Meteor.user().language),
+ });
+ }
+}
+
+slashCommands.add('leave', Leave, { description: 'Leave_the_current_channel' });
+slashCommands.add('part', Leave, { description: 'Leave_the_current_channel' });
diff --git a/app/slashcommands-me/index.js b/app/slashcommands-me/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/slashcommands-me/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-slashcommands-me/server/index.js b/app/slashcommands-me/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-me/server/index.js
rename to app/slashcommands-me/server/index.js
diff --git a/packages/rocketchat-slashcommands-me/server/me.js b/app/slashcommands-me/server/me.js
similarity index 76%
rename from packages/rocketchat-slashcommands-me/server/me.js
rename to app/slashcommands-me/server/me.js
index b02dcd1cdfd0..15321e8ba0ae 100644
--- a/packages/rocketchat-slashcommands-me/server/me.js
+++ b/app/slashcommands-me/server/me.js
@@ -1,12 +1,12 @@
import { Meteor } from 'meteor/meteor';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { slashCommands } from '../../utils';
import s from 'underscore.string';
/*
* Me is a named function that will replace /me commands
* @param {Object} message - The message object
*/
-RocketChat.slashCommands.add('me', function Me(command, params, item) {
+slashCommands.add('me', function Me(command, params, item) {
if (command !== 'me') {
return;
}
diff --git a/app/slashcommands-msg/index.js b/app/slashcommands-msg/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/slashcommands-msg/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/app/slashcommands-msg/server/index.js b/app/slashcommands-msg/server/index.js
new file mode 100644
index 000000000000..1199af15d79f
--- /dev/null
+++ b/app/slashcommands-msg/server/index.js
@@ -0,0 +1 @@
+import './server';
diff --git a/app/slashcommands-msg/server/server.js b/app/slashcommands-msg/server/server.js
new file mode 100644
index 000000000000..eed916392d6f
--- /dev/null
+++ b/app/slashcommands-msg/server/server.js
@@ -0,0 +1,56 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { slashCommands } from '../../utils';
+import { Notifications } from '../../notifications';
+import { Users } from '../../models';
+
+/*
+* Msg is a named function that will replace /msg commands
+*/
+
+function Msg(command, params, item) {
+ if (command !== 'msg' || !Match.test(params, String)) {
+ return;
+ }
+ const trimmedParams = params.trim();
+ const separator = trimmedParams.indexOf(' ');
+ const user = Meteor.users.findOne(Meteor.userId());
+ if (separator === -1) {
+ return Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_and_message_must_not_be_empty', null, user.language),
+ });
+ }
+ const message = trimmedParams.slice(separator + 1);
+ const targetUsernameOrig = trimmedParams.slice(0, separator);
+ const targetUsername = targetUsernameOrig.replace('@', '');
+ const targetUser = Users.findOneByUsername(targetUsername);
+ if (targetUser == null) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [targetUsernameOrig],
+ }, user.language),
+ });
+ return;
+ }
+ const { rid } = Meteor.call('createDirectMessage', targetUsername);
+ const msgObject = {
+ _id: Random.id(),
+ rid,
+ msg: message,
+ };
+ Meteor.call('sendMessage', msgObject);
+}
+
+slashCommands.add('msg', Msg, {
+ description: 'Direct_message_someone',
+ params: '@username ',
+});
diff --git a/app/slashcommands-mute/index.js b/app/slashcommands-mute/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/slashcommands-mute/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-slashcommands-mute/server/index.js b/app/slashcommands-mute/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-mute/server/index.js
rename to app/slashcommands-mute/server/index.js
diff --git a/app/slashcommands-mute/server/mute.js b/app/slashcommands-mute/server/mute.js
new file mode 100644
index 000000000000..f0ee7fce936f
--- /dev/null
+++ b/app/slashcommands-mute/server/mute.js
@@ -0,0 +1,57 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { slashCommands } from '../../utils';
+import { Users, Subscriptions } from '../../models';
+import { Notifications } from '../../notifications';
+
+/*
+* Mute is a named function that will replace /mute commands
+*/
+
+slashCommands.add('mute', function Mute(command, params, item) {
+ if (command !== 'mute' || !Match.test(params, String)) {
+ return;
+ }
+ const username = params.trim().replace('@', '');
+ if (username === '') {
+ return;
+ }
+ const userId = Meteor.userId();
+ const user = Meteor.users.findOne(userId);
+ const mutedUser = Users.findOneByUsername(username);
+ if (mutedUser == null) {
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ return;
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, mutedUser._id, { fields: { _id: 1 } });
+ if (!subscription) {
+ Notifications.notifyUser(userId, 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_is_not_in_this_room', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ return;
+ }
+ Meteor.call('muteUserInRoom', {
+ rid: item.rid,
+ username,
+ });
+}, {
+ description: 'Mute_someone_in_room',
+ params: '@username',
+});
diff --git a/app/slashcommands-mute/server/unmute.js b/app/slashcommands-mute/server/unmute.js
new file mode 100644
index 000000000000..01d29f2595ac
--- /dev/null
+++ b/app/slashcommands-mute/server/unmute.js
@@ -0,0 +1,55 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { slashCommands } from '../../utils';
+import { Users, Subscriptions } from '../../models';
+import { Notifications } from '../../notifications';
+
+/*
+* Unmute is a named function that will replace /unmute commands
+*/
+
+slashCommands.add('unmute', function Unmute(command, params, item) {
+
+ if (command !== 'unmute' || !Match.test(params, String)) {
+ return;
+ }
+ const username = params.trim().replace('@', '');
+ if (username === '') {
+ return;
+ }
+ const user = Meteor.users.findOne(Meteor.userId());
+ const unmutedUser = Users.findOneByUsername(username);
+ if (unmutedUser == null) {
+ return Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ }
+
+ const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, unmutedUser._id, { fields: { _id: 1 } });
+ if (!subscription) {
+ return Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date,
+ msg: TAPi18n.__('Username_is_not_in_this_room', {
+ postProcess: 'sprintf',
+ sprintf: [username],
+ }, user.language),
+ });
+ }
+ Meteor.call('unmuteUserInRoom', {
+ rid: item.rid,
+ username,
+ });
+}, {
+ description: 'Unmute_someone_in_room',
+ params: '@username',
+});
diff --git a/app/slashcommands-open/client/client.js b/app/slashcommands-open/client/client.js
new file mode 100644
index 000000000000..a31118ccc60d
--- /dev/null
+++ b/app/slashcommands-open/client/client.js
@@ -0,0 +1,54 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { slashCommands, roomTypes } from '../../utils';
+import { ChatSubscription, Subscriptions } from '../../models';
+
+function Open(command, params /* , item*/) {
+ const dict = {
+ '#': ['c', 'p'],
+ '@': ['d'],
+ };
+
+ if (command !== 'open' || !Match.test(params, String)) {
+ return;
+ }
+
+ let room = params.trim();
+ const type = dict[room[0]];
+ room = room.replace(/#|@/, '');
+
+ const query = {
+ name: room,
+ };
+
+ if (type) {
+ query.t = {
+ $in: type,
+ };
+ }
+
+ const subscription = ChatSubscription.findOne(query);
+
+ if (subscription) {
+ roomTypes.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams);
+ }
+
+ if (type && type.indexOf('d') === -1) {
+ return;
+ }
+ return Meteor.call('createDirectMessage', room, function(err) {
+ if (err) {
+ return;
+ }
+ const subscription = Subscriptions.findOne(query);
+ roomTypes.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams);
+ });
+
+}
+
+slashCommands.add('open', Open, {
+ description: 'Opens_a_channel_group_or_direct_message',
+ params: 'room_name',
+ clientOnly: true,
+});
diff --git a/packages/rocketchat-slashcommands-kick/client/index.js b/app/slashcommands-open/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-kick/client/index.js
rename to app/slashcommands-open/client/index.js
diff --git a/app/slashcommands-open/index.js b/app/slashcommands-open/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/slashcommands-open/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/packages/rocketchat-slashcommands-topic/client/index.js b/app/slashcommands-topic/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-topic/client/index.js
rename to app/slashcommands-topic/client/index.js
diff --git a/app/slashcommands-topic/lib/topic.js b/app/slashcommands-topic/lib/topic.js
new file mode 100644
index 000000000000..07a8a941fd2f
--- /dev/null
+++ b/app/slashcommands-topic/lib/topic.js
@@ -0,0 +1,34 @@
+import { Meteor } from 'meteor/meteor';
+import { handleError, slashCommands } from '../../utils';
+import { ChatRoom } from '../../models';
+import { callbacks } from '../../callbacks';
+import { hasPermission } from '../../authorization';
+/*
+ * Join is a named function that will replace /topic commands
+ * @param {Object} message - The message object
+ */
+
+function Topic(command, params, item) {
+ if (command === 'topic') {
+ if ((Meteor.isClient && hasPermission('edit-room', item.rid)) || (Meteor.isServer && hasPermission(Meteor.userId(), 'edit-room', item.rid))) {
+ Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err) => {
+ if (err) {
+ if (Meteor.isClient) {
+ return handleError(err);
+ } else {
+ throw err;
+ }
+ }
+
+ if (Meteor.isClient) {
+ callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid));
+ }
+ });
+ }
+ }
+}
+
+slashCommands.add('topic', Topic, {
+ description: 'Slash_Topic_Description',
+ params: 'Slash_Topic_Params',
+});
diff --git a/packages/rocketchat-slashcommands-topic/server/index.js b/app/slashcommands-topic/server/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-topic/server/index.js
rename to app/slashcommands-topic/server/index.js
diff --git a/app/slashcommands-unarchiveroom/client/client.js b/app/slashcommands-unarchiveroom/client/client.js
new file mode 100644
index 000000000000..36cbcf831305
--- /dev/null
+++ b/app/slashcommands-unarchiveroom/client/client.js
@@ -0,0 +1,6 @@
+import { slashCommands } from '../../utils';
+
+slashCommands.add('unarchive', null, {
+ description: 'Unarchive',
+ params: '#channel',
+});
diff --git a/packages/rocketchat-slashcommands-open/client/index.js b/app/slashcommands-unarchiveroom/client/index.js
similarity index 100%
rename from packages/rocketchat-slashcommands-open/client/index.js
rename to app/slashcommands-unarchiveroom/client/index.js
diff --git a/app/slashcommands-unarchiveroom/server/index.js b/app/slashcommands-unarchiveroom/server/index.js
new file mode 100644
index 000000000000..1199af15d79f
--- /dev/null
+++ b/app/slashcommands-unarchiveroom/server/index.js
@@ -0,0 +1 @@
+import './server';
diff --git a/app/slashcommands-unarchiveroom/server/server.js b/app/slashcommands-unarchiveroom/server/server.js
new file mode 100644
index 000000000000..6ddd880c6c91
--- /dev/null
+++ b/app/slashcommands-unarchiveroom/server/server.js
@@ -0,0 +1,76 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Random } from 'meteor/random';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { Rooms, Messages } from '../../models';
+import { slashCommands } from '../../utils';
+import { Notifications } from '../../notifications';
+
+function Unarchive(command, params, item) {
+ if (command !== 'unarchive' || !Match.test(params, String)) {
+ return;
+ }
+
+ let channel = params.trim();
+ let room;
+
+ if (channel === '') {
+ room = Rooms.findOneById(item.rid);
+ channel = room.name;
+ } else {
+ channel = channel.replace('#', '');
+ room = Rooms.findOneByName(channel);
+ }
+
+ const user = Meteor.users.findOne(Meteor.userId());
+
+ if (!room) {
+ return Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_doesnt_exist', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ }
+
+ // You can not archive direct messages.
+ if (room.t === 'd') {
+ return;
+ }
+
+ if (!room.archived) {
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_already_Unarchived', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+ return;
+ }
+
+ Meteor.call('unarchiveRoom', room._id);
+
+ Messages.createRoomUnarchivedByRoomIdAndUser(room._id, Meteor.user());
+ Notifications.notifyUser(Meteor.userId(), 'message', {
+ _id: Random.id(),
+ rid: item.rid,
+ ts: new Date(),
+ msg: TAPi18n.__('Channel_Unarchived', {
+ postProcess: 'sprintf',
+ sprintf: [channel],
+ }, user.language),
+ });
+
+ return Unarchive;
+}
+
+slashCommands.add('unarchive', Unarchive, {
+ description: 'Unarchive',
+ params: '#channel',
+});
diff --git a/packages/rocketchat-slider/README.md b/app/slider/README.md
similarity index 100%
rename from packages/rocketchat-slider/README.md
rename to app/slider/README.md
diff --git a/packages/rocketchat-slider/client/index.js b/app/slider/client/index.js
similarity index 100%
rename from packages/rocketchat-slider/client/index.js
rename to app/slider/client/index.js
diff --git a/packages/rocketchat-slider/client/rocketchat-slider.html b/app/slider/client/rocketchat-slider.html
similarity index 100%
rename from packages/rocketchat-slider/client/rocketchat-slider.html
rename to app/slider/client/rocketchat-slider.html
diff --git a/packages/rocketchat-slider/client/rocketchat-slider.js b/app/slider/client/rocketchat-slider.js
similarity index 100%
rename from packages/rocketchat-slider/client/rocketchat-slider.js
rename to app/slider/client/rocketchat-slider.js
diff --git a/app/slider/index.js b/app/slider/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/slider/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/app/smarsh-connector/index.js b/app/smarsh-connector/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/smarsh-connector/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/packages/rocketchat-smarsh-connector/server/functions/generateEml.js b/app/smarsh-connector/server/functions/generateEml.js
similarity index 82%
rename from packages/rocketchat-smarsh-connector/server/functions/generateEml.js
rename to app/smarsh-connector/server/functions/generateEml.js
index 561650ca7b58..105ec20b5b21 100644
--- a/packages/rocketchat-smarsh-connector/server/functions/generateEml.js
+++ b/app/smarsh-connector/server/functions/generateEml.js
@@ -1,6 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../settings';
+import { Rooms, Messages, Users, SmarshHistory } from '../../../models';
+import { MessageTypes } from '../../../ui-utils';
+import { smarsh } from '../lib/rocketchat';
import _ from 'underscore';
import moment from 'moment';
import 'moment-timezone';
@@ -16,20 +19,20 @@ const closetd = '';
function _getLink(attachment) {
const url = attachment.title_link.replace(/ /g, '%20');
- if (Meteor.settings.public.sandstorm || url.match(/^(https?:)?\/\//i)) {
+ if (url.match(/^(https?:)?\/\//i)) {
return url;
} else {
return Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url;
}
}
-RocketChat.smarsh.generateEml = () => {
+smarsh.generateEml = () => {
Meteor.defer(() => {
- const smarshMissingEmail = RocketChat.settings.get('Smarsh_MissingEmail_Email');
- const timeZone = RocketChat.settings.get('Smarsh_Timezone');
+ const smarshMissingEmail = settings.get('Smarsh_MissingEmail_Email');
+ const timeZone = settings.get('Smarsh_Timezone');
- RocketChat.models.Rooms.find().forEach((room) => {
- const smarshHistory = RocketChat.smarsh.History.findOne({ _id: room._id });
+ Rooms.find().forEach((room) => {
+ const smarshHistory = SmarshHistory.findOne({ _id: room._id });
const query = { rid: room._id };
if (smarshHistory) {
@@ -46,7 +49,7 @@ RocketChat.smarsh.generateEml = () => {
room: room.name ? `#${ room.name }` : `Direct Message Between: ${ room.usernames.join(' & ') }`,
};
- RocketChat.models.Messages.find(query).forEach((message) => {
+ Messages.find(query).forEach((message) => {
rows.push(opentr);
// The timestamp
@@ -56,7 +59,7 @@ RocketChat.smarsh.generateEml = () => {
// The sender
rows.push(open20td);
- const sender = RocketChat.models.Users.findOne({ _id: message.u._id });
+ const sender = Users.findOne({ _id: message.u._id });
if (data.users.indexOf(sender._id) === -1) {
data.users.push(sender._id);
}
@@ -73,7 +76,7 @@ RocketChat.smarsh.generateEml = () => {
rows.push(open60td);
data.msgs++;
if (message.t) {
- const messageType = RocketChat.MessageTypes.getType(message);
+ const messageType = MessageTypes.getType(message);
if (messageType) {
rows.push(TAPi18n.__(messageType.message, messageType.data ? messageType.data(message) : '', 'en'));
} else {
@@ -106,13 +109,13 @@ RocketChat.smarsh.generateEml = () => {
if (rows.length !== 0) {
const result = start + rows.join('') + end;
- RocketChat.smarsh.History.upsert({ _id: room._id }, {
+ SmarshHistory.upsert({ _id: room._id }, {
_id: room._id,
lastRan: date,
lastResult: result,
});
- RocketChat.smarsh.sendEmail({
+ smarsh.sendEmail({
body: result,
subject: `Rocket.Chat, ${ data.users.length } Users, ${ data.msgs } Messages, ${ data.files.length } Files, ${ data.time } Minutes, in ${ data.room }`,
files: data.files,
diff --git a/app/smarsh-connector/server/functions/sendEmail.js b/app/smarsh-connector/server/functions/sendEmail.js
new file mode 100644
index 000000000000..6c5b59e29ef4
--- /dev/null
+++ b/app/smarsh-connector/server/functions/sendEmail.js
@@ -0,0 +1,36 @@
+// Expects the following details:
+// {
+// body: '',
+// subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random',
+// files: ['i3nc9l3mn']
+// }
+import _ from 'underscore';
+import * as Mailer from '../../../mailer';
+import { Uploads } from '../../../models';
+import { settings } from '../../../settings';
+import { UploadFS } from 'meteor/jalik:ufs';
+import { smarsh } from '../lib/rocketchat';
+
+smarsh.sendEmail = (data) => {
+ const attachments = [];
+
+ _.each(data.files, (fileId) => {
+ const file = Uploads.findOneById(fileId);
+ if (file.store === 'rocketchat_uploads' || file.store === 'fileSystem') {
+ const rs = UploadFS.getStore(file.store).getReadStream(fileId, file);
+ attachments.push({
+ filename: file.name,
+ streamSource: rs,
+ });
+ }
+ });
+
+
+ Mailer.sendNoWrap({
+ to: settings.get('Smarsh_Email'),
+ from: settings.get('From_Email'),
+ subject: data.subject,
+ html: data.body,
+ attachments,
+ });
+};
diff --git a/app/smarsh-connector/server/index.js b/app/smarsh-connector/server/index.js
new file mode 100644
index 000000000000..7c301809bb58
--- /dev/null
+++ b/app/smarsh-connector/server/index.js
@@ -0,0 +1,4 @@
+import './settings';
+import './functions/sendEmail';
+import './functions/generateEml';
+import './startup';
diff --git a/app/smarsh-connector/server/lib/rocketchat.js b/app/smarsh-connector/server/lib/rocketchat.js
new file mode 100644
index 000000000000..f8f593dbebcc
--- /dev/null
+++ b/app/smarsh-connector/server/lib/rocketchat.js
@@ -0,0 +1 @@
+export const smarsh = {};
diff --git a/app/smarsh-connector/server/settings.js b/app/smarsh-connector/server/settings.js
new file mode 100644
index 000000000000..2eac794cbb7c
--- /dev/null
+++ b/app/smarsh-connector/server/settings.js
@@ -0,0 +1,62 @@
+import { settings } from '../../settings';
+import moment from 'moment';
+import 'moment-timezone';
+
+settings.addGroup('Smarsh', function addSettings() {
+ this.add('Smarsh_Enabled', false, {
+ type: 'boolean',
+ i18nLabel: 'Smarsh_Enabled',
+ enableQuery: {
+ _id: 'From_Email',
+ value: {
+ $exists: 1,
+ $ne: '',
+ },
+ },
+ });
+ this.add('Smarsh_Email', '', {
+ type: 'string',
+ i18nLabel: 'Smarsh_Email',
+ placeholder: 'email@domain.com',
+ });
+ this.add('Smarsh_MissingEmail_Email', 'no-email@example.com', {
+ type: 'string',
+ i18nLabel: 'Smarsh_MissingEmail_Email',
+ placeholder: 'no-email@example.com',
+ });
+
+ const zoneValues = moment.tz.names().map(function _timeZonesToSettings(name) {
+ return {
+ key: name,
+ i18nLabel: name,
+ };
+ });
+ this.add('Smarsh_Timezone', 'America/Los_Angeles', {
+ type: 'select',
+ values: zoneValues,
+ });
+
+ this.add('Smarsh_Interval', 'every_30_minutes', {
+ type: 'select',
+ values: [{
+ key: 'every_30_seconds',
+ i18nLabel: 'every_30_seconds',
+ }, {
+ key: 'every_30_minutes',
+ i18nLabel: 'every_30_minutes',
+ }, {
+ key: 'every_1_hours',
+ i18nLabel: 'every_hour',
+ }, {
+ key: 'every_6_hours',
+ i18nLabel: 'every_six_hours',
+ }],
+ enableQuery: {
+ _id: 'From_Email',
+ value: {
+ $exists: 1,
+ $ne: '',
+ },
+ },
+ });
+});
diff --git a/app/smarsh-connector/server/startup.js b/app/smarsh-connector/server/startup.js
new file mode 100644
index 000000000000..763a4eb7a204
--- /dev/null
+++ b/app/smarsh-connector/server/startup.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+import { SyncedCron } from 'meteor/littledata:synced-cron';
+import { smarsh } from './lib/rocketchat';
+import _ from 'underscore';
+
+const smarshJobName = 'Smarsh EML Connector';
+
+const _addSmarshSyncedCronJob = _.debounce(Meteor.bindEnvironment(function __addSmarshSyncedCronJobDebounced() {
+ if (SyncedCron.nextScheduledAtDate(smarshJobName)) {
+ SyncedCron.remove(smarshJobName);
+ }
+
+ if (settings.get('Smarsh_Enabled') && settings.get('Smarsh_Email') !== '' && settings.get('From_Email') !== '') {
+ SyncedCron.add({
+ name: smarshJobName,
+ schedule: (parser) => parser.text(settings.get('Smarsh_Interval').replace(/_/g, ' ')),
+ job: smarsh.generateEml,
+ });
+ }
+}), 500);
+
+Meteor.startup(() => {
+ Meteor.defer(() => {
+ _addSmarshSyncedCronJob();
+
+ settings.get('Smarsh_Interval', _addSmarshSyncedCronJob);
+ settings.get('Smarsh_Enabled', _addSmarshSyncedCronJob);
+ settings.get('Smarsh_Email', _addSmarshSyncedCronJob);
+ settings.get('From_Email', _addSmarshSyncedCronJob);
+ });
+});
diff --git a/packages/rocketchat-sms/README.md b/app/sms/README.md
similarity index 100%
rename from packages/rocketchat-sms/README.md
rename to app/sms/README.md
diff --git a/app/sms/index.js b/app/sms/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/sms/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/app/sms/server/SMS.js b/app/sms/server/SMS.js
new file mode 100644
index 000000000000..0d4adbc91b8c
--- /dev/null
+++ b/app/sms/server/SMS.js
@@ -0,0 +1,25 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+export const SMS = {
+ enabled: false,
+ services: {},
+ accountSid: null,
+ authToken: null,
+ fromNumber: null,
+
+ registerService(name, service) {
+ this.services[name] = service;
+ },
+
+ getService(name) {
+ if (!this.services[name]) {
+ throw new Meteor.Error('error-sms-service-not-configured');
+ }
+ return new this.services[name](this.accountSid, this.authToken, this.fromNumber);
+ },
+};
+
+settings.get('SMS_Enabled', function(key, value) {
+ SMS.enabled = value;
+});
diff --git a/app/sms/server/index.js b/app/sms/server/index.js
new file mode 100644
index 000000000000..2fbb08a89b76
--- /dev/null
+++ b/app/sms/server/index.js
@@ -0,0 +1,4 @@
+import './settings';
+export { SMS } from './SMS';
+import './services/twilio';
+import './services/voxtelesys';
diff --git a/packages/rocketchat-sms/server/services/twilio.js b/app/sms/server/services/twilio.js
similarity index 86%
rename from packages/rocketchat-sms/server/services/twilio.js
rename to app/sms/server/services/twilio.js
index 615df3ea13e4..12d256cc7bb6 100644
--- a/packages/rocketchat-sms/server/services/twilio.js
+++ b/app/sms/server/services/twilio.js
@@ -1,10 +1,11 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../../settings';
+import { SMS } from '../SMS';
import twilio from 'twilio';
class Twilio {
constructor() {
- this.accountSid = RocketChat.settings.get('SMS_Twilio_Account_SID');
- this.authToken = RocketChat.settings.get('SMS_Twilio_authToken');
+ this.accountSid = settings.get('SMS_Twilio_Account_SID');
+ this.authToken = settings.get('SMS_Twilio_authToken');
}
parse(data) {
let numMedia = 0;
@@ -85,4 +86,4 @@ class Twilio {
}
}
-RocketChat.SMS.registerService('twilio', Twilio);
+SMS.registerService('twilio', Twilio);
diff --git a/app/sms/server/services/voxtelesys.js b/app/sms/server/services/voxtelesys.js
new file mode 100644
index 000000000000..93adfcd37ad0
--- /dev/null
+++ b/app/sms/server/services/voxtelesys.js
@@ -0,0 +1,80 @@
+import { HTTP } from 'meteor/http';
+
+import { settings } from '../../../settings';
+
+import { SMS } from '../SMS';
+
+class Voxtelesys {
+ constructor() {
+ this.authToken = settings.get('SMS_Voxtelesys_authToken');
+ this.URL = settings.get('SMS_Voxtelesys_URL');
+ }
+ parse(data) {
+ const returnData = {
+ from: data.from,
+ to: data.to,
+ body: data.body,
+
+ extra: {
+ received_at: data.received_at,
+ },
+ };
+
+ returnData.media = []; /* MMS not currently supported */
+
+ return returnData;
+ }
+ send(fromNumber, toNumber, message) {
+ const options = {
+ timeout: 30000,
+ followRedirects: false,
+ headers: {
+ Authorization: `Bearer ${ this.authToken }`,
+ },
+ data: {
+ to: [toNumber],
+ from: fromNumber,
+ body: message,
+ },
+ npmRequestOptions: {
+ agentOptions: {
+ ecdhCurve: 'auto',
+ },
+ forever: true,
+ },
+ };
+
+ try {
+ HTTP.call('POST', this.URL || 'https://smsapi.voxtelesys.net/api/v1/sms', options);
+ } catch (error) {
+ console.error(`Error connecting to Voxtelesys SMS API: ${ error }`);
+ }
+ }
+ response(/* message */) {
+ return {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: {
+ success: true,
+ },
+ };
+ }
+ error(error) {
+ let message = '';
+ if (error.reason) {
+ message = error.reason;
+ }
+ return {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: {
+ success: false,
+ error: message,
+ },
+ };
+ }
+}
+
+SMS.registerService('voxtelesys', Voxtelesys);
diff --git a/app/sms/server/settings.js b/app/sms/server/settings.js
new file mode 100644
index 000000000000..fdbb74fc5d3d
--- /dev/null
+++ b/app/sms/server/settings.js
@@ -0,0 +1,64 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(function() {
+ settings.addGroup('SMS', function() {
+ this.add('SMS_Enabled', false, {
+ type: 'boolean',
+ i18nLabel: 'Enabled',
+ });
+
+ this.add('SMS_Service', 'twilio', {
+ type: 'select',
+ values: [
+ {
+ key: 'twilio',
+ i18nLabel: 'Twilio',
+ },
+ {
+ key: 'voxtelesys',
+ i18nLabel: 'Voxtelesys',
+ },
+ ],
+ i18nLabel: 'Service',
+ });
+
+ this.section('Twilio', function() {
+ this.add('SMS_Twilio_Account_SID', '', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SMS_Service',
+ value: 'twilio',
+ },
+ i18nLabel: 'Account_SID',
+ });
+ this.add('SMS_Twilio_authToken', '', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SMS_Service',
+ value: 'twilio',
+ },
+ i18nLabel: 'Auth_Token',
+ });
+ });
+
+ this.section('Voxtelesys', function() {
+ this.add('SMS_Voxtelesys_authToken', '', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SMS_Service',
+ value: 'voxtelesys',
+ },
+ i18nLabel: 'Auth_Token',
+ });
+ this.add('SMS_Voxtelesys_URL', 'https://smsapi.voxtelesys.net/api/v1/sms', {
+ type: 'string',
+ enableQuery: {
+ _id: 'SMS_Service',
+ value: 'voxtelesys',
+ },
+ i18nLabel: 'URL',
+ });
+ });
+ });
+});
diff --git a/packages/rocketchat-spotify/client/index.js b/app/spotify/client/index.js
similarity index 100%
rename from packages/rocketchat-spotify/client/index.js
rename to app/spotify/client/index.js
diff --git a/packages/rocketchat-spotify/lib/client/oembedSpotifyWidget.html b/app/spotify/lib/client/oembedSpotifyWidget.html
similarity index 92%
rename from packages/rocketchat-spotify/lib/client/oembedSpotifyWidget.html
rename to app/spotify/lib/client/oembedSpotifyWidget.html
index c66cfca2efdc..271c13e98a25 100644
--- a/packages/rocketchat-spotify/lib/client/oembedSpotifyWidget.html
+++ b/app/spotify/lib/client/oembedSpotifyWidget.html
@@ -1,6 +1,6 @@
{{#if parsedUrl}}
-
+
Spotify
{{#if match meta.ogAudio "spotify:artist:\\S+"}}
{{{meta.ogTitle}}}
diff --git a/packages/rocketchat-spotify/lib/client/widget.js b/app/spotify/lib/client/widget.js
similarity index 100%
rename from packages/rocketchat-spotify/lib/client/widget.js
rename to app/spotify/lib/client/widget.js
diff --git a/packages/rocketchat-spotify/lib/spotify.js b/app/spotify/lib/spotify.js
similarity index 87%
rename from packages/rocketchat-spotify/lib/spotify.js
rename to app/spotify/lib/spotify.js
index 979d54192b8d..2e2bcea6d02b 100644
--- a/packages/rocketchat-spotify/lib/spotify.js
+++ b/app/spotify/lib/spotify.js
@@ -2,7 +2,7 @@
* Spotify a named function that will process Spotify links or syntaxes (ex: spotify:track:1q6IK1l4qpYykOaWaLJkWG)
* @param {Object} message - The message object
*/
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { callbacks } from '../../callbacks';
import _ from 'underscore';
import s from 'underscore.string';
@@ -73,6 +73,5 @@ class Spotify {
}
}
-RocketChat.callbacks.add('beforeSaveMessage', Spotify.transform, RocketChat.callbacks.priority.LOW, 'spotify-save');
-RocketChat.callbacks.add('renderMessage', Spotify.render, RocketChat.callbacks.priority.MEDIUM, 'spotify-render');
-RocketChat.Spotify = Spotify;
+callbacks.add('beforeSaveMessage', Spotify.transform, callbacks.priority.LOW, 'spotify-save');
+callbacks.add('renderMessage', Spotify.render, callbacks.priority.MEDIUM, 'spotify-render');
diff --git a/packages/rocketchat-spotify/server/index.js b/app/spotify/server/index.js
similarity index 100%
rename from packages/rocketchat-spotify/server/index.js
rename to app/spotify/server/index.js
diff --git a/app/statistics/index.js b/app/statistics/index.js
new file mode 100644
index 000000000000..ca39cd0df4b1
--- /dev/null
+++ b/app/statistics/index.js
@@ -0,0 +1 @@
+export * from './server/index';
diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js
new file mode 100644
index 000000000000..2553ba87fca1
--- /dev/null
+++ b/app/statistics/server/functions/get.js
@@ -0,0 +1,159 @@
+import _ from 'underscore';
+import os from 'os';
+
+import { Meteor } from 'meteor/meteor';
+import { MongoInternals } from 'meteor/mongo';
+import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
+
+import {
+ Sessions,
+ Settings,
+ Users,
+ Rooms,
+ Subscriptions,
+ Uploads,
+ Messages,
+ LivechatVisitors,
+} from '../../../models/server';
+import { settings } from '../../../settings/server';
+import { Info } from '../../../utils/server';
+import { Migrations } from '../../../migrations/server';
+
+import { statistics } from '../statisticsNamespace';
+
+const wizardFields = [
+ 'Organization_Type',
+ 'Organization_Name',
+ 'Industry',
+ 'Size',
+ 'Country',
+ 'Website',
+ 'Site_Name',
+ 'Language',
+ 'Server_Type',
+ 'Allow_Marketing_Emails',
+ 'Register_Server',
+];
+
+statistics.get = function _getStatistics() {
+ const statistics = {};
+
+ // Setup Wizard
+ statistics.wizard = {};
+ wizardFields.forEach((field) => {
+ const record = Settings.findOne(field);
+ if (record) {
+ const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase());
+ statistics.wizard[wizardField] = record.value;
+ }
+ });
+
+ const firstUser = Users.getOldest({ name: 1, emails: 1 });
+ statistics.wizard.contactName = firstUser && firstUser.name;
+ statistics.wizard.contactEmail = firstUser && firstUser.emails && firstUser.emails[0].address;
+
+ if (settings.get('Organization_Email')) {
+ statistics.wizard.contactEmail = settings.get('Organization_Email');
+ }
+
+ // Version
+ statistics.uniqueId = settings.get('uniqueID');
+ if (Settings.findOne('uniqueID')) {
+ statistics.installedAt = Settings.findOne('uniqueID').createdAt;
+ }
+
+ if (Info) {
+ statistics.version = Info.version;
+ statistics.tag = Info.tag;
+ statistics.branch = Info.branch;
+ }
+
+ // User statistics
+ statistics.totalUsers = Meteor.users.find().count();
+ statistics.activeUsers = Meteor.users.find({ active: true }).count();
+ statistics.nonActiveUsers = statistics.totalUsers - statistics.activeUsers;
+ statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count();
+ statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count();
+ statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers;
+ statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers;
+
+ // Room statistics
+ statistics.totalRooms = Rooms.find().count();
+ statistics.totalChannels = Rooms.findByType('c').count();
+ statistics.totalPrivateGroups = Rooms.findByType('p').count();
+ statistics.totalDirect = Rooms.findByType('d').count();
+ statistics.totalLivechat = Rooms.findByType('l').count();
+ statistics.totalDiscussions = Rooms.countDiscussions();
+ statistics.totalThreads = Messages.countThreads();
+
+ // livechat visitors
+ statistics.totalLivechatVisitors = LivechatVisitors.find().count();
+
+ // livechat agents
+ statistics.totalLivechatAgents = Users.findAgents().count();
+
+ // livechat enabled
+ statistics.livechatEnabled = settings.get('Livechat_enabled');
+
+ // Message statistics
+ statistics.totalMessages = Messages.find().count();
+ statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0);
+ statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0);
+ statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0);
+ statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0);
+
+ statistics.lastLogin = Users.getLastLogin();
+ statistics.lastMessageSentAt = Messages.getLastTimestamp();
+ statistics.lastSeenSubscription = Subscriptions.getLastSeen();
+
+ statistics.os = {
+ type: os.type(),
+ platform: os.platform(),
+ arch: os.arch(),
+ release: os.release(),
+ uptime: os.uptime(),
+ loadavg: os.loadavg(),
+ totalmem: os.totalmem(),
+ freemem: os.freemem(),
+ cpus: os.cpus(),
+ };
+
+ statistics.process = {
+ nodeVersion: process.version,
+ pid: process.pid,
+ uptime: process.uptime(),
+ };
+
+ statistics.deploy = {
+ method: process.env.DEPLOY_METHOD || 'tar',
+ platform: process.env.DEPLOY_PLATFORM || 'selfinstall',
+ };
+
+ statistics.uploadsTotal = Uploads.find().count();
+ const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } } }]).toArray());
+ statistics.uploadsTotalSize = result ? result.total : 0;
+
+ statistics.migration = Migrations._getControl();
+ statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count();
+
+ const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
+
+ if (mongo._oplogHandle && mongo._oplogHandle.onOplogEntry) {
+ statistics.oplogEnabled = true;
+ }
+
+ try {
+ const { version, storageEngine } = Promise.await(mongo.db.command({ serverStatus: 1 }));
+ statistics.mongoVersion = version;
+ statistics.mongoStorageEngine = storageEngine.name;
+ } catch (e) {
+ console.error('Error getting MongoDB info');
+ }
+
+ statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday();
+ statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth();
+ statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday();
+ statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday();
+
+ return statistics;
+};
diff --git a/app/statistics/server/functions/save.js b/app/statistics/server/functions/save.js
new file mode 100644
index 000000000000..04bb113c7f09
--- /dev/null
+++ b/app/statistics/server/functions/save.js
@@ -0,0 +1,9 @@
+import { Statistics } from '../../../models';
+import { statistics } from '../statisticsNamespace';
+
+statistics.save = function() {
+ const rcStatistics = statistics.get();
+ rcStatistics.createdAt = new Date;
+ Statistics.insert(rcStatistics);
+ return rcStatistics;
+};
diff --git a/app/statistics/server/index.js b/app/statistics/server/index.js
new file mode 100644
index 000000000000..d3ea6b2089ff
--- /dev/null
+++ b/app/statistics/server/index.js
@@ -0,0 +1,5 @@
+export { statistics } from './statisticsNamespace';
+import './functions/get';
+import './functions/save';
+import './methods/getStatistics';
+import './startup/monitor';
diff --git a/packages/rocketchat-statistics/server/lib/SAUMonitor.js b/app/statistics/server/lib/SAUMonitor.js
similarity index 95%
rename from packages/rocketchat-statistics/server/lib/SAUMonitor.js
rename to app/statistics/server/lib/SAUMonitor.js
index dc66c06d4a16..5db2c6c518f9 100644
--- a/packages/rocketchat-statistics/server/lib/SAUMonitor.js
+++ b/app/statistics/server/lib/SAUMonitor.js
@@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import UAParser from 'ua-parser-js';
import { UAParserMobile } from './UAParserMobile';
-import { Sessions } from 'meteor/rocketchat:models';
-import { Logger } from 'meteor/rocketchat:logger';
+import { Sessions } from '../../../models';
+import { Logger } from '../../../logger';
import { SyncedCron } from 'meteor/littledata:synced-cron';
const getDateObj = (dateTime = new Date()) => ({
@@ -122,8 +122,10 @@ export class SAUMonitorClass {
Accounts.onLogout((info) => {
const sessionId = info.connection.id;
- const userId = info.user._id;
- Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId);
+ if (info.user) {
+ const userId = info.user._id;
+ Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId);
+ }
});
}
@@ -276,9 +278,7 @@ export class SAUMonitorClass {
}
if (Meteor.server.sessions[sessionId]) {
Object.keys(data).forEach((p) => {
- Object.defineProperty(Meteor.server.sessions[sessionId].connectionHandle, p, {
- value: data[p],
- });
+ Meteor.server.sessions[sessionId].connectionHandle = Object.assign({}, Meteor.server.sessions[sessionId].connectionHandle, { [p]: data[p] });
});
}
}
diff --git a/packages/rocketchat-statistics/server/lib/UAParserMobile.js b/app/statistics/server/lib/UAParserMobile.js
similarity index 100%
rename from packages/rocketchat-statistics/server/lib/UAParserMobile.js
rename to app/statistics/server/lib/UAParserMobile.js
diff --git a/app/statistics/server/methods/getStatistics.js b/app/statistics/server/methods/getStatistics.js
new file mode 100644
index 000000000000..58d99c7a8e6b
--- /dev/null
+++ b/app/statistics/server/methods/getStatistics.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { hasPermission } from '../../../authorization';
+import { Statistics } from '../../../models';
+import { statistics } from '../statisticsNamespace';
+
+Meteor.methods({
+ getStatistics(refresh) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' });
+ }
+
+ if (hasPermission(Meteor.userId(), 'view-statistics') !== true) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getStatistics' });
+ }
+
+ if (refresh) {
+ return statistics.save();
+ } else {
+ return Statistics.findLast();
+ }
+ },
+});
diff --git a/packages/rocketchat-statistics/server/startup/monitor.js b/app/statistics/server/startup/monitor.js
similarity index 100%
rename from packages/rocketchat-statistics/server/startup/monitor.js
rename to app/statistics/server/startup/monitor.js
diff --git a/app/statistics/server/statisticsNamespace.js b/app/statistics/server/statisticsNamespace.js
new file mode 100644
index 000000000000..bfd4f992e589
--- /dev/null
+++ b/app/statistics/server/statisticsNamespace.js
@@ -0,0 +1 @@
+export const statistics = {};
diff --git a/packages/rocketchat_theme/client/imports/components/alerts.css b/app/theme/client/imports/components/alerts.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/alerts.css
rename to app/theme/client/imports/components/alerts.css
diff --git a/packages/rocketchat_theme/client/imports/components/avatar.css b/app/theme/client/imports/components/avatar.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/avatar.css
rename to app/theme/client/imports/components/avatar.css
diff --git a/app/theme/client/imports/components/badge.css b/app/theme/client/imports/components/badge.css
new file mode 100644
index 000000000000..e8ab4257a6b3
--- /dev/null
+++ b/app/theme/client/imports/components/badge.css
@@ -0,0 +1,32 @@
+.badge {
+ display: flex;
+
+ min-width: 18px;
+ min-height: 18px;
+ padding: 2px 5px;
+
+ color: var(--badge-text-color);
+ border-radius: var(--badge-radius);
+ background-color: var(--badge-background);
+
+ font-size: var(--badge-text-size);
+ line-height: 1;
+ align-items: center;
+ justify-content: center;
+
+ &--unread {
+ margin: 0 3px;
+
+ white-space: nowrap;
+
+ background-color: var(--badge-unread-background);
+ }
+
+ &--user-mentions {
+ background-color: var(--badge-user-mentions-background);
+ }
+
+ &--group-mentions {
+ background-color: var(--badge-group-mentions-background);
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/components/chip.css b/app/theme/client/imports/components/chip.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/chip.css
rename to app/theme/client/imports/components/chip.css
diff --git a/packages/rocketchat_theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css
similarity index 88%
rename from packages/rocketchat_theme/client/imports/components/contextual-bar.css
rename to app/theme/client/imports/components/contextual-bar.css
index 0eabaad26ac4..43c1801d3f3c 100644
--- a/packages/rocketchat_theme/client/imports/components/contextual-bar.css
+++ b/app/theme/client/imports/components/contextual-bar.css
@@ -40,6 +40,11 @@
padding: var(--default-padding);
justify-content: space-between;
+ &--no-padding {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
& .section:not(:last-child) {
margin-bottom: 2rem;
}
@@ -51,16 +56,24 @@
padding: var(--default-padding);
- align-items: end;
+ border-bottom: solid 1px var(--color-gray-light);
+
+ background: var(--color-gray-lightest);
+
+ align-items: center;
justify-content: flex-end;
&-data {
display: flex;
+ overflow: hidden;
flex: 1 1;
align-items: center;
}
&-icon {
+
+ flex: 0 0 auto;
+
margin: 0 0.25rem;
font-size: 22px;
@@ -70,20 +83,53 @@
}
}
+ &-back-btn {
+ position: absolute;
+ left: 0;
+
+ width: auto;
+ height: auto;
+ margin: 0;
+ padding: 0;
+
+ font-size: 22px;
+ }
+
&-title {
+
+ overflow: hidden;
flex: 1;
margin: 0 0.25rem;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
font-size: 16px;
font-weight: 400;
}
+ &-description {
+
+ display: block;
+ flex: 1;
+
+ color: var(--color-gray);
+
+ font-size: 0.85rem;
+ font-weight: 400;
+ }
+
&-close-icon {
transform: rotate(45deg);
font-size: 20px;
}
+
+ &--empty {
+ border-bottom: none;
+ background: none;
+ }
}
& .search-form .rc-input__icon-svg {
@@ -111,7 +157,6 @@
top: 0;
width: 100%;
- max-width: var(--flex-tab-width);
animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
diff --git a/packages/rocketchat_theme/client/imports/components/emojiPicker.css b/app/theme/client/imports/components/emojiPicker.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/emojiPicker.css
rename to app/theme/client/imports/components/emojiPicker.css
diff --git a/packages/rocketchat_theme/client/imports/components/flex-nav.css b/app/theme/client/imports/components/flex-nav.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/flex-nav.css
rename to app/theme/client/imports/components/flex-nav.css
diff --git a/app/theme/client/imports/components/header.css b/app/theme/client/imports/components/header.css
new file mode 100644
index 000000000000..e0edd428cac7
--- /dev/null
+++ b/app/theme/client/imports/components/header.css
@@ -0,0 +1,531 @@
+.rc-header {
+
+ z-index: 10;
+
+ font-size: var(--text-heading-size);
+
+ .rc-badge {
+ position: absolute;
+ z-index: 1;
+ top: -2px;
+ left: var(--badge-size);
+
+ display: flex;
+
+ min-width: var(--badge-size);
+ height: var(--badge-size);
+
+ padding: 0 5px;
+
+ text-align: center;
+
+ color: white;
+ border-radius: calc(4 * var(--badge-font-size));
+ background: var(--rc-color-button-primary);
+ box-shadow: 0 0 0 2px #ffffff;
+
+ font-size: var(--badge-font-size);
+ font-weight: 600;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__first-icon {
+ display: flex;
+
+ width: 48px;
+
+ padding: 0 0.25rem;
+
+ cursor: pointer;
+
+ justify-content: center;
+ }
+
+ &--room {
+ padding: 1.25rem;
+
+ box-shadow: 0 1px 2px 0 rgba(31, 35, 41, 0.08);
+
+ font-size: var(--header-title-font-size);
+ }
+
+ &__wrap {
+ z-index: 2;
+
+ display: flex;
+
+ flex: 0 0 auto;
+
+ height: 36px;
+
+ white-space: nowrap;
+
+ background-color: var(--header-background-color);
+
+ align-items: center;
+
+ justify-content: space-between;
+ }
+
+ &__block {
+ display: flex;
+
+ margin: 0 -0.5rem;
+
+ padding: 0 0.5rem;
+
+ align-items: center;
+
+ &-action {
+ position: relative;
+
+ cursor: pointer;
+
+ & + & {
+ &::before,
+ &::after {
+ position: absolute;
+
+ height: 1rem;
+
+ content: "";
+ }
+
+ &::before {
+ border-left: 1px #cccccc solid;
+ }
+
+ .rtl & {
+ &::after {
+ border-right: 1px var(--color-gray) solid;
+ }
+
+ &::before {
+ border-left: 0;
+ }
+ }
+ }
+ }
+ }
+
+ &__content {
+ display: flex;
+
+ width: 100%;
+ margin-left: 0;
+ align-items: center;
+ }
+
+ &--burger {
+ display: none;
+ }
+
+ &__name {
+ overflow: hidden;
+
+ padding-bottom: 1px;
+
+ text-overflow: ellipsis;
+ }
+
+ &__section-title {
+ color: var(--header-title-username-color-darker);
+
+ font-weight: var(--header-title-username-weight);
+ }
+
+ &__section-help {
+ flex: 1 1;
+ }
+
+ &__section-button {
+ display: flex;
+ flex: 1 1 100%;
+ justify-content: flex-end;
+
+ /* max-width: use this to allign the buttons with the form */
+ }
+
+ &__section-button > button {
+ margin: 0.3rem;
+ }
+
+ &-title {
+ display: flex;
+ flex-direction: column;
+
+ width: 100%;
+ }
+
+ &__data {
+
+ overflow: hidden;
+
+ flex-direction: column;
+
+ width: 1px;
+
+ margin: 0 0.5rem;
+
+ text-overflow: ellipsis;
+
+ color: var(--header-title-username-color-darker);
+
+ font-weight: var(--header-title-username-weight);
+ align-items: flex-start;
+
+ flex-grow: 1;
+ }
+
+ &__username {
+ display: inline;
+
+ color: var(--header-title-status-color);
+ }
+
+ &__topic,
+ &__status {
+ color: var(--header-title-status-color);
+
+ font-size: var(--header-title-font-size--subtitle);
+ font-weight: var(--header-title-status-name-weight);
+ }
+
+ &__topic {
+ overflow: hidden;
+
+ width: 100%;
+ max-width: fit-content;
+
+ text-overflow: ellipsis;
+ }
+
+ &-visual-status {
+ text-transform: capitalize;
+ }
+
+ &__status {
+ display: flex;
+
+ align-items: center;
+
+ &-bullet {
+ width: var(--header-title-status-bullet-size);
+ height: var(--header-title-status-bullet-size);
+ margin-right: 0.25rem;
+
+ border-radius: var(--header-title-status-bullet-radius);
+
+ &--online {
+ background-color: var(--status-online);
+ }
+
+ &--away {
+ background-color: var(--status-away);
+ }
+
+ &--busy {
+ background-color: var(--status-busy);
+ }
+
+ &--invisible {
+ background-color: var(--status-invisible);
+ }
+
+ &--offline {
+ background-color: var(--status-invisible);
+ }
+ }
+ }
+
+ &__toggle-favorite {
+
+ padding: 0 0.25rem;
+
+ color: var(--header-toggle-favorite-star-color);
+
+ &.empty {
+ color: var(--header-toggle-favorite-color);
+
+ & > .rc-header__icon {
+ fill: none;
+ }
+ }
+
+ & > .rc-header__icon {
+ font-size: 2rem;
+ }
+
+ &:hover {
+ color: var(--header-toggle-favorite-star-color);
+ }
+ }
+
+ &__toggle-encryption {
+ color: var(--header-toggle-encryption-on-color);
+
+ &.empty {
+ color: var(--header-toggle-encryption-off-color);
+
+ & .rc-header__icon {
+ fill: none;
+ }
+ }
+
+ &:hover {
+ color: var(--header-toggle-encryption-on-color);
+ }
+ }
+
+ &__icon {
+ font-size: 1rem;
+ }
+
+ &__image {
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+ }
+
+ .rc-button {
+ height: 36px;
+ min-height: 36px;
+
+ margin: 0 0.25rem;
+ }
+}
+
+.rc-room-actions {
+ display: flex;
+
+ &__action,
+ &__more-action {
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+
+ margin: 0 6px;
+
+ cursor: pointer;
+ transition: all 0.3s;
+
+ font-size: 20px;
+ align-items: center;
+
+ &.active,
+ &:hover {
+ color: var(--rc-color-link-active);
+ }
+
+ &.enabled {
+ color: var(--header-toggle-encryption-on-color);
+ }
+
+ &.live {
+ position: relative;
+ }
+
+ &.live::before {
+
+ position: absolute;
+
+ z-index: 1;
+ right: -2px;
+ bottom: -1px;
+
+ display: block;
+
+ width: 10px;
+ width: var(--sidebar-account-status-bullet-size);
+ height: 10px;
+ height: var(--sidebar-account-status-bullet-size);
+
+ content: '';
+
+ animation: blink 1.5s ease-in-out infinite;
+
+ border-radius: 50%;
+ border-radius: var(--sidebar-account-status-bullet-radius);
+ background-color: #f5455c;
+ }
+ }
+
+ &__more {
+ &-action {
+ flex: 0 0 80px;
+
+ max-width: 80px;
+ margin: 8px;
+ }
+
+ &-container {
+ display: flex;
+
+ max-width: 480px;
+ margin: 0 -8px;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+ }
+
+ &__button {
+ color: inherit;
+
+ font-size: inherit;
+ }
+
+ &__description {
+ display: inline-block;
+ overflow: hidden;
+
+ width: 100%;
+ padding: 8px 0;
+
+ text-align: center;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ & + & {
+ border-left: 1px var(--color-gray) solid;
+
+ .rtl & {
+ border-right: 1px var(--color-gray) solid;
+ border-left: 0;
+ }
+ }
+}
+
+.tab-button-icon--star {
+ fill: none;
+}
+
+.tab-bugtton-icon--team {
+ font-size: 28px;
+}
+
+@media (width <= 500px) {
+ .rc-header {
+ &__visual-status {
+ display: none;
+ }
+
+ &__block {
+ margin: 0 0.25rem;
+ }
+
+ &__block-action {
+ order: 2;
+
+ & + & {
+ border-left: 1px var(--color-gray) solid;
+
+ .rtl & {
+ border-right: 1px var(--color-gray) solid;
+ border-left: 0;
+ }
+ }
+ }
+
+ &__favorite {
+ order: 1;
+ }
+
+ &__data {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &__status {
+ margin: 0 0.5rem;
+ }
+
+ &__image {
+ width: 20px;
+ height: 20px;
+ }
+
+ &--burger {
+ display: flex;
+
+ margin: 0;
+ padding: 0;
+ }
+ }
+}
+
+.embedded-view .room-container .rc-header--burger {
+ display: none;
+}
+
+.burger {
+
+ cursor: pointer;
+ transition: transform 0.2s ease-out 0.1s;
+ will-change: transform;
+
+ & .burger__line {
+ display: block;
+
+ width: 20px;
+ height: 2px;
+ margin: 5px 0;
+
+ transition: transform 0.2s ease-out;
+
+ opacity: 0.8;
+ }
+
+ & .unread-burger-alert {
+ position: absolute;
+ z-index: 3;
+ bottom: 13px;
+ left: 10px;
+
+ min-width: 18px;
+ height: 18px;
+ padding: 0 4px;
+
+ text-align: center;
+
+ border-radius: 20px;
+
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 18px;
+ }
+
+ &.menu-opened .burger__line {
+ &:nth-child(1),
+ &:nth-child(3) {
+ transform-origin: 50%, 50%, 0;
+
+ opacity: 1;
+ }
+
+ &:nth-child(1) {
+ transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1);
+ }
+
+ &:nth-child(3) {
+ transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1);
+ }
+ }
+}
+
+@media (max-width: 780px) {
+ .rc-header {
+ &--burger {
+ display: flex;
+
+ margin: 0;
+ padding: 0;
+ }
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/components/main-content.css b/app/theme/client/imports/components/main-content.css
similarity index 93%
rename from packages/rocketchat_theme/client/imports/components/main-content.css
rename to app/theme/client/imports/components/main-content.css
index c36f1ecfc511..7ab3cb92f3a2 100644
--- a/packages/rocketchat_theme/client/imports/components/main-content.css
+++ b/app/theme/client/imports/components/main-content.css
@@ -9,8 +9,6 @@
width: 1vw;
height: 100%;
-
- margin-bottom: 25px;
}
.messages-container .room-icon {
diff --git a/packages/rocketchat_theme/client/imports/components/memberlist.css b/app/theme/client/imports/components/memberlist.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/memberlist.css
rename to app/theme/client/imports/components/memberlist.css
diff --git a/app/theme/client/imports/components/message-box.css b/app/theme/client/imports/components/message-box.css
new file mode 100644
index 000000000000..f8840d6e17cf
--- /dev/null
+++ b/app/theme/client/imports/components/message-box.css
@@ -0,0 +1,374 @@
+.rc-message-box {
+ position: relative;
+
+ width: 100%;
+ padding: 24px;
+
+ font-size: var(--message-box-text-size);
+
+ &--embedded {
+ padding: 24px 12px 12px;
+ }
+
+ &__toolbar-formatting {
+ position: absolute;
+ left: 0;
+
+ display: flex;
+
+ width: 100%;
+ height: 24px;
+
+ justify-content: center;
+
+ &-item {
+ display: flex;
+
+ min-width: 16px;
+ margin: 0 4px;
+
+ transition: color 0.1s;
+
+ color: var(--message-box-markdown-color);
+ align-items: center;
+ justify-content: center;
+
+ &:hover,
+ &:focus,
+ &.active {
+ color: var(--message-box-markdown-hover-color);
+ }
+ }
+
+ &-link {
+ color: currentColor;
+
+ font-size: 0.75rem;
+ }
+
+ &-icon {
+ font-size: 1rem;
+ fill: currentColor;
+ }
+ }
+
+ &__typing {
+ position: absolute;
+ top: 4px;
+ left: 0;
+
+ margin-left: 24px;
+
+ color: var(--message-box-user-typing-color);
+
+ font-size: var(--message-box-user-typing-text-size);
+
+ &-user {
+ color: var(--message-box-user-typing-user-color);
+
+ font-weight: bold;
+ }
+
+ &::after {
+ display: inline-block;
+ overflow: hidden;
+
+ width: 0;
+
+ content: "\2026"; /* ascii code for the ellipsis character */
+ animation: ellipsis steps(4, end) 1.5s infinite;
+ vertical-align: bottom;
+ }
+ }
+
+ &__container {
+ display: flex;
+
+ padding: 0.75rem 0;
+
+ cursor: text;
+
+ transition: background-color 0.3s;
+
+ border-width: var(--message-box-container-border-width);
+ border-color: var(--message-box-container-border-color);
+ border-radius: var(--message-box-container-border-radius);
+
+ line-height: 20px;
+ align-items: center;
+
+ &.editing {
+ background-color: var(--message-box-editing-color);
+ }
+ }
+
+ &__textarea {
+ overflow-y: auto;
+
+ width: 100%;
+ height: 15px;
+
+ min-height: 21px;
+ max-height: 155px;
+ padding: 0;
+
+ resize: none;
+
+ border: 0;
+ background-color: transparent;
+
+ font-family: inherit;
+ font-size: var(--message-box-text-size);
+
+ &::placeholder {
+ color: var(--message-box-placeholder-color);
+ }
+ }
+
+ &__icon {
+ display: flex;
+ flex: auto 0 0;
+
+ width: 50px;
+ height: 21px;
+
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+
+ & .rc-input__icon-svg {
+ font-size: 1.4rem;
+ }
+
+ & .rc-input__icon-svg--plus {
+ transition: transform 0.1s linear;
+ }
+ }
+
+ &__action-menu {
+ position: relative;
+
+ &.active .rc-input__icon-svg--plus {
+ transform: rotate(45deg);
+ }
+ }
+
+ & [data-small] {
+ display: none;
+ }
+
+ &__audio-message {
+ display: flex;
+
+ &-mic {
+ display: flex;
+ }
+
+ &-done,
+ &-cancel,
+ &-timer,
+ &-loading {
+ display: none;
+ }
+
+ &-done {
+ color: var(--rc-color-success);
+ }
+
+ &-cancel {
+ color: var(--rc-color-error);
+ }
+
+ &-timer {
+ margin: 0 -0.25rem;
+ align-items: center;
+ justify-content: center;
+
+ &-dot,
+ &-text {
+ margin: 0 0.25rem;
+ }
+
+ &-text {
+ min-width: 3em;
+ }
+
+ &-dot {
+ flex: 0 0 auto;
+
+ width: 0.5rem;
+ height: 0.5rem;
+
+ border-radius: 50%;
+ background-color: red;
+ }
+ }
+
+ &-loading {
+ cursor: pointer;
+
+ animation: spin 1s linear infinite;
+ }
+
+ &--recording {
+ .rc-message-box__audio-message-mic,
+ .rc-message-box__audio-message-loading {
+ display: none;
+ }
+
+ .rc-message-box__audio-message-done,
+ .rc-message-box__audio-message-cancel,
+ .rc-message-box__audio-message-timer {
+ display: flex;
+ }
+ }
+
+ &--loading {
+ .rc-message-box__audio-message-mic,
+ .rc-message-box__audio-message-done,
+ .rc-message-box__audio-message-cancel,
+ .rc-message-box__audio-message-timer {
+ display: none;
+ }
+
+ .rc-message-box__audio-message-loading {
+ display: flex;
+ }
+ }
+
+ &--busy {
+ .rc-message-box__audio-message-mic {
+ cursor: not-allowed;
+
+ opacity: 0.5;
+ }
+ }
+ }
+
+ &__join {
+ display: flex;
+
+ margin: 0 -0.25rem;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
+
+ &__join-code {
+ flex: 0 0 10rem;
+
+ margin: 0 0.25rem;
+ }
+
+ &__join-button {
+ flex: 0;
+
+ margin: -1rem 0.15rem;
+ }
+
+ &__take-it-button {
+ margin: 0 0.5rem;
+ }
+}
+
+@media (width <= 500px) {
+ .rc-message-box {
+ margin-top: 1rem;
+ padding: 0;
+
+ &__typing {
+ top: -1rem;
+
+ margin-left: 1rem;
+ }
+
+ &__toolbar-formatting {
+ display: none;
+ }
+
+ &__container {
+ display: flex;
+
+ padding: var(--default-small-padding);
+ padding-bottom: calc(var(--default-small-padding) - 8px);
+
+ border-width: 0;
+ border-top-width: 1px;
+ flex-wrap: wrap;
+ }
+
+ & [data-desktop] {
+ display: none;
+ }
+
+ & [data-small] {
+ display: flex;
+ }
+
+ &__textarea {
+ flex: 1 0 100%;
+
+ margin-bottom: 10px;
+ order: 1;
+ }
+
+ &__action {
+ margin: 5px 10px;
+
+ font-size: 20px;
+ }
+
+ & [disabled] {
+ opacity: 0.4;
+ }
+
+ & .emoji-picker-icon {
+ width: initial;
+ padding-right: 10px;
+ order: 2;
+ }
+
+ &__action-label {
+ display: flex;
+ flex-direction: row;
+ flex: 1 1 auto;
+
+ font-size: 20px;
+ order: 3;
+ }
+
+ &__audio-message {
+ order: 4;
+ }
+
+ &__send {
+ flex: 0;
+
+ font-size: 20px;
+ order: 5;
+ }
+ }
+}
+
+.rc-popover--message-box {
+ & .rc-popover__divider {
+ display: none;
+ }
+
+ & .rc-popover__title {
+ text-transform: none;
+
+ color: var(--message-box-popover-title-text-color);
+
+ font-size: var(--message-box-popover-title-text-size);
+
+ &:not(:first-child) {
+ margin-top: var(--popover-column-padding);
+ }
+ }
+}
+
+.rtl .rc-message-box__typing {
+ right: 0;
+
+ margin-right: 24px;
+}
diff --git a/app/theme/client/imports/components/messages.css b/app/theme/client/imports/components/messages.css
new file mode 100644
index 000000000000..01ddccf00d38
--- /dev/null
+++ b/app/theme/client/imports/components/messages.css
@@ -0,0 +1,176 @@
+.messages-container-wrapper {
+ position: relative;
+}
+
+.message-actions {
+ position: absolute;
+ top: 2px;
+ right: 0.4rem;
+
+ display: none;
+
+ user-select: none;
+
+ color: var(--color-darkest);
+
+ font-size: 1rem;
+
+ &__buttons {
+ display: flex;
+ }
+
+ &__button {
+ margin: 0 0.2rem;
+
+ font-size: inherit;
+
+ &:hover {
+ color: var(--rc-color-button-primary);
+ }
+ }
+
+ &__menu {
+ padding: 2px 0;
+
+ cursor: pointer;
+
+ &:hover &-icon {
+ fill: var(--rc-color-button-primary);
+ }
+
+ &-icon {
+ fill: currentColor;
+ }
+ }
+}
+
+.message {
+ & .toggle-hidden {
+ display: none;
+ }
+
+ &--ignored {
+ & .body {
+ display: none;
+ }
+
+ & .toggle-hidden {
+ display: block;
+ }
+
+ & + .message--ignored.sequential {
+ display: none;
+ }
+ }
+
+ &.active {
+ & .message-actions__label {
+ color: var(--rc-color-button-primary);
+ }
+ }
+
+ & .rc-popover-anchor {
+ display: block;
+
+ visibility: hidden;
+
+ width: 0;
+ height: 0;
+
+ opacity: 0;
+ }
+
+ & .rc-popover {
+ top: -200vh;
+ right: 180px;
+
+ &__wrapper {
+ position: fixed;
+ top: -100vh;
+ left: -100vw;
+
+ width: 300vw;
+ height: 300vh;
+ }
+
+ &__content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ }
+}
+
+.message-body {
+ &--unstyled {
+ font: inherit;
+ line-height: initial;
+
+ & .emojione,
+ & .emoji {
+ display: inline-block !important;
+
+ width: 1em !important;
+ min-width: 1em;
+ height: 1em !important;
+ min-height: 1em;
+ }
+
+ & * {
+ display: inline !important;
+
+ padding: unset !important;
+
+ vertical-align: unset !important;
+
+ white-space: unset !important;
+
+ color: inherit !important;
+
+ border: unset !important;
+
+ background-color: unset !important;
+
+ font-family: inherit !important;
+
+ font-weight: unset !important;
+
+ line-height: unset !important;
+
+ &::before,
+ &::after {
+ content: unset !important;
+ }
+ }
+
+ & a {
+ pointer-events: none;
+ }
+ }
+}
+
+.message-oembed {
+ overflow: hidden;
+}
+
+.messages-box .rc-popover__list {
+ padding: 0;
+}
+
+.rtl .message-actions {
+ right: auto;
+ left: 2px;
+}
+
+@media (width >= 500px) {
+ .message {
+ &:hover,
+ &.active {
+ background-color: rgba(15, 34, 0, 0.05);
+
+ & .message-actions {
+ display: flex;
+ }
+ }
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/components/modal.css b/app/theme/client/imports/components/modal.css
similarity index 90%
rename from packages/rocketchat_theme/client/imports/components/modal.css
rename to app/theme/client/imports/components/modal.css
index 52c73c381373..cf7511504e18 100644
--- a/packages/rocketchat_theme/client/imports/components/modal.css
+++ b/app/theme/client/imports/components/modal.css
@@ -5,10 +5,12 @@
min-width: 400px;
max-width: 500px;
height: auto;
- max-height: 100%;
+ max-height: 90%;
animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
+ border: none;
+
background: white;
box-shadow: 0 0 2px 0 rgba(47, 52, 61, 0.08), 0 0 12px 0 rgba(47, 52, 61, 0.12);
@@ -30,10 +32,20 @@
justify-content: center;
}
+ &--modal {
+ width: 640px;
+ max-width: 100%;
+ }
+
+ &--modal &__title {
+ font-size: 1.375rem;
+ font-weight: 500;
+ }
+
&__title {
flex: 1 1 auto;
- font-size: 16px;
+ font-size: 1rem;
}
&__close {
@@ -59,7 +71,8 @@
position: relative;
display: flex;
- overflow: auto;
+
+ overflow: hidden auto;
flex-direction: column;
@@ -141,14 +154,13 @@
@media (width <= 400px) {
.rc-modal {
- top: initial !important;
+ top: 0 !important;
bottom: 0;
left: 0 !important;
width: 100%;
min-width: 100%;
max-width: 100%;
- margin: 0 16px;
animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
diff --git a/app/theme/client/imports/components/modal/create-channel.css b/app/theme/client/imports/components/modal/create-channel.css
new file mode 100644
index 000000000000..8fd3fb876a66
--- /dev/null
+++ b/app/theme/client/imports/components/modal/create-channel.css
@@ -0,0 +1,66 @@
+.create-channel {
+ display: flex;
+ flex-direction: column;
+
+ width: 100%;
+
+ animation-name: fadeIn;
+ animation-duration: 1s;
+
+ &__content {
+ overflow: auto;
+ flex: 1 1 auto;
+
+ margin: 0 -40px;
+ padding: 0 40px;
+ }
+
+ &__wrapper {
+
+ display: flex;
+ flex-direction: column;
+
+ height: 100%;
+ }
+
+ &__switches,
+ &__inputs:not(:only-of-type),
+ & .rc-input:not(:only-of-type) {
+ margin-bottom: var(--create-channel-gap-between-elements);
+ }
+
+ &__description {
+ padding: var(--create-channel-gap-between-elements) 0;
+
+ color: var(--create-channel-description-color);
+
+ font-size: var(--create-channel-description-text-size);
+ }
+
+ &__switches {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ & .rc-switch {
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 2rem;
+ }
+ }
+
+ & .rc-input__icon-svg {
+ font-size: 1.2rem;
+ }
+}
+
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/components/modal/full-modal.css b/app/theme/client/imports/components/modal/full-modal.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/modal/full-modal.css
rename to app/theme/client/imports/components/modal/full-modal.css
diff --git a/packages/rocketchat_theme/client/imports/components/popout.css b/app/theme/client/imports/components/popout.css
similarity index 97%
rename from packages/rocketchat_theme/client/imports/components/popout.css
rename to app/theme/client/imports/components/popout.css
index 3f16e990ffb5..52164b2c5d5a 100644
--- a/packages/rocketchat_theme/client/imports/components/popout.css
+++ b/app/theme/client/imports/components/popout.css
@@ -121,11 +121,6 @@
font-size: 16px;
- & > .rc-icon {
- fill: currentColor;
- stroke: currentColor;
- }
-
&.preparing {
animation: loading 2s infinite;
pointer-events: none;
diff --git a/packages/rocketchat_theme/client/imports/components/popover.css b/app/theme/client/imports/components/popover.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/popover.css
rename to app/theme/client/imports/components/popover.css
diff --git a/packages/rocketchat_theme/client/imports/components/setup-wizard.css b/app/theme/client/imports/components/setup-wizard.css
similarity index 99%
rename from packages/rocketchat_theme/client/imports/components/setup-wizard.css
rename to app/theme/client/imports/components/setup-wizard.css
index fd6f0496ba10..7ab2448c63d1 100644
--- a/packages/rocketchat_theme/client/imports/components/setup-wizard.css
+++ b/app/theme/client/imports/components/setup-wizard.css
@@ -452,7 +452,7 @@
display: flex;
flex-direction: row;
- margin: 0 -0.5rem 2rem -0.5rem;
+ margin: 0 -0.5rem 2rem;
& .rc-button {
margin: 0 0.5rem;
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/rooms-list.css b/app/theme/client/imports/components/sidebar/rooms-list.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/sidebar/rooms-list.css
rename to app/theme/client/imports/components/sidebar/rooms-list.css
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar-flex.css b/app/theme/client/imports/components/sidebar/sidebar-flex.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/sidebar/sidebar-flex.css
rename to app/theme/client/imports/components/sidebar/sidebar-flex.css
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar-header.css b/app/theme/client/imports/components/sidebar/sidebar-header.css
similarity index 97%
rename from packages/rocketchat_theme/client/imports/components/sidebar/sidebar-header.css
rename to app/theme/client/imports/components/sidebar/sidebar-header.css
index 9b6776363d9c..de54cba02ae4 100644
--- a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar-header.css
+++ b/app/theme/client/imports/components/sidebar/sidebar-header.css
@@ -65,13 +65,16 @@
display: flex;
flex: 1 1 100%;
- margin: 0 10px;
+ margin: 0 -10px;
+
+ padding: 0 10px;
+
justify-content: space-between;
&-button {
color: var(--sidebar-item-text-color);
- font-size: 18px;
+ font-size: 20px;
fill: var(--sidebar-item-text-color);
}
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar-item.css b/app/theme/client/imports/components/sidebar/sidebar-item.css
similarity index 88%
rename from packages/rocketchat_theme/client/imports/components/sidebar/sidebar-item.css
rename to app/theme/client/imports/components/sidebar/sidebar-item.css
index 17ad025b779e..70913a2bc2b9 100644
--- a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar-item.css
+++ b/app/theme/client/imports/components/sidebar/sidebar-item.css
@@ -125,9 +125,11 @@
display: flex;
+ width: 20px;
+
font-size: 1rem;
+
align-items: center;
- fill: currentColor;
&-status {
&--online {
@@ -153,13 +155,6 @@
font-size: 1rem;
}
- &__room-type {
- display: flex;
-
- font-size: 0.6rem;
- fill: currentColor;
- }
-
&__user-thumb {
width: var(--sidebar-item-thumb-size);
height: var(--sidebar-item-thumb-size);
@@ -275,36 +270,6 @@
font-size: 12px;
line-height: normal;
-
- & .emojione,
- & .emoji {
- display: inline-block !important;
-
- width: 13px !important;
- min-width: 13px;
- height: 13px !important;
- min-height: 13px;
- }
-
- & * {
- display: inline !important;
-
- padding: unset !important;
-
- white-space: unset !important;
-
- color: inherit !important;
-
- background-color: unset !important;
-
- font-family: inherit !important;
-
- font-weight: unset !important;
- }
-
- & a {
- pointer-events: none;
- }
}
&__time {
@@ -328,11 +293,6 @@
fill: var(--color-white);
}
}
-
- & .mention-link {
- color: inherit;
- background: transparent;
- }
}
.flex-nav .sidebar-item__message {
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/sidebar.css b/app/theme/client/imports/components/sidebar/sidebar.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/sidebar/sidebar.css
rename to app/theme/client/imports/components/sidebar/sidebar.css
diff --git a/packages/rocketchat_theme/client/imports/components/sidebar/toolbar.css b/app/theme/client/imports/components/sidebar/toolbar.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/sidebar/toolbar.css
rename to app/theme/client/imports/components/sidebar/toolbar.css
diff --git a/packages/rocketchat_theme/client/imports/components/slider.css b/app/theme/client/imports/components/slider.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/components/slider.css
rename to app/theme/client/imports/components/slider.css
diff --git a/packages/rocketchat_theme/client/imports/components/table.css b/app/theme/client/imports/components/table.css
similarity index 96%
rename from packages/rocketchat_theme/client/imports/components/table.css
rename to app/theme/client/imports/components/table.css
index da787425d985..19d96082a231 100644
--- a/packages/rocketchat_theme/client/imports/components/table.css
+++ b/app/theme/client/imports/components/table.css
@@ -67,6 +67,8 @@
overflow: hidden;
+ flex: 1 1 auto;
+
align-items: stretch;
}
@@ -137,11 +139,17 @@
width: 200px;
}
}
+
+ &-icon {
+ color: var(--color-dark-light);
+ }
}
.table-wrapper {
position: relative;
+ flex: 1 1 auto;
+
height: 100%;
}
diff --git a/packages/rocketchat_theme/client/imports/components/tabs.css b/app/theme/client/imports/components/tabs.css
similarity index 94%
rename from packages/rocketchat_theme/client/imports/components/tabs.css
rename to app/theme/client/imports/components/tabs.css
index c9d92b363766..cf7a87448cd2 100644
--- a/packages/rocketchat_theme/client/imports/components/tabs.css
+++ b/app/theme/client/imports/components/tabs.css
@@ -9,7 +9,7 @@
width: 100%;
- margin: 0 -1rem -2px -1rem;
+ margin: 0 -1rem -2px;
}
.tab {
diff --git a/packages/rocketchat_theme/client/imports/components/tooltip.css b/app/theme/client/imports/components/tooltip.css
similarity index 83%
rename from packages/rocketchat_theme/client/imports/components/tooltip.css
rename to app/theme/client/imports/components/tooltip.css
index 7e97858d599f..4d60d4ca826a 100644
--- a/packages/rocketchat_theme/client/imports/components/tooltip.css
+++ b/app/theme/client/imports/components/tooltip.css
@@ -1,5 +1,6 @@
.rc-tooltip {
position: relative;
+ --translation-x: -50%;
&::before,
&::after {
@@ -9,7 +10,7 @@
left: 50%;
transition: all 0.18s ease-out 0.18s;
- transform: translate(-50%, 10px);
+ transform: translate(var(--translation-x), 10px);
transform-origin: top;
pointer-events: none;
@@ -61,6 +62,16 @@
border-color: transparent transparent var(--tooltip-background) transparent;
}
}
+
+ &--start,
+ .rtl &--end {
+ --translation-x: -10%;
+ }
+
+ &--end,
+ .rtl &--start {
+ --translation-x: -90%;
+ }
}
@media (min-width: 501px) {
@@ -69,7 +80,7 @@
&:hover::after,
&:focus::before,
&:focus::after {
- transform: translate(-50%, 0);
+ transform: translate(var(--translation-x), 0);
pointer-events: auto;
opacity: 1;
diff --git a/packages/rocketchat_theme/client/imports/components/userInfo.css b/app/theme/client/imports/components/userInfo.css
similarity index 99%
rename from packages/rocketchat_theme/client/imports/components/userInfo.css
rename to app/theme/client/imports/components/userInfo.css
index afdfa97c5b84..aa902614dfc9 100644
--- a/packages/rocketchat_theme/client/imports/components/userInfo.css
+++ b/app/theme/client/imports/components/userInfo.css
@@ -19,6 +19,8 @@
background-color: var(--color-white);
+ will-change: transform;
+
&.animated {
transition: transform 0.45s cubic-bezier(0.5, 0, 0, 1), opacity 0.125s ease-out 0.1s, -webkit-transform 0.45s cubic-bezier(0.5, 0, 0, 1);
}
@@ -94,6 +96,8 @@
overflow-x: hidden;
overflow-y: auto;
+ flex: 1;
+
margin: 0 -1.5rem;
padding: 0 1.5rem;
diff --git a/app/theme/client/imports/forms/button.css b/app/theme/client/imports/forms/button.css
new file mode 100644
index 000000000000..85effdc6de2e
--- /dev/null
+++ b/app/theme/client/imports/forms/button.css
@@ -0,0 +1,225 @@
+.rc-button {
+ &:not([disabled]):hover {
+ opacity: 0.6;
+ }
+
+ &--icon > svg {
+ margin: 0 5px 0 -5px;
+
+ font-size: 20px;
+ fill: currentColor;
+
+ .rtl & {
+ margin: 0 -5px 0 5px;
+ }
+ }
+
+ position: relative;
+
+ display: flex;
+
+ height: 40px;
+
+ padding: 0 1.5rem;
+
+ cursor: pointer;
+ transition: opacity 0.3s, background-color 0.3s, color 0.3s;
+ text-align: center;
+
+ color: #000000;
+
+ border-width: var(--button-border-width);
+ border-style: solid;
+ border-color: #000000;
+
+ border-radius: var(--button-border-radius);
+ background-color: transparent;
+
+ font-size: var(--button-text-size);
+ font-weight: 600;
+
+ align-items: center;
+ justify-content: center;
+
+ &:active,
+ &:focus:hover {
+ outline: none;
+ }
+
+ &:active {
+ transform: translateY(2px);
+
+ opacity: 0.9;
+ }
+
+ &:active::before {
+ top: -2px;
+ }
+
+ &::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ content: "";
+ cursor: pointer;
+ }
+
+ &:disabled {
+ cursor: default;
+
+ color: var(--button-disabled-text-color);
+ border: 0;
+ border-color: var(--button-disabled-background);
+ background-color: var(--button-disabled-background);
+ }
+
+ &--invisible {
+ visibility: hidden;
+ }
+
+ &--primary {
+ color: var(--button-primary-text-color);
+ border: 0;
+ background-color: var(--button-primary-background);
+ }
+
+ &--nude {
+ border: none;
+ background-color: inherit;
+
+ font-weight: 400;
+ }
+
+ &--primary.rc-button--nude {
+ color: var(--button-primary-background);
+ }
+
+ &--primary.rc-button--outline {
+ color: var(--button-primary-background);
+ border-color: var(--button-primary-background);
+ }
+
+ &--secondary {
+ color: var(--button-secondary-text-color);
+ border: 0;
+ border-color: var(--button-secondary-background);
+ background-color: var(--button-secondary-background);
+ }
+
+ &--secondary.rc-button--outline {
+ color: var(--button-secondary-background);
+ border-color: var(--button-secondary-background);
+ }
+
+ &--cancel {
+ color: var(--button-primary-text-color);
+ border: 0;
+ border-color: var(--button-cancel-color);
+ background-color: var(--button-cancel-color);
+ }
+
+ &--cancel.rc-button--outline {
+ color: var(--button-cancel-color);
+ border-color: var(--button-cancel-color);
+ }
+
+ &--small {
+ height: var(--button-height-small);
+ padding: var(--button-padding-small);
+
+ font-size: var(--button-text-size-small);
+ }
+
+ &--square {
+ display: flex;
+ flex: 0 0 var(--button-square-size);
+
+ margin: 0;
+ padding: 0;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &--outline {
+ border-width: 2px;
+ border-style: solid;
+ background: transparent;
+ }
+
+ &--stack {
+ width: 100%;
+ }
+
+ &--no-padding {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ &.loading {
+ position: relative;
+
+ padding-right: calc(3 * 0.782rem);
+
+ transition: padding-right 0.3s;
+
+ &::before {
+ position: absolute;
+ top: 25%;
+ right: 0.782rem;
+
+ display: block;
+
+ width: 20px;
+ height: 20px;
+
+ content: "";
+ animation: spin 1s infinite cubic-bezier(0.14, 0.48, 0.45, 0.63);
+
+ border: 0.15rem solid rgba(127, 127, 127, 0.5);
+ border-top-color: white;
+ border-radius: 50%;
+ }
+ }
+
+ &__group {
+ display: flex;
+
+ flex-direction: row;
+
+ margin: 10px -5px;
+
+ & > .rc-button {
+ margin: 0 5px;
+ }
+
+ &--wrap {
+ margin: 5px -5px;
+ flex-wrap: wrap;
+
+ & > .rc-button {
+ margin: 5px;
+ }
+ }
+
+ &--stretch {
+ justify-content: stretch;
+
+ & > .rc-button {
+ flex: 1 1;
+ }
+ }
+
+ &--vertical {
+ flex-direction: column;
+ }
+ }
+}
+
+@media (width < 780px) {
+ .rc-button--full {
+ width: 100%;
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/forms/checkbox.css b/app/theme/client/imports/forms/checkbox.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/forms/checkbox.css
rename to app/theme/client/imports/forms/checkbox.css
diff --git a/packages/rocketchat_theme/client/imports/forms/input.css b/app/theme/client/imports/forms/input.css
similarity index 92%
rename from packages/rocketchat_theme/client/imports/forms/input.css
rename to app/theme/client/imports/forms/input.css
index 04cc6b77a5d1..0be7e3f66a48 100644
--- a/packages/rocketchat_theme/client/imports/forms/input.css
+++ b/app/theme/client/imports/forms/input.css
@@ -1,9 +1,18 @@
+textarea.rc-input__element {
+ height: auto;
+ padding: 0.5rem 1rem;
+
+ font-family: inherit;
+}
+
.rc-input {
position: relative;
width: 100%;
&__label {
+ display: block;
+
cursor: pointer;
}
@@ -45,8 +54,11 @@
&__element {
width: 100%;
+ height: 2.5rem;
padding: 0 1rem;
+ text-overflow: ellipsis;
+
color: var(--input-text-color);
border-width: var(--input-border-width);
@@ -55,14 +67,15 @@
background-color: transparent;
font-size: var(--input-font-size);
-
- line-height: 2.25rem;
+ line-height: normal;
&--small {
- line-height: 1.75rem;
+ height: 2rem;
}
&::placeholder {
+ text-overflow: ellipsis;
+
color: var(--input-placeholder-color);
}
@@ -128,6 +141,8 @@
width: 100%;
&__label {
+ display: block;
+
cursor: pointer;
}
diff --git a/packages/rocketchat_theme/client/imports/forms/popup-list.css b/app/theme/client/imports/forms/popup-list.css
similarity index 92%
rename from packages/rocketchat_theme/client/imports/forms/popup-list.css
rename to app/theme/client/imports/forms/popup-list.css
index 41ec0e51bdb2..35349a13d96d 100644
--- a/packages/rocketchat_theme/client/imports/forms/popup-list.css
+++ b/app/theme/client/imports/forms/popup-list.css
@@ -5,8 +5,6 @@
width: 100%;
- padding: 0 4px;
-
&__list {
overflow-y: auto;
@@ -42,9 +40,15 @@
width: 32px;
height: 32px;
margin-right: 1rem;
+ flex-shrink: 0;
}
&-name {
+
+ overflow: hidden;
+
+ text-overflow: ellipsis;
+
color: var(--popup-list-name-color);
font-size: var(--popup-list-name-size);
diff --git a/packages/rocketchat_theme/client/imports/forms/select-avatar.css b/app/theme/client/imports/forms/select-avatar.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/forms/select-avatar.css
rename to app/theme/client/imports/forms/select-avatar.css
diff --git a/packages/rocketchat_theme/client/imports/forms/select.css b/app/theme/client/imports/forms/select.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/forms/select.css
rename to app/theme/client/imports/forms/select.css
diff --git a/packages/rocketchat_theme/client/imports/forms/switch.css b/app/theme/client/imports/forms/switch.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/forms/switch.css
rename to app/theme/client/imports/forms/switch.css
diff --git a/packages/rocketchat_theme/client/imports/forms/tags.css b/app/theme/client/imports/forms/tags.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/forms/tags.css
rename to app/theme/client/imports/forms/tags.css
diff --git a/packages/rocketchat_theme/client/imports/general/animations.css b/app/theme/client/imports/general/animations.css
similarity index 97%
rename from packages/rocketchat_theme/client/imports/general/animations.css
rename to app/theme/client/imports/general/animations.css
index cebd8bea5f59..782fe056aeaa 100644
--- a/packages/rocketchat_theme/client/imports/general/animations.css
+++ b/app/theme/client/imports/general/animations.css
@@ -114,6 +114,10 @@
}
}
+.dropdown-in {
+ animation: dropdown-in 0.3s;
+}
+
@keyframes dropdown-in {
0% {
display: none;
diff --git a/app/theme/client/imports/general/apps.css b/app/theme/client/imports/general/apps.css
new file mode 100644
index 000000000000..8b3cbe7172bc
--- /dev/null
+++ b/app/theme/client/imports/general/apps.css
@@ -0,0 +1,164 @@
+.rc-apps {
+ &-settings {
+ flex-direction: column;
+
+ &__item {
+ width: 100%;
+ padding: 10px 0;
+ }
+ }
+
+ &-container {
+
+ display: flex;
+
+ width: 100%;
+ max-width: 705px;
+ margin: auto;
+ padding: 25px 0;
+
+ padding-bottom: 25px;
+
+ &__header {
+ border-bottom: 1px solid #e1e1e1;
+ }
+ }
+
+ &-details {
+ display: flex;
+
+ padding: 25px;
+
+ &__photo {
+
+ flex: 0 0 auto;
+
+ width: 95px;
+ height: 95px;
+
+ border: 1px solid #f7f7f7;
+ background-repeat: no-repeat;
+ background-position: center center;
+
+ background-size: contain;
+ }
+
+ &__content {
+
+ display: flex;
+ overflow: hidden;
+ flex-direction: column;
+ flex: 1 1 auto;
+
+ padding: 0 15px;
+ justify-content: space-between;
+ }
+
+ &__row {
+ display: flex;
+ align-items: flex-end;
+
+ & button svg {
+ margin: 0 5px 0 -5px;
+
+ font-size: 18px;
+ }
+
+ h2 {
+
+ padding: 5px 0;
+
+ font-size: 18px;
+ }
+ }
+
+ &__row + &__row {
+ padding-top: 5px;
+ }
+
+ &__block {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ &__name {
+ font-size: 24px;
+ }
+
+ &__version {
+ color: var(--rc-color-primary-light);
+ }
+
+ &__author {
+ font-weight: 500;
+ }
+
+ &__item {
+ overflow: hidden;
+
+ flex: 1 1 1px;
+
+ white-space: nowrap;
+
+ text-overflow: ellipsis;
+ }
+
+ &__api {
+ padding-bottom: 10px;
+
+ font-size: 18px;
+ font-weight: bold;
+
+ &__description {
+ overflow-x: scroll;
+ overflow-y: hidden;
+
+ white-space: nowrap;
+
+ background-color: #fafafa;
+
+ font-size: 14px;
+
+ &__item {
+ display: table;
+
+ padding: 10px;
+
+ user-select: all !important;
+
+ white-space: pre;
+
+ border-top: 1px dashed #cccccc;
+
+ line-height: 20px;
+
+ &:first-child {
+ border-top: none;
+ }
+ }
+ }
+ }
+ }
+}
+
+@media (width <= 500px) {
+ .rc-apps {
+ &-container {
+
+ flex-direction: column;
+
+ padding: 25px;
+ align-items: center;
+ }
+
+ &-details {
+ &__item {
+ text-align: center;
+ }
+
+ &__row {
+ justify-content: center;
+ }
+ }
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css
similarity index 91%
rename from packages/rocketchat_theme/client/imports/general/base.css
rename to app/theme/client/imports/general/base.css
index 6001b1030b6e..3363bca3dca8 100644
--- a/packages/rocketchat_theme/client/imports/general/base.css
+++ b/app/theme/client/imports/general/base.css
@@ -89,7 +89,7 @@ button {
flex: 1 1 auto;
- height: 100%;
+ height: 1%;
max-height: 100%;
@@ -104,15 +104,15 @@ button {
.flex-tab-bar {
& .tab-button {
+ position: relative;
+
cursor: pointer;
}
& .tab-button-icon {
color: var(--rc-color-primary-dark);
- font-size: 1.125rem;
-
- fill: var(--rc-color-primary-dark);
+ font-size: 20px;
&--star {
width: 17px;
@@ -136,7 +136,6 @@ button {
}
.rc-icon {
-
overflow: hidden;
width: 1.25em;
@@ -145,7 +144,9 @@ button {
vertical-align: -0.15em;
fill: currentColor;
- fill: currentColor;
+ &--default-size {
+ font-size: 20px;
+ }
}
.ps-scrollbar-y-rail {
@@ -159,7 +160,6 @@ button {
.first-unread .body {
&::before {
position: absolute;
- z-index: 1;
top: 0;
left: 0;
@@ -199,22 +199,6 @@ button {
}
}
-.message.new-day.first-unread {
- &::after {
- border-color: var(--rc-color-error);
- }
-
- & .body {
- &::before {
- display: none;
- }
-
- &::after {
- top: -26px;
- }
- }
-}
-
.hidden {
display: none;
}
diff --git a/packages/rocketchat_theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css
similarity index 88%
rename from packages/rocketchat_theme/client/imports/general/base_old.css
rename to app/theme/client/imports/general/base_old.css
index 01cec0cde01f..a6047ff8a680 100644
--- a/packages/rocketchat_theme/client/imports/general/base_old.css
+++ b/app/theme/client/imports/general/base_old.css
@@ -192,17 +192,26 @@
top: 0;
left: 0;
+ display: flex;
+
overflow-y: hidden;
+ flex-direction: column;
width: 100%;
height: 100%;
+ padding: 1.25rem;
+
& .content {
- overflow-y: scroll;
+ display: flex;
+ overflow-y: auto;
+ flex-direction: column;
+ flex: 1 1 auto;
- width: 100%;
- height: calc(100% - 60px);
- padding: 25px 40px;
+ margin: 0 -1.25rem;
+ padding: 1.25rem 1.25rem 0;
+
+ line-height: 1.3em;
-webkit-overflow-scrolling: touch;
& fieldset {
@@ -264,28 +273,6 @@
}
}
}
-
- & table {
- overflow: hidden;
-
- width: 100%;
- margin-bottom: 30px;
-
- & th,
- & td {
- padding: 0.6rem 0.7rem;
-
- user-select: text;
- text-align: left;
- vertical-align: middle;
-
- border-width: 0 0 1px;
- }
-
- & th {
- white-space: nowrap;
- }
- }
}
.rc-old .input-line {
@@ -1186,361 +1173,6 @@ rc-old select,
}
}
-.rc-old .side-nav {
- position: fixed;
- z-index: 100;
- top: 0;
- bottom: 0;
- left: 0;
-
- overflow: visible;
-
- width: var(--rooms-box-width);
- height: auto;
- padding: 12px 0 0;
- will-change: transform;
-
- &::before {
- position: absolute;
- top: 59px;
- left: 8px;
-
- width: 189px;
- height: 1px;
-
- content: " ";
- }
-
- & .rooms-list {
- position: absolute;
- top: calc(var(--header-min-height) + var(--toolbar-height));
- bottom: var(--footer-min-height);
-
- display: block;
- overflow-x: hidden;
- overflow-y: auto;
-
- width: 100%;
- direction: rtl;
- -webkit-overflow-scrolling: touch;
-
- & > .wrapper {
- position: relative;
-
- padding-bottom: 1em;
- padding-left: 8px;
- direction: ltr;
- }
- }
-
- & .more {
- display: block;
-
- width: 100%;
- margin-top: 2px;
- padding: 4px 0 4px 10px;
-
- font-size: 11px;
- }
-
- & .input-error {
- margin-top: -12px;
- margin-bottom: -20px;
- padding: 0;
-
- text-align: left;
-
- font-size: 12px;
-
- & strong {
- display: block;
-
- margin-bottom: 2px;
- }
- }
-
- & .empty {
- padding: 2px 10px;
-
- font-size: 11px;
- }
-
- & .header {
- position: absolute;
- z-index: 3;
- top: 0;
- left: 0;
-
- width: 100%;
- height: var(--header-min-height);
- min-height: var(--header-min-height);
-
- cursor: pointer;
- }
-
- & > .arrow {
- position: absolute;
- z-index: 3;
- top: 18px;
- right: 8px;
-
- cursor: pointer;
- }
-
- & .footer {
- position: absolute;
- z-index: 2;
- bottom: 0;
- left: 0;
-
- width: 100%;
- height: var(--footer-min-height);
- min-height: var(--footer-min-height);
- padding: 10px 15px 0;
-
- text-align: right;
-
- & .logo {
- display: block;
-
- width: 100%;
- height: 100%;
- margin-top: -1px;
-
- &:hover {
- text-decoration: none;
- }
- }
-
- & small {
- display: block;
-
- width: 100%;
- margin-top: 2px;
- margin-bottom: 0;
- padding-right: 4px;
-
- text-transform: lowercase;
-
- font-size: 11px;
- font-weight: 400;
- }
-
- & img {
- display: inline-block;
-
- max-width: 222px;
- max-height: 43px;
- }
- }
-
- & .search-form > div {
- position: relative;
- }
-
- & h3 {
- position: relative;
-
- margin: 25px 0 0;
- padding-left: 10px;
-
- text-transform: uppercase;
-
- font-weight: 500;
- line-height: 28px;
- }
-
- & .unread {
- position: absolute;
- top: 6px;
- right: 6px;
-
- min-width: 15px;
- padding: 0 2px;
-
- text-align: center;
-
- border-radius: 2px;
-
- font-size: 11px;
- font-weight: 800;
- line-height: 14px;
- }
-
- & ul {
- position: relative;
-
- & li {
- overflow: hidden;
-
- max-width: 100%;
-
- vertical-align: middle;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- & .remove,
- & .erase {
- position: absolute;
- top: 2px;
- right: -18px;
-
- display: block;
-
- transition: opacity 0.15s ease 0.35s, transform 0.12s ease-out 0.35s;
- transform: translateX(-10px);
-
- opacity: 0;
- }
-
- &:hover .opt {
- transform: translateX(0);
-
- opacity: 1;
- }
-
- &.has-unread .opt {
- opacity: 0;
- }
-
- &.has-alert .name {
- font-weight: bold;
- }
- }
-
- & a {
- position: relative;
-
- display: block;
- overflow: hidden;
-
- max-width: 100%;
- padding: 6px 25px 7px 6px;
-
- vertical-align: middle;
- white-space: nowrap;
- text-decoration: none;
- text-overflow: ellipsis;
-
- border-radius: 2px 0 0 2px;
-
- font-size: 15px;
- line-height: 16px;
-
- &:hover {
- text-decoration: none;
- }
-
- & .archived {
- font-style: italic;
- }
- }
-
- & .opt {
- position: absolute;
- top: 7px;
- right: 0;
-
- display: block;
-
- width: 50px;
- padding-right: 10px;
-
- transition: opacity 0.12s ease;
- text-align: right;
-
- opacity: 0;
-
- & i {
- margin: 0 1px;
- }
-
- & .icon-cancel-circled::before {
- margin-left: 2px;
- }
-
- & .icon-logout {
- margin-left: 1px;
- }
-
- &.fixed {
- transform: translateX(0);
-
- opacity: 1;
- }
- }
-
- & i {
- display: inline-block;
-
- width: 16px;
-
- font-size: 14px;
- }
-
- & input[type="text"] {
- width: 100%;
-
- font-size: 12px;
- }
- }
-
- & .unread-rooms {
- position: absolute;
- z-index: 1;
-
- display: -webkit-flex;
- display: flex;
-
- width: 100%;
-
- text-align: center;
- text-transform: uppercase;
-
- font-weight: bold;
- line-height: 24px;
- align-items: center;
- justify-content: center;
-
- &.top-unread-rooms {
- top: calc(var(--header-min-height) + var(--toolbar-height));
- }
-
- &.bottom-unread-rooms {
- bottom: var(--footer-min-height);
- }
-
- & i {
- margin-left: 5px;
-
- font-size: 12px;
- }
- }
-
- & .unread-rooms-mode,
- & .unread-rooms-mode + ul {
- overflow: hidden;
-
- max-height: 0;
- margin: 0;
-
- opacity: 0;
- }
-
- & .unread-rooms-mode.has-unread {
- margin: 25px 0 0;
- }
-
- & .unread-rooms-mode.has-unread,
- & .unread-rooms-mode.has-unread + ul {
- max-height: 5000px;
-
- transition: max-height 1s ease-in, opacity 0.5s linear;
-
- opacity: 1;
- }
-}
-
.rc-old .toolbar {
position: absolute;
z-index: 2;
@@ -1732,30 +1364,39 @@ rc-old select,
}
}
-.rc-old .cms-page {
+.cms-page {
+ display: flex;
+ flex-direction: column;
+
max-width: 800px;
- margin: 40px auto;
- padding: 20px;
+ max-height: 100%;
+ margin: auto;
+
+ padding: 2rem;
border-radius: var(--border-radius);
- box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
+
+ &__content {
+
+ overflow: auto;
+
+ margin: -1rem;
+ padding: 1rem;
+ }
& .cms-page-close {
+
+ display: flex;
+
margin-bottom: 10px;
- text-align: right;
+ justify-content: flex-end;
}
}
/* MAIN CONTENT + MAIN PAGES */
.rc-old.main-content {
- &.main-modal {
- left: 0;
-
- margin-right: 0;
- }
-
& .container-fluid {
padding-top: 0;
}
@@ -1788,10 +1429,16 @@ rc-old select,
& .section {
padding: 20px 0;
- border-bottom: 2px solid #dddddd;
+ &:not(:only-child) {
+ border-bottom: 2px solid #dddddd;
- &.section-collapsed .section-content {
- display: none;
+ &.section-collapsed .section-content {
+ display: none;
+ }
+
+ .section-title-right {
+ visibility: visible;
+ }
}
}
@@ -1811,8 +1458,12 @@ rc-old select,
flex-grow: 1;
}
- & .section-title-right > .rc-button {
- font-size: 1.25rem;
+ & .section-title-right {
+ visibility: hidden;
+
+ & > .rc-button {
+ font-size: 1.25rem;
+ }
}
}
@@ -2053,8 +1704,6 @@ rc-old select,
margin-bottom: 10px;
padding: 10px 0;
- border-width: 0 0 1px;
-
font-weight: 300;
& p {
@@ -2320,12 +1969,6 @@ rc-old select,
font-size: 14px;
}
- & .edit-room-title {
- margin-left: 4px;
-
- font-size: 16px;
- }
-
& .wrapper {
position: absolute;
top: 0;
@@ -2569,15 +2212,6 @@ rc-old select,
}
}
- & .stream-info {
- float: left;
-
- height: 25px;
- padding: 3px;
-
- font-size: 12px;
- }
-
& .editing-commands {
display: none;
@@ -2611,10 +2245,6 @@ rc-old select,
& .editing-commands {
display: block;
}
-
- & .stream-info {
- display: none;
- }
}
}
@@ -2684,6 +2314,10 @@ rc-old select,
line-height: unset;
}
+.rc-old .rc-message-box .reply-preview__wrap {
+ position: relative;
+}
+
.rc-old .rc-message-box .reply-preview {
position: relative;
@@ -2708,23 +2342,33 @@ rc-old select,
}
}
+.rc-old .rc-message-box .reply-preview:not(:last-child)::before {
+
+ position: absolute;
+ right: 15px;
+
+ bottom: 0;
+ left: 15px;
+
+ height: 2px;
+
+ content: "";
+
+ background: rgba(31, 35, 41, 0.08);
+}
+
.rc-old .rc-message-box .reply-preview-with-popup {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 5px;
- box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2), 0 1px 1px rgba(0, 0, 0, 0.16);
}
.rc-old .reply-preview .cancel-reply {
padding: 10px;
}
-.rc-old .reply-preview .mention-link.mention-link-all {
- color: #ffffff;
-}
-
-.rc-old .reply-preview .mention-link.mention-link-me {
- color: #ffffff;
+.rc-message-box__icon.cancel-reply .rc-input__icon-svg--cross {
+ font-size: 1em;
}
.rc-old .message-popup.popup-with-reply-preview {
@@ -2846,10 +2490,6 @@ rc-old select,
overflow: hidden;
flex-grow: 1;
- & .message-cog-container .message-action.jump-to-search-message {
- display: none !important;
- }
-
& .wrapper.has-more-next {
padding-bottom: 24px;
}
@@ -2866,10 +2506,10 @@ rc-old select,
& .start__purge-warning {
margin-top: -33px;
margin-bottom: 0.5rem;
- padding: 0.5rem;
+ padding: 1rem;
border-width: 1px 0 0;
- background: linear-gradient(to bottom, var(--rc-color-alert-message-warning-background) 0%, transparent 100%);
+ background: linear-gradient(to bottom, var(--rc-color-alert-message-warning-background) 0%, rgba(255, 255, 255, 0) 100%);
}
}
@@ -2962,6 +2602,14 @@ rc-old select,
height: 2px;
border-radius: 2px;
+
+ &--me {
+ background-color: var(--mention-link-me-background);
+ }
+
+ &--group {
+ background-color: var(--mention-link-group-background);
+ }
}
}
@@ -2974,6 +2622,23 @@ rc-old select,
line-height: 20px;
+ &-unread {
+ display: inline-block;
+
+ width: 10px;
+ height: 10px;
+ margin: 0 0.25rem;
+
+ cursor: pointer;
+
+ border-radius: 50%;
+ background: var(--rc-color-button-primary);
+ }
+
+ &.highlighted {
+ background: #ffff99;
+ }
+
&.highlight {
animation: highlight 3s;
}
@@ -2982,6 +2647,10 @@ rc-old select,
margin-top: 0;
}
+ & .time {
+ white-space: nowrap;
+ }
+
&.new-day {
margin-top: 40px;
@@ -2996,96 +2665,45 @@ rc-old select,
padding: 0 10px;
content: attr(data-date);
- text-align: center;
-
- pointer-events: none;
-
- font-size: 12px;
- font-weight: 600;
- }
-
- &::after {
- position: absolute;
- z-index: -1;
- top: -20px;
- left: 0;
- display: block;
-
- width: 100%;
-
- content: " ";
+ transition: all 0.3s;
+ text-align: center;
pointer-events: none;
- border-width: 1px 0 0;
- }
- }
-
- & .message-action {
- display: none;
-
- cursor: pointer;
- }
-
- &:hover:not(.system) .message-action {
- display: block;
- }
-
- & .message-cog-container {
- position: relative;
-
- display: none;
-
- cursor: pointer;
- }
-
- & .message-dropdown {
- position: absolute;
- z-index: 1000;
- top: -5px;
- left: -2px;
-
- display: none;
- overflow: hidden;
-
- transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out;
- animation: dropdown-in 0.15s ease-in-out;
-
- border-radius: var(--border-radius);
- box-shadow:
- 0 1px 1px 0 rgba(0, 0, 0, 0.2),
- 0 2px 10px 0 rgba(0, 0, 0, 0.16);
+ border-radius: 2px;
- & ul {
- display: flex;
- display: -webkit-flex;
+ font-size: 12px;
+ font-weight: 600;
+ }
- padding: 0;
+ &::after {
+ position: absolute;
+ z-index: -1;
+ top: -20px;
+ right: 0;
+ left: 0;
- font-size: 14px;
+ display: block;
- & li {
- display: block;
+ margin: 0 var(--default-padding);
- padding: 0 8px;
+ content: " ";
- cursor: pointer;
+ pointer-events: none;
- font-weight: 400;
- line-height: 26px;
+ border-width: 1px 0 0;
+ }
+ }
- &:first-child {
- padding-left: 6px;
+ & .message-action {
+ display: none;
- border-width: 0 1px 0 0;
- }
+ cursor: pointer;
+ }
- &:last-child {
- padding-right: 13px;
- }
- }
- }
+ &:hover:not(.system) .message-action {
+ display: block;
}
& .user {
@@ -3094,9 +2712,10 @@ rc-old select,
margin-right: 5px;
font-family: inherit;
- font-size: inherit;
+ font-size: 0.875rem;
font-weight: 600;
+ line-height: inherit;
}
& .thumb {
@@ -3130,24 +2749,30 @@ rc-old select,
}
}
- & .info {
- font-size: 12px;
+ & .title {
+ display: flex;
+ flex-direction: row;
+
+ font-size: 0.75rem;
+
+ line-height: 1.25rem;
+ align-items: center;
& .edited {
margin-left: 3px;
- padding-left: 3px;
-
- border-left: 1px dotted;
}
& .is-bot,
& .role-tag {
+ margin: 0 3px;
padding: 1px 4px;
border-width: 1px;
border-radius: var(--border-radius);
background: #ffffff;
+
+ line-height: initial;
}
}
@@ -3169,13 +2794,14 @@ rc-old select,
display: none;
}
- & .info {
+ & .title {
position: absolute;
left: 5px;
width: 60px;
text-align: right;
+ justify-content: flex-end;
& .time,
& .role-tag,
@@ -3210,55 +2836,15 @@ rc-old select,
}
}
- &.system .body {
- font-style: italic;
-
- & em {
- font-weight: 600;
- }
-
- & .attachment {
- font-style: normal;
- }
- }
-
& .avatar-initials {
line-height: 40px;
}
- & button {
- font-weight: 400;
-
- &:hover {
- text-decoration: underline;
- }
- }
-
& .body {
- transition: opacity 1s linear;
+ transition: opacity 0.3s linear;
opacity: 1;
- & .inline-image {
- display: inline-block;
-
- border-radius: 3px;
- background-repeat: no-repeat;
- background-position: center left;
- background-size: contain;
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
-
- line-height: 0;
-
- & img {
- max-width: 100%;
- max-height: 200px;
-
- cursor: pointer;
- object-fit: contain;
- }
- }
-
& > h1 {
font-size: 3em;
line-height: 1em;
@@ -3279,20 +2865,6 @@ rc-old select,
line-height: 1em;
}
- & blockquote.sandstorm-grain {
- & img {
- width: 50px;
- }
-
- & label {
- cursor: pointer;
- }
-
- & button {
- display: block;
- }
- }
-
& ul,
& ol {
padding: 0 0 0 24px;
@@ -3351,6 +2923,26 @@ rc-old select,
font-weight: 400;
}
+ & .inline-image {
+ display: inline-block;
+
+ border-radius: 3px;
+ background-repeat: no-repeat;
+ background-position: center left;
+ background-size: contain;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+
+ line-height: 0;
+
+ & img {
+ max-width: 100%;
+ max-height: 200px;
+
+ cursor: pointer;
+ object-fit: contain;
+ }
+ }
+
&.livechat_navigation_history {
& .thumb,
& .user,
@@ -3358,6 +2950,96 @@ rc-old select,
display: none;
}
}
+
+ &.collapsed {
+ min-height: 25px;
+ padding: 2px 50px 2px 70px;
+
+ font-size: 12px;
+
+ & > .thumb {
+ top: 3px;
+
+ width: 20px;
+ height: 20px;
+ margin-left: 16px;
+
+ & .avatar {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ & .user {
+ font-size: 0.75rem;
+ font-weight: initial;
+ }
+
+ & .body,
+ & .message-oembed,
+ & .attachment,
+ & .reactions,
+ & .edited,
+ & .role-tag,
+ &:hover .message-actions {
+ display: none;
+ }
+ }
+
+ &.system {
+ min-height: 25px;
+ padding: 2px 50px 2px 70px;
+
+ font-size: 12px;
+
+ & > .thumb {
+ top: 3px;
+
+ width: 20px;
+ height: 20px;
+ margin-left: 16px;
+
+ & > .avatar {
+ width: 100% !important;
+ height: 100% !important;
+ }
+ }
+
+ & .message-body-wrapper {
+ & .title {
+ display: inline-flex;
+
+ & .user {
+ font-size: 0.75rem;
+ font-weight: initial;
+ }
+
+ & .reactions,
+ & .edited,
+ & .role-tag {
+ display: none;
+ }
+ }
+
+ & .body {
+ display: inline;
+
+ font-style: italic;
+
+ & em {
+ font-weight: 600;
+ }
+
+ & .attachment {
+ font-style: normal;
+ }
+ }
+ }
+
+ &:hover .message-actions {
+ display: none;
+ }
+ }
}
.rc-old .image-labels {
@@ -3387,24 +3069,6 @@ rc-old select,
line-height: 1;
}
-body:not(.is-cordova) {
- & .message:hover:not(.system) .message-cog-container {
- display: inline-block;
- }
-
- & .message {
- & .body,
- & .user.user-card-message,
- & .time {
- user-select: text;
-
- & * {
- user-select: text;
- }
- }
- }
-}
-
.rc-old .cozy {
& .message {
padding: 4px 20px 4px 70px;
@@ -3418,10 +3082,21 @@ body:not(.is-cordova) {
.rc-old .compact {
& .message {
min-height: 26px;
- padding: 5px 15px 0 37px;
+ padding: 5px 15px 5px 37px;
- &:not(.system) .message-cog-container {
- display: inline-block;
+ & .attachment {
+ & .attachment-title > a {
+ font-size: 0.9em;
+ }
+
+ & .attachment-author img {
+ border-radius: 2px;
+ }
+ }
+
+ & blockquote iframe {
+ width: 266px;
+ height: 150px;
}
& .body {
@@ -3434,24 +3109,9 @@ body:not(.is-cordova) {
& .inline-video {
max-height: 150px;
}
-
- & .attachment {
- & .attachment-title > a {
- font-size: 0.9em;
- }
-
- & .attachment-author img {
- border-radius: 2px;
- }
- }
-
- & blockquote iframe {
- width: 266px;
- height: 150px;
- }
}
- & .info {
+ & .title {
& .avatar-image {
border-radius: 2px;
}
@@ -3466,6 +3126,7 @@ body:not(.is-cordova) {
width: 20px;
height: 20px;
+ margin-left: 0;
& .avatar {
width: 20px;
@@ -3479,11 +3140,14 @@ body:not(.is-cordova) {
display: inline-block;
}
- & .info {
+ & .title {
position: relative;
left: 0;
- text-align: right;
+ width: auto;
+
+ text-align: left;
+ justify-content: initial;
& .time,
& .edited {
@@ -3505,9 +3169,19 @@ body:not(.is-cordova) {
z-index: 2;
display: flex;
- flex: 0 0 41px;
+ flex: 0 0 40px;
+
+ transform: box-shadow 0.3s;
- border-width: 0 0 0 1px;
+ box-shadow: -1px 0 0 1px #cccccc26;
+
+ &.opened {
+ box-shadow: -1px 0 5px 2px #cccccc26;
+
+ & > .flex-tab-bar {
+ box-shadow: -1px 0 5px 2px #cccccc26;
+ }
+ }
& .flex-tab {
position: relative;
@@ -3736,7 +3410,9 @@ body:not(.is-cordova) {
min-width: 40px;
- border: 1px solid rgba(31, 35, 41, 0.08);
+ transition: box-shadow 0.3s;
+
+ box-shadow: -1px 0 0 1px #cccccc26;
& .tab-button {
position: relative;
@@ -4004,7 +3680,7 @@ body:not(.is-cordova) {
}
& .contact-code {
- margin: -5px 0 10px 0;
+ margin: -5px 0 10px;
font-size: 12px;
}
@@ -4537,17 +4213,6 @@ body:not(.is-cordova) {
font-size: 10px;
}
- & .submit,
- & .register,
- & .forgot-password,
- & .back-to-login {
- margin-top: 12px;
-
- & button {
- margin: 0 auto;
- }
- }
-
& .input-line {
position: relative;
@@ -4586,7 +4251,7 @@ body:not(.is-cordova) {
font-weight: 400;
&:focus {
- border-color: #13679a !important;
+ border-color: #1d74f5 !important;
}
}
@@ -4837,19 +4502,10 @@ body:not(.is-cordova) {
height: 100%;
}
-.rc-old .mention-link {
-
- padding: 0 4px 2px;
-
- border-radius: var(--border-radius);
-
- font-weight: bold;
-}
-
.rc-old .highlight-text {
padding: 2px;
- border-radius: var(--border-radius);
+ border-radius: 15px;
}
.rc-old .avatar-suggestions {
@@ -4959,6 +4615,7 @@ body:not(.is-cordova) {
padding: 6px 8px;
text-align: left;
+ word-break: break-word;
}
& th {
@@ -5006,34 +4663,68 @@ body:not(.is-cordova) {
height: 100%;
& .dropzone-overlay {
- display: none;
- }
- &.over .dropzone-overlay {
- position: fixed;
+ position: absolute;
z-index: 1000000;
top: 0;
right: 0;
bottom: 0;
left: 0;
- display: flex;
+ display: none;
+
+ margin: var(--default-small-padding);
+ padding: var(--default-padding);
+
+ animation-name: zoomIn;
+ animation-duration: 0.1s;
+
+ text-align: center;
+
+ color: var(--color-blue);
+
+ border: 4px dashed var(--color-blue);
+ background: rgba(255, 255, 255, 0.8);
+
+ box-shadow: 0 0 0 var(--default-small-padding) rgba(255, 255, 255, 0.9);
font-size: 42px;
align-items: center;
justify-content: center;
- & > div {
- padding: 40px;
-
- text-align: center;
- pointer-events: none;
+ &::before {
+ position: absolute;
+ z-index: -1;
- border-radius: 10px;
+ top: calc(-1 * var(--default-small-padding));
+ right: calc(-1 * var(--default-small-padding));
+ bottom: calc(-1 * var(--default-small-padding));
+ left: calc(-1 * var(--default-small-padding));
- line-height: 1.3em;
+ content: '';
}
}
+
+ &.over .dropzone-overlay {
+ display: flex;
+ }
+}
+
+@keyframes zoomIn {
+ 0% {
+ transform: scale3d(0.9, 0.9, 0.9);
+
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+.zoomIn {
+ -webkit-animation-name: zoomIn;
+ animation-name: zoomIn;
}
.rc-old .is-cordova {
@@ -5267,7 +4958,6 @@ body:not(.is-cordova) {
flex-direction: column;
height: 100%;
- padding: 20px;
}
&__header {
@@ -5281,16 +4971,6 @@ body:not(.is-cordova) {
flex: 1 1 auto;
}
- & .message-cog-container {
- & .message-action {
- display: none !important;
-
- &.jump-to-star-message {
- display: block !important;
- }
- }
- }
-
& .no-results {
text-align: center;
}
@@ -5514,23 +5194,6 @@ body:not(.is-cordova) {
float: right;
width: auto;
-
- & .message-cog-container {
- float: left;
- }
-
- & .message-dropdown {
- right: -2px;
- left: auto;
-
- & ul {
- flex-direction: row-reverse;
-
- & li:first-child i::before {
- content: "\d7";
- }
- }
- }
}
.rc-old .form-inline {
@@ -5605,8 +5268,6 @@ body:not(.is-cordova) {
right: 40px;
height: 100%;
-
- border-width: 0 0 0 1px;
}
}
@@ -5747,7 +5408,7 @@ body:not(.is-cordova) {
text-decoration: none;
color: #555555;
- border: 1px solid #eaeaea;
+ border: 1px solid var(--color-gray-light);
border-radius: var(--border-radius);
font-family: arial;
diff --git a/packages/rocketchat_theme/client/imports/general/forms.css b/app/theme/client/imports/general/forms.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/general/forms.css
rename to app/theme/client/imports/general/forms.css
diff --git a/packages/rocketchat_theme/client/imports/general/reset.css b/app/theme/client/imports/general/reset.css
similarity index 97%
rename from packages/rocketchat_theme/client/imports/general/reset.css
rename to app/theme/client/imports/general/reset.css
index 4a2980d47bf5..1a3a4b35414b 100644
--- a/packages/rocketchat_theme/client/imports/general/reset.css
+++ b/app/theme/client/imports/general/reset.css
@@ -115,10 +115,6 @@ section {
display: block;
}
-body {
- line-height: 1;
-}
-
ol,
ul {
list-style: none;
diff --git a/app/theme/client/imports/general/rtl.css b/app/theme/client/imports/general/rtl.css
new file mode 100644
index 000000000000..23b27c37b15e
--- /dev/null
+++ b/app/theme/client/imports/general/rtl.css
@@ -0,0 +1,564 @@
+.rtl {
+ direction: rtl;
+
+ & .rc-old .buttons-group .button {
+ margin-right: 4px;
+ margin-left: auto;
+
+ &:first-child {
+ margin-right: 0;
+ }
+ }
+
+ & .rc-old .page-container .content .rocket-form .submit {
+ text-align: left;
+ }
+
+ & button {
+ text-align: right;
+ }
+
+ & .text-right {
+ text-align: left;
+ }
+
+ & .main-content {
+ left: 0;
+
+ transition: left 0.25s cubic-bezier(0.5, 0, 0.1, 1);
+
+ &.flex-opened {
+ left: calc(var(--flex-tab-width) + 40px);
+ }
+ }
+
+ & .page-settings {
+ & .content > .info {
+ padding-left: 20px;
+ }
+
+ & .section {
+ border-right: none;
+ border-left: 1px solid #dddddd;
+
+ & .section-content .input-line > label {
+ text-align: right;
+ }
+ }
+ }
+
+ & .messages-box {
+ margin: 60px 0 0;
+
+ & .new-message {
+ right: 50%;
+ left: auto;
+ }
+
+ &.compact {
+ & .message {
+ padding: 5px 45px 5px 15px;
+
+ &.collapsed .thumb {
+ margin: 0;
+ }
+ }
+ }
+ }
+
+ & .terminal {
+ direction: ltr;
+ }
+
+ & .container-bars {
+ & .upload-progress {
+ & .upload-progress-progress {
+ right: 0;
+ left: auto;
+ }
+
+ & .upload-progress-text > a {
+ float: left;
+ }
+ }
+
+ & .unread-bar {
+ & > a.mark-read {
+ float: left;
+ }
+
+ & > a.jump-to {
+ float: right;
+ }
+ }
+ }
+
+ & .messages-container {
+ right: 0;
+ left: auto;
+
+ & .wrapper {
+ right: 0;
+ left: auto;
+ }
+
+ & .footer {
+ right: 0;
+ left: auto;
+ }
+
+ & .message-form {
+ & > div .input-message-container .inner-left-toolbar {
+ right: 13px;
+ left: auto;
+ }
+
+ & textarea {
+ padding-right: 49px;
+ padding-left: 8px;
+
+ text-align: right;
+
+ border-width: 0 0 0 1px;
+ border-right-width: 0;
+ }
+
+ & > .formatting-tips {
+ position: relative;
+ right: auto;
+
+ float: left;
+
+ & q {
+ padding: 0 3px 0 0;
+
+ border-right: 3px solid;
+ border-left: 0 none;
+ }
+ }
+ }
+ }
+
+ & .account-box .options {
+ direction: ltr;
+
+ & > .wrapper {
+ direction: rtl;
+ }
+ }
+
+ & .flex-tab-container {
+ border-width: 0 1px 0 0;
+ }
+
+ & .flex-tab-bar .tab-button.active {
+ margin-right: -1px;
+ margin-left: auto;
+
+ border-right: unset;
+ border-left: 3px solid #ff0000;
+ }
+
+ & .flex-tab .control {
+ padding: 12px 30px;
+
+ text-align: right;
+
+ & > a,
+ & > form {
+ float: right;
+ }
+
+ & .more {
+ right: 0;
+ left: auto;
+
+ transform: translateX(27px);
+ }
+
+ & .search-form {
+ width: 100%;
+ padding: 0 0 0 4px;
+
+ & .icon-plus {
+ right: 4px;
+ left: auto;
+ }
+ }
+
+ & .info-tabs {
+ right: auto;
+ left: 20px;
+
+ text-align: left;
+ }
+ }
+
+ & .flex-opened .flex-tab .control .more {
+ transform: translateX(0);
+ }
+
+ & .input-line {
+ &.search {
+ & .icon-spin {
+ right: auto;
+ left: 5px;
+ }
+
+ & .icon-search,
+ & .icon-right-open-small {
+ right: 2px;
+ left: auto;
+ }
+
+ & input {
+ padding-right: 20px;
+ padding-left: 8px;
+
+ text-align: right;
+ }
+ }
+
+ & > div .right {
+ right: auto;
+ left: 10px;
+ }
+
+ &.double-col {
+ & > label {
+ float: right;
+
+ padding: 10px 0 10px 20px;
+
+ text-align: left;
+ }
+
+ & > div {
+ float: right;
+
+ & label {
+ margin-right: auto;
+ margin-left: 4px;
+
+ &:nth-last-child(1) {
+ margin-right: auto;
+ margin-left: 0;
+ }
+
+ & input {
+ margin-right: auto;
+ margin-left: 4px;
+ }
+ }
+ }
+ }
+ }
+
+ & .user-image .avatar::after {
+ right: -12px;
+ left: auto;
+ }
+
+ & .lines .user-image {
+ & button > div {
+ float: right;
+ }
+
+ & p {
+ float: right;
+
+ padding-right: 10px;
+ padding-left: auto;
+ }
+ }
+
+ & .user-view {
+ & nav {
+ margin-right: -4px;
+ margin-left: auto;
+
+ & .back {
+ float: left;
+ }
+ }
+
+ & .stats li {
+ border-right: unset;
+ border-left: 2px;
+ }
+ }
+
+ & .burger {
+ right: 0;
+ left: auto;
+
+ margin-right: 7px;
+ margin-left: auto;
+
+ & .unread-burger-alert {
+ right: auto;
+ left: 4px;
+ }
+
+ &.menu-opened i {
+ &:nth-child(1) {
+ transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1);
+ }
+
+ &:nth-child(3) {
+ transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1);
+ }
+ }
+ }
+
+ & .arrow {
+ &::before,
+ &::after {
+ right: calc(50% - 5px);
+ }
+
+ &::before {
+ transform: rotate(135deg) translateX(4px);
+ }
+
+ &::after {
+ transform: rotate(-135deg) translateX(4px);
+ }
+
+ &.left {
+ &::before {
+ transform: rotate(-45deg) translateY(-4px);
+ }
+
+ &::after {
+ transform: rotate(45deg) translateY(4px);
+ }
+ }
+
+ &.top {
+ &::before {
+ transform: rotate(45deg) translateX(-2px) translateY(2px);
+ }
+
+ &::after {
+ transform: rotate(-45deg) translateX(2px) translateY(2px);
+ }
+ }
+
+ &.bottom {
+ &::before {
+ transform: rotate(-45deg) translateX(-2px) translateY(-2px);
+ }
+
+ &::after {
+ transform: rotate(45deg) translateX(2px) translateY(-2px);
+ }
+ }
+
+ &.close {
+ &::before {
+ transform: rotate(-45deg);
+ }
+
+ &::after {
+ transform: rotate(45deg);
+ }
+ }
+ }
+
+ & .message {
+ padding-right: 70px;
+ padding-left: 20px;
+
+ & .user {
+ margin-right: 0;
+ margin-left: 5px;
+ }
+
+ & .thumb {
+ right: 20px;
+ left: auto;
+ }
+
+ & .title .edited {
+ margin-right: 3px;
+ margin-left: auto;
+ }
+
+ & .private {
+ margin-right: 10px;
+ margin-left: auto;
+ }
+
+ &.sequential {
+ padding-top: 4px;
+
+ & .title {
+ right: 5px;
+ left: auto;
+
+ text-align: left;
+
+ & .edited {
+ margin-right: 0;
+ margin-left: auto;
+ padding-right: 0;
+ padding-left: auto;
+
+ border-right: 0;
+ }
+
+ & .message-action {
+ float: right;
+
+ margin-right: 1px;
+ margin-left: auto;
+ }
+ }
+ }
+
+ &.collapsed {
+ & > .thumb {
+ margin: 0 16px 0 0;
+ }
+ }
+ }
+
+ & blockquote {
+ padding-right: 10px;
+ padding-left: auto;
+
+ &::before {
+ right: 0;
+ left: auto;
+ }
+ }
+
+ & .ticks-bar {
+ right: auto;
+ left: 2px;
+ }
+
+ & .fixed-title {
+ right: 0;
+ left: auto;
+
+ padding: 0 20px 0 10px;
+ }
+
+ & .list-view {
+ & > .title .see-all {
+ float: left;
+ }
+ }
+
+ & .page-list .list {
+ & a .info ul {
+ margin-right: 3px;
+ margin-left: auto;
+ }
+
+ & .user-image {
+ float: left;
+
+ margin-right: 12px;
+ margin-left: auto;
+ }
+
+ & table thead th {
+ text-align: right;
+ }
+ }
+
+ & .statistics-table {
+ & th,
+ & td {
+ text-align: right;
+ }
+ }
+
+ & .code-mirror-box {
+ direction: ltr;
+
+ & .buttons {
+ text-align: left;
+ }
+
+ &.code-mirror-box-fullscreen {
+ right: 260px;
+ left: 40px;
+
+ & .title {
+ padding-right: 10px;
+ padding-left: unset;
+ direction: rtl;
+ }
+ }
+ }
+
+ & .rocket-form {
+ & legend::after {
+ right: 0;
+ }
+
+ & .logoutOthers {
+ text-align: left;
+ }
+
+ & .submit {
+ text-align: left;
+ }
+ }
+
+ /* Override toastr messages to show on hte left side */
+ & .toast-top-right {
+ right: auto;
+ left: 12px;
+ }
+
+ & .toolbar-search__icon {
+ right: 0;
+ }
+
+ & .toolbar-search__icon--cancel {
+ right: auto;
+ left: 0;
+ }
+
+ & .message-popup.search-results-list {
+ padding: 25px 8px 0 0;
+
+ text-align: right;
+ direction: ltr;
+
+ & .popup-item {
+ direction: rtl;
+ }
+ }
+
+ @media (width <= 1100px) {
+ & #rocket-chat .flex-opened {
+ left: 0;
+
+ & .flex-tab {
+ transform: translateX(calc(100% + 40px));
+ }
+ }
+ }
+
+ @media (width <= 780px) {
+ & #rocket-chat {
+ & .main-content {
+ right: 0;
+ }
+
+ & .fixed-title h2 {
+ margin-right: 45px;
+ }
+ }
+
+ & .code-mirror-box.code-mirror-box-fullscreen {
+ right: 0;
+ }
+ }
+}
diff --git a/packages/rocketchat_theme/client/imports/general/typography.css b/app/theme/client/imports/general/typography.css
similarity index 100%
rename from packages/rocketchat_theme/client/imports/general/typography.css
rename to app/theme/client/imports/general/typography.css
diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css
new file mode 100644
index 000000000000..8404b70812c7
--- /dev/null
+++ b/app/theme/client/imports/general/variables.css
@@ -0,0 +1,378 @@
+:root {
+ /*
+ * Color palette
+ */
+ --color-dark-100: #0c0d0f;
+ --color-dark-90: #1e232a;
+ --color-dark-80: #2e343e;
+ --color-dark-70: #53585f;
+ --color-dark-30: #9da2a9;
+ --color-dark-20: #caced1;
+ --color-dark-10: #e0e5e8;
+ --color-dark-05: #f1f2f4;
+ --color-dark-blue: #175cc4;
+ --color-blue: #1d74f5;
+ --color-light-blue: #4eb2f5;
+ --color-lighter-blue: #e8f2ff;
+ --color-purple: #861da8;
+ --color-red: #f5455c;
+ --color-dark-red: #e0364d;
+ --color-orange: #f59547;
+ --color-yellow: #ffd21f;
+ --color-dark-yellow: #f6c502;
+ --color-green: #2de0a5;
+ --color-dark-green: #26d198;
+
+ /*
+ * General Colors
+ */
+ --color-darkest: #1f2329;
+ --color-dark: #2f343d;
+ --color-dark-medium: #414852;
+ --color-dark-light: #6c727a;
+ --color-gray: #9ea2a8;
+ --color-gray-medium: #cbced1;
+ --color-gray-light: #e1e5e8;
+ --color-gray-lightest: #f2f3f5;
+ --color-black: #000000;
+ --color-white: #ffffff;
+ --rc-color-error: var(--color-red);
+ --rc-color-error-light: #e1364c;
+ --rc-color-alert: var(--color-yellow);
+ --rc-color-alert-light: var(--color-dark-yellow);
+ --rc-color-success: var(--color-green);
+ --rc-color-success-light: #25d198;
+ --rc-color-button-primary: var(--color-blue);
+ --rc-color-button-primary-light: var(--color-dark-blue);
+ --rc-color-alert-message-primary: var(--color-blue);
+ --rc-color-alert-message-primary-background: #f1f6ff;
+ --rc-color-alert-message-secondary: #7ca52b;
+ --rc-color-alert-message-secondary-background: #fafff1;
+ --rc-color-alert-message-warning: #d52d24;
+ --rc-color-alert-message-warning-background: #fff3f3;
+ --rc-color-primary: var(--color-dark);
+ --rc-color-primary-darkest: var(--color-darkest);
+ --rc-color-primary-dark: var(--color-dark-medium);
+ --rc-color-primary-light: var(--color-gray);
+ --rc-color-primary-light-medium: var(--color-gray-medium);
+ --rc-color-primary-lightest: var(--color-gray-lightest);
+ --rc-color-content: var(--color-white);
+ --rc-color-link-active: var(--rc-color-button-primary);
+
+ /*
+ * General
+ */
+ --text-size: 0.875rem;
+ --header-min-height: 60px;
+ --toolbar-height: 55px;
+ --footer-min-height: 70px;
+ --rooms-box-width: 280px;
+ --flex-tab-width: 400px;
+ --flex-tab-webrtc-width: 400px;
+ --flex-tab-webrtc-2-width: 850px;
+ --user-image-square: 20px;
+ --border: 2px;
+ --border-radius: 2px;
+ --status-online: var(--rc-color-success);
+ --status-away: var(--rc-color-alert);
+ --status-busy: var(--rc-color-error);
+ --status-invisible: var(--color-gray-medium);
+ --status-invisible-sidebar: var(--rc-color-primary-darkest);
+ --default-padding: 1.5rem;
+ --default-small-padding: 1rem;
+ --status-bullet-size: 10px;
+ --status-bullet-radius: 50%;
+ --account-username-weight: 700;
+ --status-name-weight: 400;
+ --default-font-weight-header: 500;
+
+ /*
+ * General Typography
+ */
+ --text-default-size: 1rem;
+ --text-default-weight: 500;
+ --text-small-size: 0.875rem;
+ --text-small-weight: 500;
+ --text-heading-size: 1.375rem;
+ --text-heading-weight: 700;
+ --text-label-size: 075rem;
+ --text-label-weight: 600;
+ --text-tiny-size: 075rem;
+ --text-tiny-weight: 400;
+ --text-micro-size: 0.625rem;
+ --text-micro-weight: 700;
+
+ /*
+ * Forms
+ */
+ --gap-between-elements: 2.5rem;
+ --label-margin-bottom: 1rem;
+
+ /*
+ * Forms - Button
+ */
+ --button-square-size: 36px;
+ --button-padding: 0.782rem;
+ --button-padding-small: 0 0.5rem;
+ --button-height-small: 28px;
+ --button-text-size-small: 13px;
+ --button-text-size: var(--input-font-size);
+ --button-border-width: var(--border);
+ --button-border-radius: var(--border-radius);
+ --button-disabled-background: var(--color-gray-light);
+ --button-disabled-text-color: var(--color-white);
+ --button-primary-background: var(--rc-color-button-primary);
+ --button-primary-text-color: var(--color-white);
+ --button-cancel-color: var(--rc-color-error);
+ --button-secondary-background: var(--color-gray-medium);
+ --button-secondary-text-color: var(--color-dark-medium);
+
+ /*
+ * Forms - Input
+ */
+ --input-font-size: 0.875rem;
+ --input-title-text-size: var(--input-font-size);
+ --input-title-color: #2d343d;
+ --input-text-color: var(--color-dark-medium);
+ --input-placeholder-color: var(--color-gray-medium);
+ --input-icon-color: var(--color-dark);
+ --input-border-color: var(--color-gray-light);
+ --input-border-width: var(--border);
+ --input-border-radius: var(--border-radius);
+ --input-description-text-color: var(--color-gray);
+ --input-description-text-size: var(--input-font-size);
+ --input-error-color: var(--rc-color-error);
+
+ /*
+ * Forms - popup list
+ */
+ --popup-list-border-radius: var(--border-radius);
+ --popup-list-background: var(--color-white);
+ --popup-list-background-hover: var(--color-gray-lightest);
+ --popup-list-selected-background: var(--color-gray-lightest);
+ --popup-list-name-color: #2d343d;
+ --popup-list-name-size: 1rem;
+
+ /*
+ * Forms - tags
+ */
+ --tags-border-width: var(--border);
+ --tags-border-radius: var(--border-radius);
+ --tags-border-color: var(--color-gray-light);
+ --tags-text-color: var(--rc-color-primary);
+ --tags-background: #f2f3f5;
+ --tags-avatar-size: 20px;
+
+ /*
+ * Forms - select avatar
+ */
+ --select-avatar-size: 48px;
+ --select-avatar-preview-size: 150px;
+ --select-avatar-upload-background: var(--color-gray-light);
+ --select-avatar-upload-color: #2d343d;
+
+ /*
+ * Sidebar
+ */
+ --sidebar-width: 280px;
+ --sidebar-small-width: 90%;
+ --sidebar-background: var(--rc-color-primary);
+ --sidebar-background-hover: var(--rc-color-primary-dark);
+ --sidebar-background-light: var(--rc-color-primary-lightest);
+ --sidebar-background-light-hover: var(--rc-color-primary-light);
+ --sidebar-default-padding: 24px;
+ --sidebar-small-default-padding: 16px;
+ --sidebar-extra-small-default-padding: 12px;
+ --sidebar-footer-height: 48px;
+ --sidebar-small-header-padding: var(--sidebar-small-default-padding);
+
+ /*
+ * Sidebar flex
+ */
+ --sidebar-flex-search-background: var(--color-white);
+ --sidebar-flex-search-placeholder-color: var(--color-gray);
+
+ /*
+ * Sidebar Account
+ */
+ --sidebar-account-thumb-size: 23px;
+ --sidebar-small-account-thumb-size: 40px;
+ --sidebar-account-status-bullet-size: 10px;
+ --sidebar-small-account-status-bullet-size: 8px;
+ --sidebar-account-status-bullet-radius: 50%;
+ --sidebar-account-username-size: 1rem;
+ --sidebar-account-username-weight: 700;
+ --sidebar-small-account-username-weight: 400;
+ --sidebar-account-username-color: var(--color-white);
+ --sidebar-account-username-color-darker: var(--color-dark);
+ --sidebar-account-status-font-size: 0.875rem;
+ --sidebar-account-status-color: var(--color-gray);
+
+ /*
+ * Sidebar Item
+ */
+ --sidebar-item-radius: 2px;
+ --sidebar-item-height: 24px;
+ --sidebar-item-height-medium: 34px;
+ --sidebar-item-height-extended: 52px;
+ --sidebar-item-thumb-size: 18px;
+ --sidebar-item-thumb-size-medium: 27px;
+ --sidebar-item-thumb-size-extended: 36px;
+ --sidebar-item-text-color: var(--color-gray);
+ --sidebar-item-background: inherit;
+ --sidebar-item-hover-background: var(--rc-color-primary-darkest);
+ --sidebar-item-active-background: var(--rc-color-primary-dark);
+ --sidebar-item-active-color: var(--sidebar-item-text-color);
+ --sidebar-item-unread-color: var(--color-white);
+ --sidebar-item-unread-font-weight: 600;
+ --sidebar-item-popup-background: var(--color-dark-medium);
+ --sidebar-item-user-status-size: 6px;
+ --sidebar-item-user-status-radius: 50%;
+ --sidebar-item-text-size: 0.875rem;
+
+ /*
+ * Modal
+ */
+ --modal-wrapper-width: 650px;
+ --modal-wrapper-margin: 3rem;
+ --modal-back-button-color: var(--color-gray);
+
+ /*
+ * Modal - Create Channel
+ */
+ --create-channel-gap-between-elements: 1rem;
+ --create-channel-title-color: var(--color-darkest);
+ --create-channel-title-text-size: 1.375rem;
+ --create-channel-description-color: var(--color-gray);
+ --create-channel-description-text-size: 0.875rem;
+
+ /*
+ * Toolbar
+ */
+ --toolbar-placeholder-color: var(--color-gray);
+
+ /*
+ * Rooms list
+ */
+ --rooms-list-title-color: var(--color-gray);
+ --rooms-list-title-text-size: 0.75rem;
+ --rooms-list-empty-text-color: var(--color-gray);
+ --rooms-list-empty-text-size: 0.75rem;
+ --rooms-list-padding: var(--sidebar-default-padding);
+ --rooms-list-small-padding: var(--sidebar-small-default-padding);
+
+ /*
+ * Chip
+ */
+ --chip-background: #dddddd;
+
+ /*
+ * Avatar
+ */
+ --avatar-radius: var(--border-radius);
+ --avatar-initials-text-size: 22px;
+ --avatar-initials-text-weight: 700;
+
+ /*
+ * Badge
+ */
+ --badge-text-color: var(--color-white);
+ --badge-radius: 12px;
+ --badge-text-size: 0.75rem;
+ --badge-background: var(--rc-color-primary-dark);
+ --badge-unread-background: var(--rc-color-primary-dark);
+ --badge-user-mentions-background: var(--color-dark-blue);
+ --badge-group-mentions-background: var(--rc-color-primary-dark);
+
+ /*
+ * Mention link
+ */
+ --mention-link-radius: 10px;
+ --mention-link-background: var(--color-lighter-blue);
+ --mention-link-text-color: var(--color-dark-blue);
+ --mention-link-me-background: var(--color-dark-blue);
+ --mention-link-me-text-color: var(--color-white);
+ --mention-link-group-background: var(--rc-color-primary-dark);
+ --mention-link-group-text-color: var(--color-white);
+
+ /*
+ * Message box
+ */
+ --message-box-text-size: var(--input-font-size);
+ --message-box-placeholder-color: var(--color-gray-medium);
+ --message-box-markdown-color: var(--color-gray);
+ --message-box-markdown-hover-color: var(--color-dark);
+ --message-box-user-typing-color: var(--color-gray);
+ --message-box-user-typing-text-size: 0.75rem;
+ --message-box-user-typing-user-color: var(--color-dark);
+ --message-box-container-border-color: var(--color-gray-medium);
+ --message-box-container-border-width: var(--border);
+ --message-box-container-border-radius: var(--border-radius);
+ --message-box-editing-color: #fff5df;
+ --message-box-popover-title-text-color: var(--color-gray);
+ --message-box-popover-title-text-size: 0.75rem;
+
+ /*
+ * Header
+ */
+ --header-height: 77px;
+ --header-padding: 16px;
+ --header-toggle-favorite-color: var(--color-gray-medium);
+ --header-toggle-favorite-star-color: var(--rc-color-alert-light);
+ --header-toggle-encryption-off-color: var(--color-gray-medium);
+ --header-toggle-encryption-on-color: var(--rc-color-alert-message-secondary);
+ --header-title-username-color-darker: var(--color-dark);
+ --header-title-font-size: var(--text-default-size);
+ --header-title-font-size--subtitle: var(--text-small-size);
+ --header-title-status-color: var(--color-gray);
+ --header-title-username-weight: 400;
+ --header-title-status-name-weight: 400;
+ --header-title-status-bullet-radius: var(--status-bullet-radius);
+ --header-title-status-bullet-size: var(--status-bullet-size);
+ --header-background-color: var(--color-white);
+
+ /*
+ * Flex nav
+ */
+ --flex-nav-background: var(--color-gray-lightest);
+
+ /*
+ * Popover
+ */
+ --popover-padding: 1rem;
+ --popover-radius: var(--border-radius);
+ --popover-background: var(--color-white);
+ --popover-column-min-width: 130px;
+ --popover-column-padding: 1rem;
+ --popover-title-color: var(--color-dark);
+ --popover-title-text-size: 0.75rem;
+ --popover-item-color: var(--color-dark);
+ --popover-item-text-size: 0.875rem;
+ --popover-divider-height: 2px;
+ --popover-divider-color: var(--color-gray-light);
+
+ /*
+ * Tooltip
+ */
+ --tooltip-background: var(--color-darkest);
+ --tooltip-text-color: var(--color-white);
+ --tooltip-text-size: 0.75rem;
+ --tooltip-radius: var(--border-radius);
+
+ /*
+ * alert
+ */
+ --alerts-padding: var(--sidebar-default-padding);
+ --alerts-padding-vertical: 10px;
+ --alerts-padding-vertical-large: 20px;
+ --alerts-background: #1d73f5;
+ --alerts-color: var(--color-white);
+ --alerts-font-size: var(--text-default-size);
+ --content-page-padding: 2.5rem;
+
+ /*
+ * badge
+ */
+ --badge-size: 14px;
+ --badge-font-size: 0.625rem;
+}
diff --git a/packages/rocketchat_theme/client/index.js b/app/theme/client/index.js
similarity index 100%
rename from packages/rocketchat_theme/client/index.js
rename to app/theme/client/index.js
diff --git a/packages/rocketchat_theme/client/main.css b/app/theme/client/main.css
similarity index 97%
rename from packages/rocketchat_theme/client/main.css
rename to app/theme/client/main.css
index b70e1ebb299a..d1b828f16b15 100644
--- a/packages/rocketchat_theme/client/main.css
+++ b/app/theme/client/main.css
@@ -53,7 +53,6 @@
/* Modal */
@import 'imports/components/modal/full-modal.css';
@import 'imports/components/modal/create-channel.css';
-@import 'imports/components/modal/directory.css';
/* User Info */
@import 'imports/components/userInfo.css';
diff --git a/packages/rocketchat_theme/client/vendor/fontello/config.json b/app/theme/client/vendor/fontello/config.json
similarity index 100%
rename from packages/rocketchat_theme/client/vendor/fontello/config.json
rename to app/theme/client/vendor/fontello/config.json
diff --git a/packages/rocketchat_theme/client/vendor/fontello/css/fontello.css b/app/theme/client/vendor/fontello/css/fontello.css
similarity index 100%
rename from packages/rocketchat_theme/client/vendor/fontello/css/fontello.css
rename to app/theme/client/vendor/fontello/css/fontello.css
diff --git a/packages/rocketchat_theme/client/vendor/jscolor.js b/app/theme/client/vendor/jscolor.js
similarity index 100%
rename from packages/rocketchat_theme/client/vendor/jscolor.js
rename to app/theme/client/vendor/jscolor.js
diff --git a/packages/rocketchat_theme/client/vendor/photoswipe.css b/app/theme/client/vendor/photoswipe.css
similarity index 100%
rename from packages/rocketchat_theme/client/vendor/photoswipe.css
rename to app/theme/client/vendor/photoswipe.css
diff --git a/packages/rocketchat_theme/server/index.js b/app/theme/server/index.js
similarity index 100%
rename from packages/rocketchat_theme/server/index.js
rename to app/theme/server/index.js
diff --git a/app/theme/server/server.js b/app/theme/server/server.js
new file mode 100644
index 000000000000..7520ca248c36
--- /dev/null
+++ b/app/theme/server/server.js
@@ -0,0 +1,175 @@
+import _ from 'underscore';
+import less from 'less';
+import Autoprefixer from 'less-plugin-autoprefix';
+import crypto from 'crypto';
+
+import { WebApp } from 'meteor/webapp';
+import { Meteor } from 'meteor/meteor';
+import { Inject } from 'meteor/meteorhacks:inject-initial';
+
+import { settings } from '../../settings';
+import { Logger } from '../../logger';
+import { getURL } from '../../utils/lib/getURL';
+
+const logger = new Logger('rocketchat:theme', {
+ methods: {
+ stop_rendering: {
+ type: 'info',
+ },
+ },
+});
+
+let currentHash = '';
+let currentSize = 0;
+
+export const theme = new class {
+ constructor() {
+ this.variables = {};
+ this.packageCallbacks = [];
+ this.files = ['server/colors.less'];
+ this.customCSS = '';
+ settings.add('css', '');
+ settings.addGroup('Layout');
+ settings.onload('css', Meteor.bindEnvironment((key, value, initialLoad) => {
+ if (!initialLoad) {
+ Meteor.startup(function() {
+ process.emit('message', {
+ refresh: 'client',
+ });
+ });
+ }
+ }));
+ this.compileDelayed = _.debounce(Meteor.bindEnvironment(this.compile.bind(this)), 100);
+ Meteor.startup(() => {
+ settings.onAfterInitialLoad(() => {
+ settings.get(/^theme-./, Meteor.bindEnvironment((key, value) => {
+ if (key === 'theme-custom-css' && value != null) {
+ this.customCSS = value;
+ } else {
+ const name = key.replace(/^theme-[a-z]+-/, '');
+ if (this.variables[name] != null) {
+ this.variables[name].value = value;
+ }
+ }
+
+ this.compileDelayed();
+ }));
+ });
+ });
+ }
+
+ compile() {
+ let content = [this.getVariablesAsLess()];
+
+ content.push(...this.files.map((name) => Assets.getText(name)));
+
+ content.push(...this.packageCallbacks.map((name) => name()));
+
+ content.push(this.customCSS);
+ content = content.join('\n');
+ const options = {
+ compress: true,
+ plugins: [new Autoprefixer()],
+ };
+ const start = Date.now();
+ return less.render(content, options, function(err, data) {
+ logger.stop_rendering(Date.now() - start);
+ if (err != null) {
+ return console.log(err);
+ }
+ settings.updateById('css', data.css);
+
+ return Meteor.startup(function() {
+ return Meteor.setTimeout(function() {
+ return process.emit('message', {
+ refresh: 'client',
+ });
+ }, 200);
+ });
+ });
+ }
+
+ addColor(name, value, section, properties) {
+ const config = {
+ group: 'Colors',
+ type: 'color',
+ editor: 'color',
+ public: true,
+ properties,
+ section,
+ };
+
+ return settings.add(`theme-color-${ name }`, value, config);
+ }
+
+ addVariable(type, name, value, section, persist = true, editor, allowedTypes, property) {
+ this.variables[name] = {
+ type,
+ value,
+ };
+ if (persist) {
+ const config = {
+ group: 'Layout',
+ type,
+ editor: editor || type,
+ section,
+ public: true,
+ allowedTypes,
+ property,
+ };
+ return settings.add(`theme-${ type }-${ name }`, value, config);
+ }
+
+ }
+
+ addPublicColor(name, value, section, editor = 'color', property) {
+ return this.addVariable('color', name, value, section, true, editor, ['color', 'expression'], property);
+ }
+
+ addPublicFont(name, value) {
+ return this.addVariable('font', name, value, 'Fonts', true);
+ }
+
+ getVariablesAsObject() {
+ return Object.keys(this.variables).reduce((obj, name) => {
+ obj[name] = this.variables[name].value;
+ return obj;
+ }, {});
+ }
+
+ getVariablesAsLess() {
+ return Object.keys(this.variables).map((name) => {
+ const variable = this.variables[name];
+ return `@${ name }: ${ variable.value };`;
+ }).join('\n');
+ }
+
+ addPackageAsset(cb) {
+ this.packageCallbacks.push(cb);
+ return this.compileDelayed();
+ }
+
+ getCss() {
+ return settings.get('css') || '';
+ }
+};
+
+settings.get('css', (key, value = '') => {
+ currentHash = crypto.createHash('sha1').update(value).digest('hex');
+ currentSize = value.length;
+ Inject.rawHead('css-theme', ` `);
+});
+
+WebApp.rawConnectHandlers.use(function(req, res, next) {
+ const path = req.url.split('?')[0];
+ const prefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
+ if (path !== `${ prefix }/theme.css`) {
+ return next();
+ }
+
+ res.setHeader('Content-Type', 'text/css; charset=UTF-8');
+ res.setHeader('Content-Length', currentSize);
+ res.setHeader('ETag', `"${ currentHash }"`);
+ res.write(theme.getCss());
+ res.end();
+});
diff --git a/packages/rocketchat_theme/server/variables.js b/app/theme/server/variables.js
similarity index 75%
rename from packages/rocketchat_theme/server/variables.js
rename to app/theme/server/variables.js
index 8596dbbb8ea6..1214f7d7ddf7 100644
--- a/packages/rocketchat_theme/server/variables.js
+++ b/app/theme/server/variables.js
@@ -1,4 +1,5 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { settings } from '../../settings';
+import { theme } from './server';
// TODO: Define registers/getters/setters for packages to work with established
// heirarchy of colors instead of making duplicate definitions
// TODO: Settings pages to show simple separation of major/minor/addon colors
@@ -21,20 +22,20 @@ const colors = [...Assets.getText('client/imports/general/variables.css').match(
colors.forEach(([key, color]) => {
if (/var/.test(color)) {
const [, value] = color.match(/var\(--(.*?)\)/i);
- return RocketChat.theme.addPublicColor(key, value, 'Colors', 'expression');
+ return theme.addPublicColor(key, value, 'Colors', 'expression');
}
- RocketChat.theme.addPublicColor(key, color, 'Colors');
+ theme.addPublicColor(key, color, 'Colors');
});
const majorColors = {
'content-background-color': '#FFFFFF',
'primary-background-color': '#04436A',
'primary-font-color': '#444444',
- 'primary-action-color': '#13679A', // was action-buttons-color
+ 'primary-action-color': '#1d74f5', // was action-buttons-color
'secondary-background-color': '#F4F4F4',
'secondary-font-color': '#A0A0A0',
'secondary-action-color': '#DDDDDD',
- 'component-color': '#EAEAEA',
+ 'component-color': '#f2f3f5',
'success-color': '#4dff4d',
'pending-color': '#FCB316',
'error-color': '#BC2031',
@@ -58,17 +59,17 @@ const minorColors = {
// Bulk-add settings for color scheme
Object.keys(majorColors).forEach((key) => {
const value = majorColors[key];
- RocketChat.theme.addPublicColor(key, value, 'Old Colors');
+ theme.addPublicColor(key, value, 'Old Colors');
});
Object.keys(minorColors).forEach((key) => {
const value = minorColors[key];
- RocketChat.theme.addPublicColor(key, value, 'Old Colors (minor)', 'expression');
+ theme.addPublicColor(key, value, 'Old Colors (minor)', 'expression');
});
-RocketChat.theme.addPublicFont('body-font-family', '-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Oxygen, Ubuntu, Cantarell, \'Helvetica Neue\', \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Meiryo UI\', Arial, sans-serif');
+theme.addPublicFont('body-font-family', '-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Oxygen, Ubuntu, Cantarell, \'Helvetica Neue\', \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Segoe UI Symbol\', \'Meiryo UI\', Arial, sans-serif');
-RocketChat.settings.add('theme-custom-css', '', {
+settings.add('theme-custom-css', '', {
group: 'Layout',
type: 'code',
code: 'text/css',
@@ -76,4 +77,3 @@ RocketChat.settings.add('theme-custom-css', '', {
section: 'Custom CSS',
public: true,
});
-
diff --git a/app/threads/README.md b/app/threads/README.md
new file mode 100644
index 000000000000..513899a0c82b
--- /dev/null
+++ b/app/threads/README.md
@@ -0,0 +1,17 @@
+# Threads
+
+
+# TODO
+
+* Tests
+ * reply to message and check if thread is created
+ * reply to a msg reply and see if thead id updated
+ * remove the unique reply of a thread and see if the thread is removed
+ * remove some message from a thread and see if tlm is updated
+ * open threads list
+ * start a thread sending a reply, the thread should appear on the list
+ * remove a thread, the thread should be removed from the list
+ * open a thread
+ * send a new reply, the message should appear
+ * delete a reply, the message should be removed
+ * remove the thread, the panel should close
diff --git a/app/threads/client/flextab/thread.html b/app/threads/client/flextab/thread.html
new file mode 100644
index 000000000000..3424cfff3b9a
--- /dev/null
+++ b/app/threads/client/flextab/thread.html
@@ -0,0 +1,34 @@
+
+
+
+ {{_ "Drop_to_upload_file"}}
+
+ {{> messageBox messageBoxData}}
+
+
diff --git a/app/threads/client/flextab/thread.js b/app/threads/client/flextab/thread.js
new file mode 100644
index 000000000000..353dd7f3e927
--- /dev/null
+++ b/app/threads/client/flextab/thread.js
@@ -0,0 +1,176 @@
+import _ from 'underscore';
+
+import { Mongo } from 'meteor/mongo';
+import { Template } from 'meteor/templating';
+import { ReactiveDict } from 'meteor/reactive-dict';
+import { Tracker } from 'meteor/tracker';
+
+import { ChatMessages } from '../../../ui';
+import { normalizeThreadMessage, call } from '../../../ui-utils/client';
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
+import { Messages } from '../../../models';
+import { lazyloadtick } from '../../../lazy-load';
+import { fileUpload } from '../../../ui/client/lib/fileUpload';
+import { dropzoneEvents } from '../../../ui/client/views/app/room';
+import { upsert } from '../upsert';
+import './thread.html';
+
+const sort = { ts: 1 };
+
+Template.thread.events({
+ ...dropzoneEvents,
+ 'click .js-close'(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const { close } = this;
+ return close && close();
+ },
+ 'scroll .js-scroll-thread': _.throttle(({ currentTarget: e }, i) => {
+ lazyloadtick();
+ i.atBottom = e.scrollTop >= e.scrollHeight - e.clientHeight;
+ }, 500),
+ 'load img'() {
+ const { atBottom } = this;
+ atBottom && this.sendToBottom();
+ },
+});
+
+Template.thread.helpers({
+ threadTitle() {
+ return normalizeThreadMessage(Template.currentData().mainMessage);
+ },
+ mainMessage() {
+ return Template.parentData().mainMessage;
+ },
+ isLoading() {
+ return Template.instance().state.get('loading') !== false;
+ },
+ messages() {
+ const { Threads, state } = Template.instance();
+ const tmid = state.get('tmid');
+ return Threads.find({ tmid }, { sort });
+ },
+ messageContext() {
+ const result = messageContext.apply(this);
+ return {
+ ...result,
+ settings: {
+ ...result.settings,
+ showReplyButton: false,
+ showreply:false,
+ },
+ };
+ },
+ messageBoxData() {
+ const instance = Template.instance();
+ const { mainMessage: { rid, _id: tmid } } = this;
+
+ return {
+ rid,
+ tmid,
+ onSend: (...args) => instance.chatMessages && instance.chatMessages.send.apply(instance.chatMessages, args),
+ onKeyUp: (...args) => instance.chatMessages && instance.chatMessages.keyup.apply(instance.chatMessages, args),
+ onKeyDown: (...args) => instance.chatMessages && instance.chatMessages.keydown.apply(instance.chatMessages, args),
+ };
+ },
+});
+
+
+Template.thread.onRendered(function() {
+ const rid = Tracker.nonreactive(() => this.state.get('rid'));
+ const tmid = Tracker.nonreactive(() => this.state.get('tmid'));
+
+ this.chatMessages = new ChatMessages;
+ this.chatMessages.initializeWrapper(this.find('.js-scroll-thread'));
+ this.chatMessages.initializeInput(this.find('.js-input-message'), { rid, tmid });
+
+ this.onFile = (filesToUpload) => {
+ fileUpload(filesToUpload, this.chatMessages.input, { rid: this.state.get('rid'), tmid: this.state.get('tmid') });
+ };
+
+ this.sendToBottom = _.throttle(() => {
+ this.chatMessages.wrapper.scrollTop = this.chatMessages.wrapper.scrollHeight;
+ }, 300);
+
+ this.autorun(() => {
+ const tmid = this.state.get('tmid');
+ this.state.set({
+ tmid,
+ });
+ this.loadMore();
+ });
+
+ this.autorun(() => {
+ const tmid = this.state.get('tmid');
+ this.threadsObserve && this.threadsObserve.stop();
+
+ this.threadsObserve = Messages.find({ tmid, _updatedAt: { $gt: new Date() } }, {
+ fields: {
+ collapsed: 0,
+ threadMsg: 0,
+ repliesCount: 0,
+ },
+ }).observe({
+ added: ({ _id, ...message }) => {
+ const { atBottom } = this;
+ this.Threads.upsert({ _id }, message);
+ atBottom && this.sendToBottom();
+ },
+ changed: ({ _id, ...message }) => {
+ const { atBottom } = this;
+ this.Threads.update({ _id }, message);
+ atBottom && this.sendToBottom();
+ },
+ removed: ({ _id }) => this.Threads.remove(_id),
+ });
+ });
+
+ this.autorun(() => {
+ const rid = this.state.get('rid');
+ const tmid = this.state.get('tmid');
+ this.chatMessages.initializeInput(this.find('.js-input-message'), { rid, tmid });
+ });
+
+ Tracker.afterFlush(() => {
+ this.autorun(async () => {
+ const { mainMessage } = Template.currentData();
+ this.state.set({
+ tmid: mainMessage._id,
+ rid: mainMessage.rid,
+ });
+ });
+ });
+});
+
+Template.thread.onCreated(async function() {
+ this.Threads = new Mongo.Collection(null);
+
+ this.state = new ReactiveDict({
+ });
+
+ this.loadMore = _.debounce(async () => {
+ if (this.state.get('loading') === true) {
+ return;
+ }
+
+ const { tmid } = Tracker.nonreactive(() => this.state.all());
+
+ this.state.set('loading', true);
+
+ const messages = await call('getThreadMessages', { tmid });
+
+ upsert(this.Threads, messages);
+
+ Tracker.afterFlush(() => {
+ this.state.set('loading', false);
+ });
+
+
+ }, 500);
+});
+
+Template.thread.onDestroyed(function() {
+ const { Threads, threadsObserve } = this;
+ Threads.remove({});
+ threadsObserve && threadsObserve.stop();
+});
diff --git a/app/threads/client/flextab/threadlist.js b/app/threads/client/flextab/threadlist.js
new file mode 100644
index 000000000000..3a88fb8eed73
--- /dev/null
+++ b/app/threads/client/flextab/threadlist.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { Session } from 'meteor/session';
+import { TabBar } from '../../../ui-utils/client';
+import { Subscriptions } from '../../../models/client';
+
+Meteor.startup(function() {
+ return TabBar.addButton({
+ groups: ['channel', 'group', 'direct'],
+ id: 'thread',
+ i18nTitle: 'Threads',
+ icon: 'thread',
+ template: 'threads',
+ badge: () => {
+ const subscription = Subscriptions.findOne({ rid: Session.get('openedRoom') }, { fields: { tunread: 1 } });
+ if (subscription) {
+ return subscription.tunread && subscription.tunread.length && { body: subscription.tunread.length > 99 ? '99+' : subscription.tunread.length };
+ }
+ },
+ order: 0,
+ });
+});
diff --git a/app/threads/client/flextab/threads.html b/app/threads/client/flextab/threads.html
new file mode 100644
index 000000000000..a79423b54ff1
--- /dev/null
+++ b/app/threads/client/flextab/threads.html
@@ -0,0 +1,39 @@
+
+ {{# with messageContext}}
+ {{#if hasNoThreads}}
+ {{_ "No_Threads"}}
+ {{/if}}
+ {{#unless doDotLoadThreads}}
+
+ {{#if isLoading}}
+
+ {{> loading}}
+
+ {{/if}}
+ {{/unless}}
+ {{#if message}}
+
+ {{> thread mainMessage=message room=room subscription=subscription settings=settings close=close}}
+
+ {{/if}}
+ {{/with}}
+
diff --git a/app/threads/client/flextab/threads.js b/app/threads/client/flextab/threads.js
new file mode 100644
index 000000000000..89925f27226e
--- /dev/null
+++ b/app/threads/client/flextab/threads.js
@@ -0,0 +1,169 @@
+import { Mongo } from 'meteor/mongo';
+import { Tracker } from 'meteor/tracker';
+import { Template } from 'meteor/templating';
+import { ReactiveDict } from 'meteor/reactive-dict';
+
+import _ from 'underscore';
+
+import { lazyloadtick } from '../../../lazy-load';
+import { call } from '../../../ui-utils';
+import { Messages, Subscriptions } from '../../../models';
+import { messageContext } from '../../../ui-utils/client/lib/messageContext';
+import { messageArgs } from '../../../ui-utils/client/lib/messageArgs';
+import { getConfig } from '../../../ui-utils/client/config';
+
+import { upsert } from '../upsert';
+
+import './threads.html';
+
+const LIST_SIZE = parseInt(getConfig('threadsListSize')) || 50;
+
+const sort = { tlm: -1 };
+
+Template.threads.events({
+ 'click .js-open-thread'(e, instance) {
+ const { msg } = messageArgs(this);
+ instance.state.set('mid', msg._id);
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ },
+ 'scroll .js-scroll-threads': _.throttle(({ currentTarget: e }, { incLimit }) => {
+ lazyloadtick();
+ if (e.offsetHeight + e.scrollTop <= e.scrollHeight - 50) {
+ incLimit && incLimit();
+ }
+ }, 500),
+});
+
+Template.threads.helpers({
+ doDotLoadThreads() {
+ return Template.instance().state.get('close');
+ },
+ close() {
+ const { state, data } = Template.instance();
+ const { tabBar } = data;
+ return () => (state.get('close') ? tabBar.close() : state.set('mid', null));
+ },
+ message() {
+ return Template.instance().state.get('thread');
+ },
+ isLoading() {
+ return Template.instance().state.get('loading');
+ },
+ hasNoThreads() {
+ return !Template.instance().state.get('loading') && Template.instance().Threads.find({ rid: Template.instance().state.get('rid') }, { sort }).count() === 0;
+ },
+ threads() {
+ return Template.instance().Threads.find({ rid: Template.instance().state.get('rid') }, { sort, limit: Template.instance().state.get('limit') });
+ },
+ messageContext,
+});
+
+Template.threads.onCreated(async function() {
+ this.Threads = new Mongo.Collection(null);
+ const { rid, mid, msg } = this.data;
+ this.state = new ReactiveDict({
+ rid,
+ close: !!mid,
+ loading: true,
+ mid,
+ thread: msg,
+ });
+
+
+ this.incLimit = () => {
+
+ const { rid, limit } = Tracker.nonreactive(() => this.state.all());
+
+ const count = this.Threads.find({ rid }).count();
+
+ if (limit > count) {
+ return;
+ }
+
+ this.state.set('limit', this.state.get('limit') + LIST_SIZE);
+ this.loadMore();
+ };
+
+ this.loadMore = _.debounce(async () => {
+ const { rid, limit } = Tracker.nonreactive(() => this.state.all());
+ if (this.state.get('loading') === rid) {
+ return;
+ }
+
+
+ this.state.set('loading', rid);
+ const threads = await call('getThreadsList', { rid, limit: LIST_SIZE, skip: limit - LIST_SIZE });
+ upsert(this.Threads, threads);
+ // threads.forEach(({ _id, ...msg }) => this.Threads.upsert({ _id }, msg));
+ this.state.set('loading', false);
+
+ }, 500);
+
+ Tracker.afterFlush(() => {
+ this.autorun(async () => {
+ const { rid, mid } = Template.currentData();
+
+ this.state.set({
+ close: !!mid,
+ mid,
+ rid,
+ });
+ });
+ });
+
+ this.autorun(() => {
+ const rid = this.state.get('rid');
+ this.rid = rid;
+ this.state.set({
+ limit: LIST_SIZE,
+ });
+ this.loadMore();
+ });
+
+ this.autorun(() => {
+ const rid = this.state.get('rid');
+ this.threadsObserve && this.threadsObserve.stop();
+ this.threadsObserve = Messages.find({ rid, _updatedAt: { $gt: new Date() }, tcount: { $exists: true } }).observe({
+ added: ({ _id, ...message }) => {
+ this.Threads.upsert({ _id }, message);
+ }, // Update message to re-render DOM
+ changed: ({ _id, ...message }) => {
+ this.Threads.update({ _id }, message);
+ }, // Update message to re-render DOM
+ removed: ({ _id }) => {
+ this.Threads.remove(_id);
+
+ const { _id: mid } = this.mid.get() || {};
+ if (_id === mid) {
+ this.mid.set(null);
+ }
+ },
+ });
+
+ const alert = 'Unread';
+ this.subscriptionObserve && this.subscriptionObserve.stop();
+ this.subscriptionObserve = Subscriptions.find({ rid }, { fields: { tunread: 1 } }).observeChanges({
+ added: (_id, { tunread }) => {
+ tunread && tunread.length && this.Threads.update({ tmid: { $in: tunread } }, { $set: { alert } }, { multi: true });
+ },
+ changed: (id, { tunread = [] }) => {
+ this.Threads.update({ alert, _id: { $nin: tunread } }, { $unset: { alert: 1 } }, { multi: true });
+ tunread && tunread.length && this.Threads.update({ _id: { $in: tunread } }, { $set: { alert } }, { multi: true });
+ },
+ });
+ });
+
+ this.autorun(async () => {
+ const mid = this.state.get('mid');
+ return this.state.set('thread', mid && (Messages.findOne({ _id: mid }, { fields: { tcount: 0, tlm: 0, replies: 0, _updatedAt: 0 } }) || this.Threads.findOne({ _id: mid }, { fields: { tcount: 0, tlm: 0, replies: 0, _updatedAt: 0 } })));
+ });
+});
+
+Template.threads.onDestroyed(function() {
+ const { Threads, threadsObserve, subscriptionObserve } = this;
+ Threads.remove({});
+ threadsObserve && threadsObserve.stop();
+ subscriptionObserve && subscriptionObserve.stop();
+});
diff --git a/app/threads/client/index.js b/app/threads/client/index.js
new file mode 100644
index 000000000000..5331d52fbc5e
--- /dev/null
+++ b/app/threads/client/index.js
@@ -0,0 +1,8 @@
+import './flextab/threadlist';
+import './flextab/thread';
+import './flextab/threads';
+import './threads.css';
+import './messageAction/follow';
+import './messageAction/unfollow';
+import './messageAction/replyInThread';
+
diff --git a/app/threads/client/messageAction/follow.js b/app/threads/client/messageAction/follow.js
new file mode 100644
index 000000000000..8e4d20ec0755
--- /dev/null
+++ b/app/threads/client/messageAction/follow.js
@@ -0,0 +1,36 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+
+import { Messages } from '../../../models/client';
+import { settings } from '../../../settings/client';
+import { MessageAction, call } from '../../../ui-utils/client';
+import { messageArgs } from '../../../ui-utils/client/lib/messageArgs';
+
+Meteor.startup(function() {
+ Tracker.autorun(() => {
+ if (!settings.get('Threads_enabled')) {
+ return MessageAction.removeButton('follow-message');
+ }
+ MessageAction.addButton({
+ id: 'follow-message',
+ icon: 'bell',
+ label: 'Follow_message',
+ context: ['threads'],
+ async action() {
+ const { msg } = messageArgs(this);
+ call('followMessage', { mid: msg._id });
+ },
+ condition({ tmid, replies = [] }) {
+ if (tmid) {
+ const parentMessage = Messages.findOne({ _id: tmid }, { fields: { replies: 1 } });
+ if (parentMessage) {
+ replies = parentMessage.replies || [];
+ }
+ }
+ return !replies.includes(Meteor.userId());
+ },
+ order: 0,
+ group: 'menu',
+ });
+ });
+});
diff --git a/app/threads/client/messageAction/replyInThread.js b/app/threads/client/messageAction/replyInThread.js
new file mode 100644
index 000000000000..337bd5ba7399
--- /dev/null
+++ b/app/threads/client/messageAction/replyInThread.js
@@ -0,0 +1,41 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+
+import { settings } from '../../../settings/client';
+import { MessageAction } from '../../../ui-utils/client';
+import { messageArgs } from '../../../ui-utils/client/lib/messageArgs';
+import { chatMessages } from '../../../ui/client';
+import { addMessageToList } from '../../../ui-utils/client/lib/MessageAction';
+import { Subscriptions } from '../../../models/client';
+
+Meteor.startup(function() {
+ Tracker.autorun(() => {
+ if (!settings.get('Threads_enabled')) {
+ return MessageAction.removeButton('reply-in-thread');
+ }
+ MessageAction.addButton({
+ id: 'reply-in-thread',
+ icon: 'thread',
+ label: 'Reply_in_thread',
+ context: ['message', 'message-mobile', 'threads'],
+ action() {
+ const { msg: message } = messageArgs(this);
+ const { input } = chatMessages[message.rid];
+ const $input = $(input);
+
+ const messages = addMessageToList($input.data('reply') || [], message, input);
+
+ $(input)
+ .focus()
+ .data('mention-user', true)
+ .data('reply', messages)
+ .trigger('dataChange');
+ },
+ condition(message) {
+ return Boolean(Subscriptions.findOne({ rid: message.rid }));
+ },
+ order: 1,
+ group: 'menu',
+ });
+ });
+});
diff --git a/app/threads/client/messageAction/unfollow.js b/app/threads/client/messageAction/unfollow.js
new file mode 100644
index 000000000000..49dfdd74addf
--- /dev/null
+++ b/app/threads/client/messageAction/unfollow.js
@@ -0,0 +1,36 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+
+import { Messages } from '../../../models/client';
+import { settings } from '../../../settings/client';
+import { MessageAction, call } from '../../../ui-utils/client';
+import { messageArgs } from '../../../ui-utils/client/lib/messageArgs';
+
+Meteor.startup(function() {
+ Tracker.autorun(() => {
+ if (!settings.get('Threads_enabled')) {
+ return MessageAction.removeButton('unfollow-message');
+ }
+ MessageAction.addButton({
+ id: 'unfollow-message',
+ icon: 'bell-off',
+ label: 'Unfollow_message',
+ context: ['threads'],
+ async action() {
+ const { msg } = messageArgs(this);
+ call('unfollowMessage', { mid: msg._id });
+ },
+ condition({ tmid, replies = [] }) {
+ if (tmid) {
+ const parentMessage = Messages.findOne({ _id: tmid }, { fields: { replies: 1 } });
+ if (parentMessage) {
+ replies = parentMessage.replies || [];
+ }
+ }
+ return replies.includes(Meteor.userId());
+ },
+ order: 0,
+ group: 'menu',
+ });
+ });
+});
diff --git a/app/threads/client/threads.css b/app/threads/client/threads.css
new file mode 100644
index 000000000000..165edf58c6b2
--- /dev/null
+++ b/app/threads/client/threads.css
@@ -0,0 +1,163 @@
+.message.thread-main {
+ padding-top: var(--default-padding);
+ padding-bottom: var(--default-padding);
+
+ border-bottom: 1px solid var(--color-gray-light);
+}
+
+.message.sequential[data-tmid] > .thread-replied > .thumb,
+.message.sequential.system[data-tmid] > .thumb {
+ display: block;
+}
+
+.message.sequential[data-tmid] > .message-body-wrapper > .title {
+ display: none;
+}
+
+.message.thread-message {
+ padding-top: 16px;
+ padding-bottom: 8px;
+}
+
+.thread-message + .thread-message {
+ border-top: 1px solid var(--color-gray-light);
+}
+
+.thread-empty {
+ padding: calc(2 * var(--default-padding));
+}
+
+.thread-list {
+ overflow: auto;
+
+ word-wrap: break-word;
+ flex-shrink: 1;
+}
+
+.message {
+ & .thread-replied {
+
+ display: inline-flex;
+
+ display: flex;
+
+ cursor: pointer;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ color: var(--rc-color-link-active);
+
+ align-items: baseline;
+ flex-flow: row nowrap;
+ }
+
+ .thread-icons {
+ display: block;
+ flex: 0 0 20px;
+
+ width: 20px;
+
+ &--thread {
+ position: absolute;
+ left: 40px;
+
+ width: 20px;
+ height: 20px;
+
+ color: var(--rc-color-alert-message-primary);
+ }
+
+ &--bell,
+ &--bell-off {
+ cursor: pointer;
+
+ color: #a0a0a0;
+ }
+ }
+
+ & > .thread-quote {
+
+ display: flex;
+
+ margin: calc((var(--default-padding) /2) - 6px) 0 7px 0;
+ align-items: center;
+ }
+
+ .thread-quote__message {
+
+ display: flex;
+
+ overflow: hidden;
+
+ cursor: pointer;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ color: var(--rc-color-alert-message-primary);
+
+ align-items: center;
+
+ > .message-body--unstyled {
+ overflow: hidden;
+
+ text-overflow: ellipsis;
+ }
+ }
+
+ & .thread-reply-preview {
+ display: none;
+ overflow: hidden;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &.collapsed .thread-reply-preview {
+ display: initial;
+ }
+}
+
+.message.collapsed + .message.system:not(.collapsed) {
+ margin-top: calc(var(--default-padding) / 2);
+ margin-bottom: calc(var(--default-padding) / 2);
+}
+
+.message.collapsed + .message:not(.collapsed) {
+ margin-top: calc(var(--default-padding) / 2);
+}
+
+.message.collapsed > .thread-replied > .thumb {
+ left: 40px;
+
+ width: 20px;
+ height: 20px;
+ margin-left: 0;
+
+ & .avatar {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.message.sequential .thread-quote {
+ display: none;
+}
+
+@media (width < 500px) {
+ .message {
+ & .title {
+ flex-wrap: wrap;
+ }
+
+ & .thread-replied {
+ flex-basis: 100%;
+ }
+ }
+}
+
+.contextual-bar__content.thread,
+.contextual-bar__content.threads {
+ padding: 0;
+}
diff --git a/app/threads/client/upsert.js b/app/threads/client/upsert.js
new file mode 100644
index 000000000000..be5068a94e8f
--- /dev/null
+++ b/app/threads/client/upsert.js
@@ -0,0 +1,10 @@
+export const upsert = (Collection, objects) => {
+ const { queries } = Collection;
+ Collection.queries = [];
+ objects.forEach(({ _id, ...obj }, index) => {
+ if (index === obj.length - 1) {
+ Collection.queries = queries;
+ }
+ Collection.upsert({ _id }, { _id, ...obj });
+ });
+};
diff --git a/app/threads/server/functions.js b/app/threads/server/functions.js
new file mode 100644
index 000000000000..2a8fe49ee51c
--- /dev/null
+++ b/app/threads/server/functions.js
@@ -0,0 +1,49 @@
+import { Messages, Subscriptions } from '../../models/server';
+
+export const reply = ({ tmid }, { rid, ts, u, editedAt }, parentMessage) => {
+ if (!tmid || editedAt) {
+ return false;
+ }
+
+ Messages.updateRepliesByThreadId(tmid, [parentMessage.u._id, u._id], ts);
+
+ const replies = Messages.getThreadFollowsByThreadId(tmid);
+
+ // doesnt need to update the sender (u._id) subscription, so filter it
+ Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, replies.filter((userId) => userId !== u._id), tmid);
+};
+
+export const undoReply = ({ tmid }) => {
+ if (!tmid) {
+ return false;
+ }
+
+ const { ts } = Messages.getFirstReplyTsByThreadId(tmid) || {};
+ if (!ts) {
+ return Messages.unsetThreadByThreadId(tmid);
+ }
+
+ return Messages.updateThreadLastMessageAndCountByThreadId(tmid, ts, -1);
+};
+
+export const follow = ({ tmid, uid }) => {
+ if (!tmid || !uid) {
+ return false;
+ }
+
+ Messages.addThreadFollowerByThreadId(tmid, uid);
+};
+
+export const unfollow = ({ tmid, rid, uid }) => {
+ if (!tmid || !uid) {
+ return false;
+ }
+
+ Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid);
+
+ return Messages.removeThreadFollowerByThreadId(tmid, uid);
+};
+
+export const readThread = ({ userId, rid, tmid }) => Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid);
+
+export const readAllThreads = (rid, userId) => Subscriptions.removeAllUnreadThreadsByRoomIdAndUserId(rid, userId);
diff --git a/app/threads/server/hooks/afterReadMessages.js b/app/threads/server/hooks/afterReadMessages.js
new file mode 100644
index 000000000000..ef01a1d82574
--- /dev/null
+++ b/app/threads/server/hooks/afterReadMessages.js
@@ -0,0 +1,19 @@
+import { Meteor } from 'meteor/meteor';
+
+import { callbacks } from '../../../callbacks/server';
+import { settings } from '../../../settings';
+import { readAllThreads } from '../functions';
+
+const readThreads = (rid, { userId }) => {
+ readAllThreads(rid, userId);
+};
+
+Meteor.startup(function() {
+ settings.get('Threads_enabled', function(key, value) {
+ if (!value) {
+ callbacks.remove('afterReadMessages', 'threads-after-read-messages');
+ return;
+ }
+ callbacks.add('afterReadMessages', readThreads, callbacks.priority.LOW, 'threads-after-read-messages');
+ });
+});
diff --git a/app/threads/server/hooks/afterdeletemessage.js b/app/threads/server/hooks/afterdeletemessage.js
new file mode 100644
index 000000000000..e7d3ac93d037
--- /dev/null
+++ b/app/threads/server/hooks/afterdeletemessage.js
@@ -0,0 +1,33 @@
+import { Meteor } from 'meteor/meteor';
+
+import { callbacks } from '../../../callbacks/server';
+import { settings } from '../../../settings/server';
+import { Messages } from '../../../models/server';
+
+import { undoReply } from '../functions';
+
+Meteor.startup(function() {
+ const fn = function(message) {
+
+ // is a reply from a thread
+ if (message.tmid) {
+ undoReply(message);
+ }
+
+ // is a thread
+ if (message.tcount) {
+ Messages.removeThreadRefByThreadId(message._id);
+ }
+
+ return message;
+ };
+
+ settings.get('Threads_enabled', function(key, value) {
+ if (!value) {
+ callbacks.remove('afterDeleteMessage', 'threads-after-delete-message');
+ return;
+ }
+ callbacks.add('afterDeleteMessage', fn, callbacks.priority.LOW, 'threads-after-delete-message');
+ });
+
+});
diff --git a/app/threads/server/hooks/aftersavemessage.js b/app/threads/server/hooks/aftersavemessage.js
new file mode 100644
index 000000000000..b6c358424554
--- /dev/null
+++ b/app/threads/server/hooks/aftersavemessage.js
@@ -0,0 +1,69 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Messages } from '../../../models/server';
+import { callbacks } from '../../../callbacks/server';
+import { settings } from '../../../settings/server';
+import { reply } from '../functions';
+import { updateUsersSubscriptions } from '../../../lib/server/lib/notifyUsersOnMessage';
+import { sendMessageNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage';
+
+function notifyUsersOnReply(message, replies, room) {
+
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ return message;
+ }
+
+ updateUsersSubscriptions(message, room, replies);
+
+ return message;
+}
+
+const metaData = (message, parentMessage) => {
+ reply({ tmid: message.tmid }, message, parentMessage);
+
+ return message;
+};
+
+const notification = (message, room, replies) => {
+
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ return message;
+ }
+
+ // will send a notification to everyone who replied/followed the thread except the owner of the message
+ sendMessageNotifications(message, room, replies);
+
+ return message;
+};
+
+const processThreads = (message, room) => {
+ if (!message.tmid) {
+ return;
+ }
+
+ const parentMessage = Messages.findOneById(message.tmid);
+ if (!parentMessage) {
+ return;
+ }
+
+ const replies = [
+ parentMessage.u._id,
+ ...(parentMessage.replies || []),
+ ].filter((userId) => userId !== message.u._id);
+
+ notifyUsersOnReply(message, replies, room);
+ metaData(message, parentMessage);
+ notification(message, room, replies);
+};
+
+Meteor.startup(function() {
+ settings.get('Threads_enabled', function(key, value) {
+ if (!value) {
+ callbacks.remove('afterSaveMessage', 'threads-after-save-message');
+ return;
+ }
+ callbacks.add('afterSaveMessage', processThreads, callbacks.priority.LOW, 'threads-after-save-message');
+ });
+});
diff --git a/app/threads/server/hooks/index.js b/app/threads/server/hooks/index.js
new file mode 100644
index 000000000000..8ca16a6257fe
--- /dev/null
+++ b/app/threads/server/hooks/index.js
@@ -0,0 +1,3 @@
+import './afterdeletemessage';
+import './afterReadMessages';
+import './aftersavemessage';
diff --git a/app/threads/server/index.js b/app/threads/server/index.js
new file mode 100644
index 000000000000..a4ea0a9c372c
--- /dev/null
+++ b/app/threads/server/index.js
@@ -0,0 +1,3 @@
+import './hooks';
+import './methods';
+import './settings';
diff --git a/app/threads/server/methods/followMessage.js b/app/threads/server/methods/followMessage.js
new file mode 100644
index 000000000000..f855babeabb7
--- /dev/null
+++ b/app/threads/server/methods/followMessage.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+
+import { Messages } from '../../../models/server';
+import { RateLimiter } from '../../../lib/server';
+import { settings } from '../../../settings/server';
+
+import { follow } from '../functions';
+
+Meteor.methods({
+ 'followMessage'({ mid }) {
+ check(mid, String);
+
+ const uid = Meteor.userId();
+ if (!uid) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'followMessage' });
+ }
+
+ if (mid && !settings.get('Threads_enabled')) {
+ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
+ }
+
+ const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } });
+ if (!message) {
+ throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' });
+ }
+
+ const room = Meteor.call('canAccessRoom', message.rid, uid);
+ if (!room) {
+ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
+ }
+
+ return follow({ tmid: message.tmid || message._id, uid });
+ },
+});
+
+RateLimiter.limitMethod('followMessage', 5, 5000, {
+ userId() { return true; },
+});
diff --git a/app/threads/server/methods/getThreadMessages.js b/app/threads/server/methods/getThreadMessages.js
new file mode 100644
index 000000000000..4fb878254098
--- /dev/null
+++ b/app/threads/server/methods/getThreadMessages.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Messages, Rooms } from '../../../models/server';
+import { canAccessRoom } from '../../../authorization/server';
+import { settings } from '../../../settings/server';
+
+import { readThread } from '../functions';
+
+const MAX_LIMIT = 100;
+
+Meteor.methods({
+ getThreadMessages({ tmid, limit, skip }) {
+ if (limit > MAX_LIMIT) {
+ throw new Meteor.Error('error-not-allowed', `max limit: ${ MAX_LIMIT }`, { method: 'getThreadMessages' });
+ }
+
+ if (!Meteor.userId() || !settings.get('Threads_enabled')) {
+ throw new Meteor.Error('error-not-allowed', 'Threads Disabled', { method: 'getThreadMessages' });
+ }
+
+ const thread = Messages.findOneById(tmid);
+ if (!thread) {
+ return [];
+ }
+
+ const user = Meteor.user();
+ const room = Rooms.findOneById(thread.rid);
+
+ if (!canAccessRoom(room, user)) {
+ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' });
+ }
+
+ readThread({ userId: user._id, rid: thread.rid, tmid });
+
+ const result = Messages.find({ tmid }, { ...(skip && { skip }), ...(limit && { limit }), sort: { ts : -1 } }).fetch();
+
+ return [thread, ...result];
+ },
+});
diff --git a/app/threads/server/methods/getThreadsList.js b/app/threads/server/methods/getThreadsList.js
new file mode 100644
index 000000000000..61f2dd01e9d0
--- /dev/null
+++ b/app/threads/server/methods/getThreadsList.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Messages, Rooms } from '../../../models/server';
+import { canAccessRoom } from '../../../authorization/server';
+import { settings } from '../../../settings/server';
+
+const MAX_LIMIT = 100;
+
+Meteor.methods({
+ getThreadsList({ rid, limit = 50, skip = 0 }) {
+
+ if (limit > MAX_LIMIT) {
+ throw new Meteor.Error('error-not-allowed', `max limit: ${ MAX_LIMIT }`, { method: 'getThreadsList' });
+ }
+
+ if (!Meteor.userId() || !settings.get('Threads_enabled')) {
+ throw new Meteor.Error('error-not-allowed', 'Threads Disabled', { method: 'getThreadsList' });
+ }
+
+ const user = Meteor.user();
+ const room = Rooms.findOneById(rid);
+
+ if (!canAccessRoom(room, user)) {
+ throw new Meteor.Error('error-not-allowed', 'Not Allowed', { method: 'getThreadsList' });
+ }
+
+ return Messages.findThreadsByRoomId(rid, skip, limit).fetch();
+ },
+});
diff --git a/app/threads/server/methods/index.js b/app/threads/server/methods/index.js
new file mode 100644
index 000000000000..4e6ed3003b11
--- /dev/null
+++ b/app/threads/server/methods/index.js
@@ -0,0 +1,4 @@
+import './followMessage';
+import './getThreadMessages';
+import './getThreadsList';
+import './unfollowMessage';
diff --git a/app/threads/server/methods/unfollowMessage.js b/app/threads/server/methods/unfollowMessage.js
new file mode 100644
index 000000000000..4e602747f5a5
--- /dev/null
+++ b/app/threads/server/methods/unfollowMessage.js
@@ -0,0 +1,39 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+
+import { Messages } from '../../../models/server';
+import { RateLimiter } from '../../../lib/server';
+import { settings } from '../../../settings/server';
+
+import { unfollow } from '../functions';
+
+Meteor.methods({
+ 'unfollowMessage'({ mid }) {
+ check(mid, String);
+
+ const uid = Meteor.userId();
+ if (!uid) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unfollowMessage' });
+ }
+
+ if (mid && !settings.get('Threads_enabled')) {
+ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' });
+ }
+
+ const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } });
+ if (!message) {
+ throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' });
+ }
+
+ const room = Meteor.call('canAccessRoom', message.rid, uid);
+ if (!room) {
+ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
+ }
+
+ return unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid });
+ },
+});
+
+RateLimiter.limitMethod('unfollowMessage', 5, 5000, {
+ userId() { return true; },
+});
diff --git a/app/threads/server/settings.js b/app/threads/server/settings.js
new file mode 100644
index 000000000000..644f9a33e88c
--- /dev/null
+++ b/app/threads/server/settings.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import { settings } from '../../settings';
+
+Meteor.startup(() => {
+ settings.addGroup('Threads', function() {
+ this.add('Threads_enabled', true, {
+ group: 'Threads',
+ i18nLabel: 'Enable',
+ type: 'boolean',
+ public: true,
+ });
+ });
+});
diff --git a/packages/rocketchat-token-login/client/index.js b/app/token-login/client/index.js
similarity index 100%
rename from packages/rocketchat-token-login/client/index.js
rename to app/token-login/client/index.js
diff --git a/packages/rocketchat-token-login/client/login_token_client.js b/app/token-login/client/login_token_client.js
similarity index 100%
rename from packages/rocketchat-token-login/client/login_token_client.js
rename to app/token-login/client/login_token_client.js
diff --git a/packages/rocketchat-token-login/server/index.js b/app/token-login/server/index.js
similarity index 100%
rename from packages/rocketchat-token-login/server/index.js
rename to app/token-login/server/index.js
diff --git a/packages/rocketchat-token-login/server/login_token_server.js b/app/token-login/server/login_token_server.js
similarity index 100%
rename from packages/rocketchat-token-login/server/login_token_server.js
rename to app/token-login/server/login_token_server.js
diff --git a/packages/rocketchat-tokenpass/README.md b/app/tokenpass/README.md
similarity index 100%
rename from packages/rocketchat-tokenpass/README.md
rename to app/tokenpass/README.md
diff --git a/packages/rocketchat-tokenpass/client/channelSettings.css b/app/tokenpass/client/channelSettings.css
similarity index 100%
rename from packages/rocketchat-tokenpass/client/channelSettings.css
rename to app/tokenpass/client/channelSettings.css
diff --git a/packages/rocketchat-tokenpass/client/index.js b/app/tokenpass/client/index.js
similarity index 100%
rename from packages/rocketchat-tokenpass/client/index.js
rename to app/tokenpass/client/index.js
diff --git a/packages/rocketchat-tokenpass/client/login-button.css b/app/tokenpass/client/login-button.css
similarity index 100%
rename from packages/rocketchat-tokenpass/client/login-button.css
rename to app/tokenpass/client/login-button.css
diff --git a/app/tokenpass/client/roomType.js b/app/tokenpass/client/roomType.js
new file mode 100644
index 000000000000..c58735e9ecac
--- /dev/null
+++ b/app/tokenpass/client/roomType.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { roomTypes, RoomTypeConfig } from '../../utils';
+
+class TokenPassRoomType extends RoomTypeConfig {
+ constructor() {
+ super({
+ identifier: 'tokenpass',
+ order: 1,
+ });
+
+ this.customTemplate = 'tokenChannelsList';
+ }
+
+ condition() {
+ const user = Meteor.users.findOne(Meteor.userId(), { fields: { 'services.tokenpass': 1 } });
+ const hasTokenpass = !!(user && user.services && user.services.tokenpass);
+
+ return hasTokenpass;
+ }
+}
+
+roomTypes.add(new TokenPassRoomType());
diff --git a/app/tokenpass/client/startup.js b/app/tokenpass/client/startup.js
new file mode 100644
index 000000000000..2167827595c6
--- /dev/null
+++ b/app/tokenpass/client/startup.js
@@ -0,0 +1,20 @@
+import { Meteor } from 'meteor/meteor';
+import { ChannelSettings } from '../../channel-settings';
+import { Rooms } from '../../models';
+
+Meteor.startup(function() {
+ ChannelSettings.addOption({
+ group: ['room'],
+ id: 'tokenpass',
+ template: 'channelSettings__tokenpass',
+ validation(data) {
+ if (data && data.rid) {
+ const room = Rooms.findOne(data.rid, { fields: { tokenpass: 1 } });
+
+ return room && room.tokenpass;
+ }
+
+ return false;
+ },
+ });
+});
diff --git a/packages/rocketchat-tokenpass/client/styles.css b/app/tokenpass/client/styles.css
similarity index 100%
rename from packages/rocketchat-tokenpass/client/styles.css
rename to app/tokenpass/client/styles.css
diff --git a/packages/rocketchat-tokenpass/client/tokenChannelsList.html b/app/tokenpass/client/tokenChannelsList.html
similarity index 100%
rename from packages/rocketchat-tokenpass/client/tokenChannelsList.html
rename to app/tokenpass/client/tokenChannelsList.html
diff --git a/packages/rocketchat-tokenpass/client/tokenChannelsList.js b/app/tokenpass/client/tokenChannelsList.js
similarity index 84%
rename from packages/rocketchat-tokenpass/client/tokenChannelsList.js
rename to app/tokenpass/client/tokenChannelsList.js
index cc4b8a963523..4c9cbf9545c6 100644
--- a/packages/rocketchat-tokenpass/client/tokenChannelsList.js
+++ b/app/tokenpass/client/tokenChannelsList.js
@@ -2,11 +2,11 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Subscriptions } from '../../models';
Template.tokenChannelsList.helpers({
rooms() {
- return Template.instance().tokenpassRooms.get().filter((room) => RocketChat.models.Subscriptions.find({ rid: room._id }).count() === 0);
+ return Template.instance().tokenpassRooms.get().filter((room) => Subscriptions.find({ rid: room._id }).count() === 0);
},
});
diff --git a/packages/rocketchat-tokenpass/client/tokenpassChannelSettings.html b/app/tokenpass/client/tokenpassChannelSettings.html
similarity index 100%
rename from packages/rocketchat-tokenpass/client/tokenpassChannelSettings.html
rename to app/tokenpass/client/tokenpassChannelSettings.html
diff --git a/packages/rocketchat-tokenpass/client/tokenpassChannelSettings.js b/app/tokenpass/client/tokenpassChannelSettings.js
similarity index 95%
rename from packages/rocketchat-tokenpass/client/tokenpassChannelSettings.js
rename to app/tokenpass/client/tokenpassChannelSettings.js
index d8f66006f6f2..da312c3bde79 100644
--- a/packages/rocketchat-tokenpass/client/tokenpassChannelSettings.js
+++ b/app/tokenpass/client/tokenpassChannelSettings.js
@@ -2,9 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
-import { handleError } from 'meteor/rocketchat:lib';
-import { ChatRoom } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { t, handleError } from '../../utils';
+import { ChatRoom } from '../../models';
import toastr from 'toastr';
Template.channelSettings__tokenpass.helpers({
diff --git a/app/tokenpass/lib/common.js b/app/tokenpass/lib/common.js
new file mode 100644
index 000000000000..a1820059f092
--- /dev/null
+++ b/app/tokenpass/lib/common.js
@@ -0,0 +1,40 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { settings } from '../../settings';
+import { CustomOAuth } from '../../custom-oauth';
+
+const config = {
+ serverURL: '',
+ identityPath: '/oauth/user',
+ authorizePath: '/oauth/authorize',
+ tokenPath: '/oauth/access-token',
+ scope: 'user,tca,private-balances',
+ tokenSentVia: 'payload',
+ usernameField: 'username',
+ mergeUsers: true,
+ addAutopublishFields: {
+ forLoggedInUser: ['services.tokenpass'],
+ forOtherUsers: ['services.tokenpass.name'],
+ },
+ accessTokenParam: 'access_token',
+};
+
+const Tokenpass = new CustomOAuth('tokenpass', config);
+
+if (Meteor.isServer) {
+ Meteor.startup(function() {
+ settings.get('API_Tokenpass_URL', function(key, value) {
+ config.serverURL = value;
+ Tokenpass.configure(config);
+ });
+ });
+} else {
+ Meteor.startup(function() {
+ Tracker.autorun(function() {
+ if (settings.get('API_Tokenpass_URL')) {
+ config.serverURL = settings.get('API_Tokenpass_URL');
+ Tokenpass.configure(config);
+ }
+ });
+ });
+}
diff --git a/packages/rocketchat-tokenpass/server/Tokenpass.js b/app/tokenpass/server/Tokenpass.js
similarity index 78%
rename from packages/rocketchat-tokenpass/server/Tokenpass.js
rename to app/tokenpass/server/Tokenpass.js
index b312b8842782..9a257990ba2f 100644
--- a/packages/rocketchat-tokenpass/server/Tokenpass.js
+++ b/app/tokenpass/server/Tokenpass.js
@@ -1,6 +1,4 @@
-import { RocketChat } from 'meteor/rocketchat:lib';
-
-RocketChat.Tokenpass = {
+export const Tokenpass = {
validateAccess(tokenpass, balances) {
const compFunc = tokenpass.require === 'any' ? 'some' : 'every';
return tokenpass.tokens[compFunc]((config) => balances.some((userToken) => config.token === userToken.asset && parseFloat(config.balance) <= parseFloat(userToken.balance)));
diff --git a/app/tokenpass/server/cronRemoveUsers.js b/app/tokenpass/server/cronRemoveUsers.js
new file mode 100644
index 000000000000..7edf7d1db60b
--- /dev/null
+++ b/app/tokenpass/server/cronRemoveUsers.js
@@ -0,0 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+import { Rooms, Subscriptions, Users } from '../../models';
+import { SyncedCron } from 'meteor/littledata:synced-cron';
+import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances';
+import { Tokenpass } from './Tokenpass';
+import { removeUserFromRoom } from '../../lib/server/functions/removeUserFromRoom';
+
+function removeUsersFromTokenChannels() {
+ const rooms = {};
+
+ Rooms.findAllTokenChannels().forEach((room) => {
+ rooms[room._id] = room.tokenpass;
+ });
+
+ const users = {};
+
+ Subscriptions.findByRoomIds(Object.keys(rooms)).forEach((sub) => {
+ if (!users[sub.u._id]) {
+ users[sub.u._id] = [];
+ }
+ users[sub.u._id].push(sub.rid);
+ });
+
+ Object.keys(users).forEach((user) => {
+ const userInfo = Users.findOneById(user);
+
+ if (userInfo && userInfo.services && userInfo.services.tokenpass) {
+ const balances = updateUserTokenpassBalances(userInfo);
+
+ users[user].forEach((roomId) => {
+ const valid = Tokenpass.validateAccess(rooms[roomId], balances);
+
+ if (!valid) {
+ removeUserFromRoom(roomId, userInfo);
+ }
+ });
+ }
+ });
+}
+
+Meteor.startup(function() {
+ Meteor.defer(function() {
+ removeUsersFromTokenChannels();
+
+ SyncedCron.add({
+ name: 'Remove users from Token Channels',
+ schedule: (parser) => parser.cron('0 * * * *'),
+ job: removeUsersFromTokenChannels,
+ });
+ });
+});
diff --git a/app/tokenpass/server/functions/getProtectedTokenpassBalances.js b/app/tokenpass/server/functions/getProtectedTokenpassBalances.js
new file mode 100644
index 000000000000..93c3c3a055b0
--- /dev/null
+++ b/app/tokenpass/server/functions/getProtectedTokenpassBalances.js
@@ -0,0 +1,23 @@
+import { Meteor } from 'meteor/meteor';
+import { HTTP } from 'meteor/http';
+import { settings } from '../../../settings';
+
+let userAgent = 'Meteor';
+if (Meteor.release) { userAgent += `/${ Meteor.release }`; }
+
+export const getProtectedTokenpassBalances = function(accessToken) {
+ try {
+ return HTTP.get(
+ `${ settings.get('API_Tokenpass_URL') }/api/v1/tca/protected/balances`, {
+ headers: {
+ Accept: 'application/json',
+ 'User-Agent': userAgent,
+ },
+ params: {
+ oauth_token: accessToken,
+ },
+ }).data;
+ } catch (error) {
+ throw new Error(`Failed to fetch protected tokenpass balances from Tokenpass. ${ error.message }`);
+ }
+};
diff --git a/app/tokenpass/server/functions/getPublicTokenpassBalances.js b/app/tokenpass/server/functions/getPublicTokenpassBalances.js
new file mode 100644
index 000000000000..d3b3a94d032f
--- /dev/null
+++ b/app/tokenpass/server/functions/getPublicTokenpassBalances.js
@@ -0,0 +1,23 @@
+import { Meteor } from 'meteor/meteor';
+import { HTTP } from 'meteor/http';
+import { settings } from '../../../settings';
+
+let userAgent = 'Meteor';
+if (Meteor.release) { userAgent += `/${ Meteor.release }`; }
+
+export const getPublicTokenpassBalances = function(accessToken) {
+ try {
+ return HTTP.get(
+ `${ settings.get('API_Tokenpass_URL') }/api/v1/tca/public/balances`, {
+ headers: {
+ Accept: 'application/json',
+ 'User-Agent': userAgent,
+ },
+ params: {
+ oauth_token: accessToken,
+ },
+ }).data;
+ } catch (error) {
+ throw new Error(`Failed to fetch public tokenpass balances from Tokenpass. ${ error.message }`);
+ }
+};
diff --git a/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js b/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js
new file mode 100644
index 000000000000..c0d31a2d7fa8
--- /dev/null
+++ b/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+import { Match } from 'meteor/check';
+import { Rooms } from '../../../models';
+import s from 'underscore.string';
+
+export const saveRoomTokensMinimumBalance = function(rid, roomTokensMinimumBalance) {
+ if (!Match.test(rid, String)) {
+ throw new Meteor.Error('invalid-room', 'Invalid room', {
+ function: 'RocketChat.saveRoomTokensMinimumBalance',
+ });
+ }
+
+ const minimumTokenBalance = parseFloat(s.escapeHTML(roomTokensMinimumBalance));
+
+ return Rooms.setMinimumTokenBalanceById(rid, minimumTokenBalance);
+};
diff --git a/app/tokenpass/server/functions/updateUserTokenpassBalances.js b/app/tokenpass/server/functions/updateUserTokenpassBalances.js
new file mode 100644
index 000000000000..9fe7972e8529
--- /dev/null
+++ b/app/tokenpass/server/functions/updateUserTokenpassBalances.js
@@ -0,0 +1,17 @@
+import { Users } from '../../../models';
+import { getPublicTokenpassBalances } from './getPublicTokenpassBalances';
+import { getProtectedTokenpassBalances } from './getProtectedTokenpassBalances';
+import _ from 'underscore';
+
+export const updateUserTokenpassBalances = function(user) {
+ if (user && user.services && user.services.tokenpass) {
+ const tcaPublicBalances = getPublicTokenpassBalances(user.services.tokenpass.accessToken);
+ const tcaProtectedBalances = getProtectedTokenpassBalances(user.services.tokenpass.accessToken);
+
+ const balances = _.uniq(_.union(tcaPublicBalances, tcaProtectedBalances), false, (item) => item.asset);
+
+ Users.setTokenpassTcaBalances(user._id, balances);
+
+ return balances;
+ }
+};
diff --git a/app/tokenpass/server/index.js b/app/tokenpass/server/index.js
new file mode 100644
index 000000000000..98577e23a026
--- /dev/null
+++ b/app/tokenpass/server/index.js
@@ -0,0 +1,10 @@
+import '../lib/common';
+import './startup';
+import './functions/getProtectedTokenpassBalances';
+import './functions/getPublicTokenpassBalances';
+import './functions/saveRoomTokensMinimumBalance';
+export { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances';
+import './methods/findTokenChannels';
+import './methods/getChannelTokenpass';
+import './cronRemoveUsers';
+export { Tokenpass } from './Tokenpass';
diff --git a/app/tokenpass/server/methods/findTokenChannels.js b/app/tokenpass/server/methods/findTokenChannels.js
new file mode 100644
index 000000000000..93039a49c420
--- /dev/null
+++ b/app/tokenpass/server/methods/findTokenChannels.js
@@ -0,0 +1,25 @@
+import { Meteor } from 'meteor/meteor';
+import { Rooms } from '../../../models';
+import { Tokenpass } from '../Tokenpass';
+
+Meteor.methods({
+ findTokenChannels() {
+ if (!Meteor.userId()) {
+ return [];
+ }
+
+ const user = Meteor.user();
+
+ if (user.services && user.services.tokenpass && user.services.tokenpass.tcaBalances) {
+ const tokens = {};
+ user.services.tokenpass.tcaBalances.forEach((token) => {
+ tokens[token.asset] = 1;
+ });
+
+ return Rooms.findByTokenpass(Object.keys(tokens))
+ .filter((room) => Tokenpass.validateAccess(room.tokenpass, user.services.tokenpass.tcaBalances));
+ }
+
+ return [];
+ },
+});
diff --git a/packages/rocketchat-tokenpass/server/methods/getChannelTokenpass.js b/app/tokenpass/server/methods/getChannelTokenpass.js
similarity index 79%
rename from packages/rocketchat-tokenpass/server/methods/getChannelTokenpass.js
rename to app/tokenpass/server/methods/getChannelTokenpass.js
index 1dad36c89122..bd446bfb80f8 100644
--- a/packages/rocketchat-tokenpass/server/methods/getChannelTokenpass.js
+++ b/app/tokenpass/server/methods/getChannelTokenpass.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
-import { RocketChat } from 'meteor/rocketchat:lib';
+import { Rooms } from '../../../models';
Meteor.methods({
getChannelTokenpass(rid) {
@@ -10,7 +10,7 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelTokenpass' });
}
- const room = RocketChat.models.Rooms.findOneById(rid);
+ const room = Rooms.findOneById(rid);
if (!room) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getChannelTokenpass' });
diff --git a/app/tokenpass/server/startup.js b/app/tokenpass/server/startup.js
new file mode 100644
index 000000000000..6d5384f71ce4
--- /dev/null
+++ b/app/tokenpass/server/startup.js
@@ -0,0 +1,57 @@
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+import { settings } from '../../settings';
+import { addRoomAccessValidator } from '../../authorization';
+import { Users } from '../../models';
+import { callbacks } from '../../callbacks';
+import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances';
+import { Tokenpass } from './Tokenpass';
+
+settings.addGroup('OAuth', function() {
+ this.section('Tokenpass', function() {
+ const enableQuery = {
+ _id: 'Accounts_OAuth_Tokenpass',
+ value: true,
+ };
+
+ this.add('Accounts_OAuth_Tokenpass', false, { type: 'boolean' });
+ this.add('API_Tokenpass_URL', '', { type: 'string', public: true, enableQuery, i18nDescription: 'API_Tokenpass_URL_Description' });
+ this.add('Accounts_OAuth_Tokenpass_id', '', { type: 'string', enableQuery });
+ this.add('Accounts_OAuth_Tokenpass_secret', '', { type: 'string', enableQuery });
+ this.add('Accounts_OAuth_Tokenpass_callback_url', '_oauth/tokenpass', { type: 'relativeUrl', readonly: true, force: true, enableQuery });
+ });
+});
+
+function validateTokenAccess(userData, roomData) {
+ if (!userData || !userData.services || !userData.services.tokenpass || !userData.services.tokenpass.tcaBalances) {
+ return false;
+ }
+
+ return Tokenpass.validateAccess(roomData.tokenpass, userData.services.tokenpass.tcaBalances);
+}
+
+Meteor.startup(function() {
+ addRoomAccessValidator(function(room, user) {
+ if (!room || !room.tokenpass || !user) {
+ return false;
+ }
+
+ const userData = Users.getTokenBalancesByUserId(user._id);
+
+ return validateTokenAccess(userData, room);
+ });
+
+ callbacks.add('beforeJoinRoom', function(user, room) {
+ if (room.tokenpass && !validateTokenAccess(user, room)) {
+ throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' });
+ }
+
+ return room;
+ });
+});
+
+Accounts.onLogin(function({ user }) {
+ if (user && user.services && user.services.tokenpass) {
+ updateUserTokenpassBalances(user);
+ }
+});
diff --git a/packages/rocketchat-tooltip/README.md b/app/tooltip/README.md
similarity index 100%
rename from packages/rocketchat-tooltip/README.md
rename to app/tooltip/README.md
diff --git a/packages/rocketchat-tooltip/client/index.js b/app/tooltip/client/index.js
similarity index 100%
rename from packages/rocketchat-tooltip/client/index.js
rename to app/tooltip/client/index.js
diff --git a/packages/rocketchat-tooltip/client/rocketchat-tooltip.html b/app/tooltip/client/rocketchat-tooltip.html
similarity index 100%
rename from packages/rocketchat-tooltip/client/rocketchat-tooltip.html
rename to app/tooltip/client/rocketchat-tooltip.html
diff --git a/packages/rocketchat-tooltip/client/rocketchat-tooltip.js b/app/tooltip/client/rocketchat-tooltip.js
similarity index 100%
rename from packages/rocketchat-tooltip/client/rocketchat-tooltip.js
rename to app/tooltip/client/rocketchat-tooltip.js
diff --git a/packages/rocketchat-tooltip/client/tooltip.css b/app/tooltip/client/tooltip.css
similarity index 100%
rename from packages/rocketchat-tooltip/client/tooltip.css
rename to app/tooltip/client/tooltip.css
diff --git a/app/tooltip/index.js b/app/tooltip/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/tooltip/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/packages/rocketchat-ui-account/README.md b/app/ui-account/README.md
similarity index 100%
rename from packages/rocketchat-ui-account/README.md
rename to app/ui-account/README.md
diff --git a/packages/rocketchat-ui-account/client/account.html b/app/ui-account/client/account.html
similarity index 100%
rename from packages/rocketchat-ui-account/client/account.html
rename to app/ui-account/client/account.html
diff --git a/packages/rocketchat-ui-account/client/account.js b/app/ui-account/client/account.js
similarity index 82%
rename from packages/rocketchat-ui-account/client/account.js
rename to app/ui-account/client/account.js
index 5f95f6e508fd..004a483693ba 100644
--- a/packages/rocketchat-ui-account/client/account.js
+++ b/app/ui-account/client/account.js
@@ -1,6 +1,6 @@
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
-import { SideNav } from 'meteor/rocketchat:ui';
+import { SideNav } from '../../ui-utils';
Template.account.onRendered(function() {
Tracker.afterFlush(function() {
diff --git a/packages/rocketchat-ui-account/client/accountFlex.html b/app/ui-account/client/accountFlex.html
similarity index 100%
rename from packages/rocketchat-ui-account/client/accountFlex.html
rename to app/ui-account/client/accountFlex.html
diff --git a/app/ui-account/client/accountFlex.js b/app/ui-account/client/accountFlex.js
new file mode 100644
index 000000000000..14dfd8fdfb27
--- /dev/null
+++ b/app/ui-account/client/accountFlex.js
@@ -0,0 +1,42 @@
+import { Template } from 'meteor/templating';
+import { settings } from '../../settings';
+import { hasAllPermission } from '../../authorization';
+import { SideNav, Layout } from '../../ui-utils';
+import { t } from '../../utils';
+
+Template.accountFlex.events({
+ 'click [data-action="close"]'() {
+ SideNav.closeFlex();
+ },
+});
+
+// Template.accountFlex.onRendered(function() {
+// $(this.find('.rooms-list')).perfectScrollbar();
+// });
+
+Template.accountFlex.helpers({
+ allowUserProfileChange() {
+ return settings.get('Accounts_AllowUserProfileChange');
+ },
+ accessTokensEnabled() {
+ return hasAllPermission(['create-personal-access-tokens']);
+ },
+ encryptionEnabled() {
+ return settings.get('E2E_Enable');
+ },
+ webdavIntegrationEnabled() {
+ return settings.get('Webdav_Integration_Enabled');
+ },
+ menuItem(name, icon, section, group) {
+ return {
+ name: t(name),
+ icon,
+ pathSection: section,
+ pathGroup: group,
+ darken: true,
+ };
+ },
+ embeddedVersion() {
+ return Layout.isEmbedded();
+ },
+});
diff --git a/packages/rocketchat-ui-account/client/accountIntegrations.html b/app/ui-account/client/accountIntegrations.html
similarity index 100%
rename from packages/rocketchat-ui-account/client/accountIntegrations.html
rename to app/ui-account/client/accountIntegrations.html
diff --git a/packages/rocketchat-ui-account/client/accountIntegrations.js b/app/ui-account/client/accountIntegrations.js
similarity index 81%
rename from packages/rocketchat-ui-account/client/accountIntegrations.js
rename to app/ui-account/client/accountIntegrations.js
index 803c84b20d48..c610e04b809e 100644
--- a/packages/rocketchat-ui-account/client/accountIntegrations.js
+++ b/app/ui-account/client/accountIntegrations.js
@@ -1,13 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import { modal } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { WebdavAccounts } from '../../models';
+import { modal } from '../../ui-utils';
+import { t } from '../../utils';
import toastr from 'toastr';
Template.accountIntegrations.helpers({
webdavAccounts() {
- return RocketChat.models.WebdavAccounts.find().fetch();
+ return WebdavAccounts.find().fetch();
},
getOptionValue(account) {
return account.name || `${ account.username }@${ account.server_url.replace(/^https?\:\/\//i, '') }`;
diff --git a/packages/rocketchat-ui-account/client/accountPreferences.html b/app/ui-account/client/accountPreferences.html
similarity index 97%
rename from packages/rocketchat-ui-account/client/accountPreferences.html
rename to app/ui-account/client/accountPreferences.html
index 887506f27f56..76e95a26c038 100644
--- a/packages/rocketchat-ui-account/client/accountPreferences.html
+++ b/app/ui-account/client/accountPreferences.html
@@ -270,6 +270,14 @@ {{_ "Sidebar"}}
{{_ "False"}}
+
+
diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/app/ui-account/client/accountPreferences.js
similarity index 80%
rename from packages/rocketchat-ui-account/client/accountPreferences.js
rename to app/ui-account/client/accountPreferences.js
index 334356280544..835f11d19dbd 100644
--- a/packages/rocketchat-ui-account/client/accountPreferences.js
+++ b/app/ui-account/client/accountPreferences.js
@@ -4,9 +4,11 @@ import { Tracker } from 'meteor/tracker';
import { Reload } from 'meteor/reload';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
-import { RocketChat, handleError } from 'meteor/rocketchat:lib';
-import { modal, SideNav, KonchatNotification } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { t, handleError, getUserPreference } from '../../utils';
+import { modal, SideNav } from '../../ui-utils';
+import { KonchatNotification } from '../../ui';
+import { settings } from '../../settings';
+import { CustomSounds } from '../../custom-sounds/client';
import _ from 'underscore';
import s from 'underscore.string';
import toastr from 'toastr';
@@ -26,21 +28,21 @@ function checkedSelected(property, value, defaultValue = undefined) {
if (defaultValue && defaultValue.hash) {
defaultValue = undefined;
}
- return RocketChat.getUserPreference(Meteor.userId(), property, defaultValue) === value;
+ return getUserPreference(Meteor.userId(), property, defaultValue) === value;
}
Template.accountPreferences.helpers({
audioAssets() {
- return (RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList()) || [];
+ return (CustomSounds && CustomSounds.getList && CustomSounds.getList()) || [];
},
newMessageNotification() {
- return RocketChat.getUserPreference(Meteor.userId(), 'newMessageNotification');
+ return getUserPreference(Meteor.userId(), 'newMessageNotification');
},
newRoomNotification() {
- return RocketChat.getUserPreference(Meteor.userId(), 'newRoomNotification');
+ return getUserPreference(Meteor.userId(), 'newRoomNotification');
},
muteFocusedConversations() {
- return RocketChat.getUserPreference(Meteor.userId(), 'muteFocusedConversations');
+ return getUserPreference(Meteor.userId(), 'muteFocusedConversations');
},
languages() {
const languages = TAPi18n.getLanguages();
@@ -68,7 +70,7 @@ Template.accountPreferences.helpers({
return checkedSelected(property, value, defaultValue);
},
highlights() {
- const userHighlights = RocketChat.getUserPreference(Meteor.userId(), 'highlights');
+ const userHighlights = getUserPreference(Meteor.userId(), 'highlights');
return userHighlights ? userHighlights.join(',\n') : undefined;
},
desktopNotificationEnabled() {
@@ -78,38 +80,38 @@ Template.accountPreferences.helpers({
return KonchatNotification.notificationStatus.get() === 'denied' || (window.Notification && Notification.permission === 'denied');
},
desktopNotificationDuration() {
- const userPref = RocketChat.getUserPreference(Meteor.userId(), 'desktopNotificationDuration', 'undefined');
+ const userPref = getUserPreference(Meteor.userId(), 'desktopNotificationDuration', 'undefined');
return userPref !== 'undefined' ? userPref : undefined;
},
defaultDesktopNotificationDuration() {
- return RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotificationDuration');
+ return settings.get('Accounts_Default_User_Preferences_desktopNotificationDuration');
},
idleTimeLimit() {
- return RocketChat.getUserPreference(Meteor.userId(), 'idleTimeLimit');
+ return getUserPreference(Meteor.userId(), 'idleTimeLimit');
},
defaultIdleTimeLimit() {
- return RocketChat.settings.get('Accounts_Default_User_Preferences_idleTimeLimit');
+ return settings.get('Accounts_Default_User_Preferences_idleTimeLimit');
},
defaultDesktopNotification() {
- return notificationLabels[RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotifications')];
+ return notificationLabels[settings.get('Accounts_Default_User_Preferences_desktopNotifications')];
},
defaultMobileNotification() {
- return notificationLabels[RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications')];
+ return notificationLabels[settings.get('Accounts_Default_User_Preferences_mobileNotifications')];
},
defaultEmailNotification() {
- return emailLabels[RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode')];
+ return emailLabels[settings.get('Accounts_Default_User_Preferences_emailNotificationMode')];
},
showRoles() {
- return RocketChat.settings.get('UI_DisplayRoles');
+ return settings.get('UI_DisplayRoles');
},
userDataDownloadEnabled() {
- return RocketChat.settings.get('UserData_EnableDownload') !== false;
+ return settings.get('UserData_EnableDownload') !== false;
},
notificationsSoundVolume() {
- return RocketChat.getUserPreference(Meteor.userId(), 'notificationsSoundVolume');
+ return getUserPreference(Meteor.userId(), 'notificationsSoundVolume');
},
dontAskAgainList() {
- return RocketChat.getUserPreference(Meteor.userId(), 'dontAskAgainList');
+ return getUserPreference(Meteor.userId(), 'dontAskAgainList');
},
});
@@ -122,7 +124,7 @@ Template.accountPreferences.onCreated(function() {
settingsTemplate.child.push(this);
- this.useEmojis = new ReactiveVar(RocketChat.getUserPreference(Meteor.userId(), 'useEmojis'));
+ this.useEmojis = new ReactiveVar(getUserPreference(Meteor.userId(), 'useEmojis'));
let instance = this;
@@ -161,10 +163,11 @@ Template.accountPreferences.onCreated(function() {
data.sendOnEnter = $('#sendOnEnter').find('select').val();
data.autoImageLoad = JSON.parse($('input[name=autoImageLoad]:checked').val());
data.emailNotificationMode = $('select[name=emailNotificationMode]').val();
- data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val() === '' ? RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotificationDuration') : parseInt($('input[name=desktopNotificationDuration]').val());
+ data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val() === '' ? settings.get('Accounts_Default_User_Preferences_desktopNotificationDuration') : parseInt($('input[name=desktopNotificationDuration]').val());
data.desktopNotifications = $('#desktopNotifications').find('select').val();
data.mobileNotifications = $('#mobileNotifications').find('select').val();
data.unreadAlert = JSON.parse($('#unreadAlert').find('input:checked').val());
+ data.sidebarShowDiscussion = JSON.parse($('#sidebarShowDiscussion').find('input:checked').val());
data.notificationsSoundVolume = parseInt($('#notificationsSoundVolume').val());
data.roomCounterSidebar = JSON.parse($('#roomCounterSidebar').find('input:checked').val());
data.highlights = _.compact(_.map($('[name=highlights]').val().split(/,|\n/), function(e) {
@@ -174,12 +177,12 @@ Template.accountPreferences.onCreated(function() {
let reload = false;
- if (RocketChat.settings.get('UI_DisplayRoles')) {
+ if (settings.get('UI_DisplayRoles')) {
data.hideRoles = JSON.parse($('#hideRoles').find('input:checked').val());
}
// if highlights changed we need page reload
- const highlights = RocketChat.getUserPreference(Meteor.userId(), 'highlights');
+ const highlights = getUserPreference(Meteor.userId(), 'highlights');
if (highlights && highlights.join('\n') !== data.highlights.join('\n')) {
reload = true;
}
@@ -198,7 +201,7 @@ Template.accountPreferences.onCreated(function() {
reload = true;
}
- const idleTimeLimit = $('input[name=idleTimeLimit]').val() === '' ? RocketChat.settings.get('Accounts_Default_User_Preferences_idleTimeLimit') : parseInt($('input[name=idleTimeLimit]').val());
+ const idleTimeLimit = $('input[name=idleTimeLimit]').val() === '' ? settings.get('Accounts_Default_User_Preferences_idleTimeLimit') : parseInt($('input[name=idleTimeLimit]').val());
data.idleTimeLimit = idleTimeLimit;
if (this.shouldUpdateLocalStorageSetting('idleTimeLimit', idleTimeLimit)) {
localStorage.setItem('idleTimeLimit', idleTimeLimit);
diff --git a/packages/rocketchat-ui-account/client/accountProfile.html b/app/ui-account/client/accountProfile.html
similarity index 99%
rename from packages/rocketchat-ui-account/client/accountProfile.html
rename to app/ui-account/client/accountProfile.html
index a60e73e9df44..c3e3e5a40a57 100644
--- a/packages/rocketchat-ui-account/client/accountProfile.html
+++ b/app/ui-account/client/accountProfile.html
@@ -118,7 +118,7 @@
{{#unless emailVerified}}
- {{> icon block="rc-input__icon-svg" icon="cross-circled"}}
+ {{> icon block="rc-input__icon-svg" icon="circle-cross"}}
{{else}}
diff --git a/packages/rocketchat-ui-account/client/accountProfile.js b/app/ui-account/client/accountProfile.js
similarity index 89%
rename from packages/rocketchat-ui-account/client/accountProfile.js
rename to app/ui-account/client/accountProfile.js
index ddb1ea964b3c..d6ee828ea621 100644
--- a/packages/rocketchat-ui-account/client/accountProfile.js
+++ b/app/ui-account/client/accountProfile.js
@@ -4,16 +4,18 @@ import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { RocketChat, handleError } from 'meteor/rocketchat:lib';
-import { modal, SideNav } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
+import { modal, SideNav } from '../../ui-utils';
+import { t, handleError } from '../../utils';
+import { settings } from '../../settings';
+import { Notifications } from '../../notifications';
+import { callbacks } from '../../callbacks';
import _ from 'underscore';
import s from 'underscore.string';
import toastr from 'toastr';
const validateEmail = (email) => /^(([^<>()\[\]\\.,;:\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,}))$/.test(email);
const validateUsername = (username) => {
- const reg = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`);
+ const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return reg.test(username);
};
const validateName = (name) => name && name.length;
@@ -26,7 +28,7 @@ const validatePassword = (password, confirmationPassword) => {
};
const filterNames = (old) => {
- const reg = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`);
+ const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return [...old.replace(' ', '')].filter((f) => reg.test(f)).join('');
};
const filterEmail = (old) => old.replace(' ', '');
@@ -83,7 +85,7 @@ Template.accountProfile.helpers({
return Object.keys(suggestions.avatars).map((service) => ({
name: service,
// TODO: improve this fix
- service: !suggestions.avatars[service.toLowerCase()] ? RocketChat.settings.get(`Accounts_OAuth_${ s.capitalize(service.toLowerCase()) }`) : false,
+ service: !suggestions.avatars[service.toLowerCase()] ? settings.get(`Accounts_OAuth_${ s.capitalize(service.toLowerCase()) }`) : false,
suggestion: suggestions.avatars[service.toLowerCase()],
}))
.filter(({ service, suggestion }) => service || suggestion);
@@ -133,7 +135,7 @@ Template.accountProfile.helpers({
return;
},
allowDeleteOwnAccount() {
- return RocketChat.settings.get('Accounts_AllowDeleteOwnAccount');
+ return settings.get('Accounts_AllowDeleteOwnAccount');
},
realname() {
return Meteor.user().name;
@@ -150,23 +152,23 @@ Template.accountProfile.helpers({
return isUserEmailVerified(user);
},
allowRealNameChange() {
- return RocketChat.settings.get('Accounts_AllowRealNameChange');
+ return settings.get('Accounts_AllowRealNameChange');
},
allowUsernameChange() {
- return RocketChat.settings.get('Accounts_AllowUsernameChange') && RocketChat.settings.get('LDAP_Enable') !== true;
+ return settings.get('Accounts_AllowUsernameChange') && settings.get('LDAP_Enable') !== true;
},
allowEmailChange() {
- return RocketChat.settings.get('Accounts_AllowEmailChange');
+ return settings.get('Accounts_AllowEmailChange');
},
allowPasswordChange() {
- return RocketChat.settings.get('Accounts_AllowPasswordChange');
+ return settings.get('Accounts_AllowPasswordChange');
},
canConfirmNewPassword() {
const password = Template.instance().password.get();
- return RocketChat.settings.get('Accounts_AllowPasswordChange') && password && password !== '';
+ return settings.get('Accounts_AllowPasswordChange') && password && password !== '';
},
allowAvatarChange() {
- return RocketChat.settings.get('Accounts_AllowUserAvatarChange');
+ return settings.get('Accounts_AllowUserAvatarChange');
},
customFields() {
return Meteor.user().customFields;
@@ -187,7 +189,7 @@ Template.accountProfile.onCreated(function() {
self.url = new ReactiveVar('');
self.usernameAvaliable = new ReactiveVar(true);
- RocketChat.Notifications.onLogged('updateAvatar', () => self.avatar.set());
+ Notifications.onLogged('updateAvatar', () => self.avatar.set());
self.getSuggestions = function() {
self.suggestions.set(undefined);
Meteor.call('getAvatarSuggestion', function(error, avatars) {
@@ -207,7 +209,7 @@ Template.accountProfile.onCreated(function() {
const instance = this;
if (!newPassword) {
return callback();
- } else if (!RocketChat.settings.get('Accounts_AllowPasswordChange')) {
+ } else if (!settings.get('Accounts_AllowPasswordChange')) {
toastr.remove();
toastr.error(t('Password_Change_Disabled'));
instance.clearForm();
@@ -229,7 +231,7 @@ Template.accountProfile.onCreated(function() {
}));
} else {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', avatar.service);
+ callbacks.run('userAvatarSet', avatar.service);
}
});
}
@@ -239,11 +241,11 @@ Template.accountProfile.onCreated(function() {
if (typedPassword) {
data.typedPassword = typedPassword;
}
- if (s.trim(self.password.get()) && RocketChat.settings.get('Accounts_AllowPasswordChange')) {
+ if (s.trim(self.password.get()) && settings.get('Accounts_AllowPasswordChange')) {
data.newPassword = self.password.get();
}
if (s.trim(self.realname.get()) !== user.name) {
- if (!RocketChat.settings.get('Accounts_AllowRealNameChange')) {
+ if (!settings.get('Accounts_AllowRealNameChange')) {
toastr.remove();
toastr.error(t('RealName_Change_Disabled'));
instance.clearForm();
@@ -253,7 +255,7 @@ Template.accountProfile.onCreated(function() {
}
}
if (s.trim(self.username.get()) !== user.username) {
- if (!RocketChat.settings.get('Accounts_AllowUsernameChange')) {
+ if (!settings.get('Accounts_AllowUsernameChange')) {
toastr.remove();
toastr.error(t('Username_Change_Disabled'));
instance.clearForm();
@@ -263,7 +265,7 @@ Template.accountProfile.onCreated(function() {
}
}
if (s.trim(self.email.get()) !== getUserEmailAddress(user)) {
- if (!RocketChat.settings.get('Accounts_AllowEmailChange')) {
+ if (!settings.get('Accounts_AllowEmailChange')) {
toastr.remove();
toastr.error(t('Email_Change_Disabled'));
instance.clearForm();
@@ -299,7 +301,7 @@ Template.accountProfile.onCreated(function() {
Template.accountProfile.onRendered(function() {
Tracker.afterFlush(() => {
- if (!RocketChat.settings.get('Accounts_AllowUserProfileChange')) {
+ if (!settings.get('Accounts_AllowUserProfileChange')) {
FlowRouter.go('home');
}
this.clearForm();
@@ -326,7 +328,7 @@ Template.accountProfile.events({
}));
} else {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', 'initials');
+ callbacks.run('userAvatarSet', 'initials');
}
});
},
@@ -527,7 +529,7 @@ Template.accountProfile.events({
contentType: blob.type,
blob: reader.result,
});
- RocketChat.callbacks.run('userAvatarSet', 'upload');
+ callbacks.run('userAvatarSet', 'upload');
};
});
},
diff --git a/packages/rocketchat-ui-account/client/avatar/avatar.html b/app/ui-account/client/avatar/avatar.html
similarity index 100%
rename from packages/rocketchat-ui-account/client/avatar/avatar.html
rename to app/ui-account/client/avatar/avatar.html
diff --git a/app/ui-account/client/avatar/avatar.js b/app/ui-account/client/avatar/avatar.js
new file mode 100644
index 000000000000..b16e4e71f560
--- /dev/null
+++ b/app/ui-account/client/avatar/avatar.js
@@ -0,0 +1,30 @@
+import { Meteor } from 'meteor/meteor';
+import { Session } from 'meteor/session';
+import { Template } from 'meteor/templating';
+import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
+
+Template.avatar.helpers({
+ src() {
+ const { url } = Template.instance().data;
+ if (url) {
+ return url;
+ }
+
+ let { username } = this;
+ if (username == null && this.userId != null) {
+ const user = Meteor.users.findOne(this.userId);
+ username = user && user.username;
+ }
+ if (!username) {
+ return;
+ }
+
+ Session.get(`avatar_random_${ username }`);
+
+ if (this.roomIcon) {
+ username = `@${ username }`;
+ }
+
+ return getUserAvatarURL(username);
+ },
+});
diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.html b/app/ui-account/client/avatar/prompt.html
similarity index 100%
rename from packages/rocketchat-ui-account/client/avatar/prompt.html
rename to app/ui-account/client/avatar/prompt.html
diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/app/ui-account/client/avatar/prompt.js
similarity index 86%
rename from packages/rocketchat-ui-account/client/avatar/prompt.js
rename to app/ui-account/client/avatar/prompt.js
index db53a5a25a11..664253c818f4 100644
--- a/packages/rocketchat-ui-account/client/avatar/prompt.js
+++ b/app/ui-account/client/avatar/prompt.js
@@ -3,13 +3,14 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { RocketChat } from 'meteor/rocketchat:lib';
-import { SideNav } from 'meteor/rocketchat:ui';
-import { t } from 'meteor/rocketchat:utils';
-import { fileUploadHandler } from 'meteor/rocketchat:file-upload';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { SideNav } from '../../../ui-utils';
+import { t } from '../../../utils';
+import { mime } from '../../../utils/lib/mimeTypes';
+import { fileUploadHandler } from '../../../file-upload';
import s from 'underscore.string';
import toastr from 'toastr';
-import mime from 'mime-type/with-db';
Template.avatarPrompt.onCreated(function() {
const self = this;
@@ -26,7 +27,7 @@ Template.avatarPrompt.onCreated(function() {
Template.avatarPrompt.onRendered(function() {
Tracker.afterFlush(function() {
- if (!RocketChat.settings.get('Accounts_AllowUserAvatarChange')) {
+ if (!settings.get('Accounts_AllowUserAvatarChange')) {
FlowRouter.go('home');
}
SideNav.setFlex('accountFlex');
@@ -40,7 +41,7 @@ Template.avatarPrompt.helpers({
},
suggestAvatar(service) {
const suggestions = Template.instance().suggestions.get();
- return RocketChat.settings.get(`Accounts_OAuth_${ s.capitalize(service) }`) && !suggestions.avatars[service];
+ return settings.get(`Accounts_OAuth_${ s.capitalize(service) }`) && !suggestions.avatars[service];
},
upload() {
return Template.instance().upload.get();
@@ -65,7 +66,7 @@ Template.avatarPrompt.events({
}));
} else {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', 'initials');
+ callbacks.run('userAvatarSet', 'initials');
}
});
} else if (this.service === 'url') {
@@ -81,7 +82,7 @@ Template.avatarPrompt.events({
}
} else {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', 'url');
+ callbacks.run('userAvatarSet', 'url');
}
});
} else {
@@ -113,7 +114,7 @@ Template.avatarPrompt.events({
upload.start((error, result) => {
if (result) {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', this.service);
+ callbacks.run('userAvatarSet', this.service);
}
});
} else {
@@ -125,7 +126,7 @@ Template.avatarPrompt.events({
}));
} else {
toastr.success(t('Avatar_changed_successfully'));
- RocketChat.callbacks.run('userAvatarSet', tmpService);
+ callbacks.run('userAvatarSet', tmpService);
}
});
}
@@ -163,7 +164,7 @@ Template.avatarPrompt.events({
contentType: blob.type,
blob: reader.result,
});
- RocketChat.callbacks.run('userAvatarSet', 'upload');
+ callbacks.run('userAvatarSet', 'upload');
};
});
},
diff --git a/packages/rocketchat-ui-account/client/index.js b/app/ui-account/client/index.js
similarity index 100%
rename from packages/rocketchat-ui-account/client/index.js
rename to app/ui-account/client/index.js
diff --git a/app/ui-account/index.js b/app/ui-account/index.js
new file mode 100644
index 000000000000..40a7340d3887
--- /dev/null
+++ b/app/ui-account/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/packages/rocketchat-ui-admin/README.md b/app/ui-admin/README.md
similarity index 100%
rename from packages/rocketchat-ui-admin/README.md
rename to app/ui-admin/README.md
diff --git a/packages/rocketchat-ui-admin/client/admin.html b/app/ui-admin/client/admin.html
similarity index 100%
rename from packages/rocketchat-ui-admin/client/admin.html
rename to app/ui-admin/client/admin.html
diff --git a/app/ui-admin/client/admin.js b/app/ui-admin/client/admin.js
new file mode 100644
index 000000000000..f2ad5770a400
--- /dev/null
+++ b/app/ui-admin/client/admin.js
@@ -0,0 +1,633 @@
+import { Meteor } from 'meteor/meteor';
+import { Mongo } from 'meteor/mongo';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Random } from 'meteor/random';
+import { Tracker } from 'meteor/tracker';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/tap:i18n';
+import { settings } from '../../settings';
+import { SideNav, modal } from '../../ui-utils';
+import { t, handleError } from '../../utils';
+import { CachedCollection } from '../../ui-cached-collection';
+import _ from 'underscore';
+import s from 'underscore.string';
+import toastr from 'toastr';
+
+const TempSettings = new Mongo.Collection(null);
+
+const getDefaultSetting = function(settingId) {
+ return settings.collectionPrivate.findOne({
+ _id: settingId,
+ });
+};
+
+const setFieldValue = function(settingId, value, type, editor) {
+ const input = $('.page-settings').find(`[name="${ settingId }"]`);
+ switch (type) {
+ case 'boolean':
+ $('.page-settings').find(`[name="${ settingId }"][value="${ Number(value) }"]`).prop('checked', true).change();
+ break;
+ case 'code':
+ input.next()[0].CodeMirror.setValue(value);
+ break;
+ case 'color':
+ editor = value && value[0] === '#' ? 'color' : 'expression';
+ input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change();
+ input.val(value).change();
+ break;
+ case 'roomPick':
+ const selectedRooms = Template.instance().selectedRooms.get();
+ selectedRooms[settingId] = value;
+ Template.instance().selectedRooms.set(selectedRooms);
+ TempSettings.update({ _id: settingId }, { $set: { value, changed: JSON.stringify(settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value) } });
+ break;
+ default:
+ input.val(value).change();
+ }
+};
+
+Template.admin.onCreated(function() {
+ if (settings.cachedCollectionPrivate == null) {
+ settings.cachedCollectionPrivate = new CachedCollection({
+ name: 'private-settings',
+ eventType: 'onLogged',
+ useCache: false,
+ });
+ settings.collectionPrivate = settings.cachedCollectionPrivate.collection;
+ settings.cachedCollectionPrivate.init();
+ }
+ this.selectedRooms = new ReactiveVar({});
+ settings.collectionPrivate.find().observe({
+ added: (data) => {
+ const selectedRooms = this.selectedRooms.get();
+ if (data.type === 'roomPick') {
+ selectedRooms[data._id] = data.value;
+ this.selectedRooms.set(selectedRooms);
+ }
+ TempSettings.insert(data);
+ },
+ changed: (data) => {
+ const selectedRooms = this.selectedRooms.get();
+ if (data.type === 'roomPick') {
+ selectedRooms[data._id] = data.value;
+ this.selectedRooms.set(selectedRooms);
+ }
+ TempSettings.update(data._id, data);
+ },
+ removed: (data) => {
+ const selectedRooms = this.selectedRooms.get();
+ if (data.type === 'roomPick') {
+ delete selectedRooms[data._id];
+ this.selectedRooms.set(selectedRooms);
+ }
+ TempSettings.remove(data._id);
+ },
+ });
+});
+
+Template.admin.onDestroyed(function() {
+ TempSettings.remove({});
+});
+
+Template.admin.helpers({
+ languages() {
+ const languages = TAPi18n.getLanguages();
+
+ const result = Object.entries(languages)
+ .map(([key, language]) => ({ ...language, key: key.toLowerCase() }))
+ .sort((a, b) => a.key - b.key);
+
+ result.unshift({
+ name: 'Default',
+ en: 'Default',
+ key: '',
+ });
+
+ return result;
+ },
+ isAppLanguage(key) {
+ const languageKey = settings.get('Language');
+ return typeof languageKey === 'string' && languageKey.toLowerCase() === key;
+ },
+ group() {
+ const groupId = FlowRouter.getParam('group');
+ const group = settings.collectionPrivate.findOne({
+ _id: groupId,
+ type: 'group',
+ });
+ if (!group) {
+ return;
+ }
+ const rcSettings = settings.collectionPrivate.find({ group: groupId }, { sort: { section: 1, sorter: 1, i18nLabel: 1 } }).fetch();
+ const sections = {};
+
+ Object.keys(rcSettings).forEach((key) => {
+ const setting = rcSettings[key];
+ let i18nDefaultQuery;
+ if (setting.i18nDefaultQuery != null) {
+ if (_.isString(setting.i18nDefaultQuery)) {
+ i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery);
+ } else {
+ i18nDefaultQuery = setting.i18nDefaultQuery;
+ }
+ if (!_.isArray(i18nDefaultQuery)) {
+ i18nDefaultQuery = [i18nDefaultQuery];
+ }
+ Object.keys(i18nDefaultQuery).forEach((key) => {
+ const item = i18nDefaultQuery[key];
+ if (settings.collectionPrivate.findOne(item) != null) {
+ setting.value = TAPi18n.__(`${ setting._id }_Default`);
+ }
+ });
+ }
+ const settingSection = setting.section || '';
+ if (sections[settingSection] == null) {
+ sections[settingSection] = [];
+ }
+ sections[settingSection].push(setting);
+ });
+
+ group.sections = Object.keys(sections).map((key) => {
+ const value = sections[key];
+ return {
+ section: key,
+ settings: value,
+ };
+ });
+ return group;
+ },
+ i18nDefaultValue() {
+ return TAPi18n.__(`${ this._id }_Default`);
+ },
+ isDisabled() {
+ let enableQuery;
+ if (this.blocked) {
+ return {
+ disabled: 'disabled',
+ };
+ }
+ if (this.enableQuery == null) {
+ return {};
+ }
+ if (_.isString(this.enableQuery)) {
+ enableQuery = JSON.parse(this.enableQuery);
+ } else {
+ enableQuery = this.enableQuery;
+ }
+ if (!_.isArray(enableQuery)) {
+ enableQuery = [enableQuery];
+ }
+ let found = 0;
+
+ Object.keys(enableQuery).forEach((key) => {
+ const item = enableQuery[key];
+ if (TempSettings.findOne(item) != null) {
+ found++;
+ }
+ });
+ if (found === enableQuery.length) {
+ return {};
+ } else {
+ return {
+ disabled: 'disabled',
+ };
+ }
+ },
+ isReadonly() {
+ if (this.readonly === true) {
+ return {
+ readonly: 'readonly',
+ };
+ }
+ },
+ canAutocomplete() {
+ if (this.autocomplete === false) {
+ return {
+ autocomplete: 'off',
+ };
+ }
+ },
+ hasChanges(section) {
+ const group = FlowRouter.getParam('group');
+ const query = {
+ group,
+ changed: true,
+ };
+ if (section != null) {
+ if (section === '') {
+ query.$or = [
+ {
+ section: '',
+ }, {
+ section: {
+ $exists: false,
+ },
+ },
+ ];
+ } else {
+ query.section = section;
+ }
+ }
+ return TempSettings.find(query).count() > 0;
+ },
+ isSettingChanged(id) {
+ return TempSettings.findOne({
+ _id: id,
+ }, {
+ fields: {
+ changed: 1,
+ },
+ }).changed;
+ },
+ translateSection(section) {
+ if (section.indexOf(':') > -1) {
+ return section;
+ }
+ return t(section);
+ },
+ label() {
+ const label = this.i18nLabel || this._id;
+ if (label) {
+ return TAPi18n.__(label);
+ }
+ },
+ description() {
+ let description;
+ if (this.i18nDescription) {
+ description = TAPi18n.__(this.i18nDescription);
+ }
+ if ((description != null) && description !== this.i18nDescription) {
+ return description;
+ }
+ },
+ sectionIsCustomOAuth(section) {
+ return /^Custom OAuth:\s.+/.test(section);
+ },
+ callbackURL(section) {
+ const id = s.strRight(section, 'Custom OAuth: ').toLowerCase();
+ return Meteor.absoluteUrl(`_oauth/${ id }`);
+ },
+ relativeUrl(url) {
+ return Meteor.absoluteUrl(url);
+ },
+ selectedOption(_id, val) {
+ const option = settings.collectionPrivate.findOne({ _id });
+ return option && option.value === val;
+ },
+ random() {
+ return Random.id();
+ },
+ getEditorOptions(readOnly = false) {
+ return {
+ lineNumbers: true,
+ mode: this.code || 'javascript',
+ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+ foldGutter: true,
+ matchBrackets: true,
+ autoCloseBrackets: true,
+ matchTags: true,
+ showTrailingSpace: true,
+ highlightSelectionMatches: true,
+ readOnly,
+ };
+ },
+ setEditorOnBlur(_id) {
+ Meteor.defer(function() {
+ if (!$(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0]) {
+ return;
+ }
+ const codeMirror = $(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0].CodeMirror;
+ if (codeMirror.changeAdded === true) {
+ return;
+ }
+ const onChange = function() {
+ const value = codeMirror.getValue();
+ TempSettings.update({ _id }, { $set: { value, changed: settings.collectionPrivate.findOne(_id).value !== value } });
+ };
+ const onChangeDelayed = _.debounce(onChange, 500);
+ codeMirror.on('change', onChangeDelayed);
+ codeMirror.changeAdded = true;
+ });
+ },
+ assetAccept(fileConstraints) {
+ if (fileConstraints.extensions && fileConstraints.extensions.length) {
+ return `.${ fileConstraints.extensions.join(', .') }`;
+ }
+ },
+ autocompleteRoom() {
+ return {
+ limit: 10,
+ // inputDelay: 300
+ rules: [
+ {
+ // @TODO maybe change this 'collection' and/or template
+ collection: 'CachedChannelList',
+ subscription: 'channelAndPrivateAutocomplete',
+ field: 'name',
+ template: Template.roomSearch,
+ noMatchTemplate: Template.roomSearchEmpty,
+ matchAll: true,
+ selector(match) {
+ return {
+ name: match,
+ };
+ },
+ sort: 'name',
+ },
+ ],
+ };
+ },
+ selectedRooms() {
+ return Template.instance().selectedRooms.get()[this._id] || [];
+ },
+ getColorVariable(color) {
+ return color.replace(/theme-color-/, '@');
+ },
+ showResetButton() {
+ const setting = TempSettings.findOne({ _id: this._id }, { fields: { value: 1, packageValue: 1 } });
+ return !this.disableReset && !this.readonly && this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked;
+ },
+});
+
+Template.admin.events({
+ 'change .input-monitor, keyup .input-monitor': _.throttle(function(e) {
+ let value = s.trim($(e.target).val());
+ switch (this.type) {
+ case 'int':
+ value = parseInt(value);
+ break;
+ case 'boolean':
+ value = value === '1';
+ break;
+ case 'color':
+ $(e.target).siblings('.colorpicker-swatch').css('background-color', value);
+ }
+ TempSettings.update({
+ _id: this._id,
+ }, {
+ $set: {
+ value,
+ changed: settings.collectionPrivate.findOne(this._id).value !== value,
+ },
+ });
+ }, 500),
+ 'change select[name=color-editor]'(e) {
+ const value = s.trim($(e.target).val());
+ TempSettings.update({ _id: this._id }, { $set: { editor: value } });
+ settings.collectionPrivate.update({ _id: this._id }, { $set: { editor: value } });
+ },
+ 'click .rc-header__section-button .discard'() {
+ const group = FlowRouter.getParam('group');
+ const query = {
+ group,
+ changed: true,
+ };
+ const rcSettings = TempSettings.find(query, {
+ fields: { _id: 1, value: 1, packageValue: 1 },
+ }).fetch();
+ rcSettings.forEach(function(setting) {
+ const oldSetting = settings.collectionPrivate.findOne({ _id: setting._id }, { fields: { value: 1, type: 1, editor: 1 } });
+ setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor);
+ });
+ },
+ 'click .reset-setting'(e) {
+ e.preventDefault();
+ let settingId = $(e.target).data('setting');
+ if (typeof settingId === 'undefined') {
+ settingId = $(e.target).parent().data('setting');
+ }
+ const defaultValue = getDefaultSetting(settingId);
+ setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor);
+ },
+ 'click .reset-group'(e) {
+ let rcSettings;
+ e.preventDefault();
+ const group = FlowRouter.getParam('group');
+ const section = $(e.target).data('section');
+ if (section === '') {
+ rcSettings = TempSettings.find({ group, section: { $exists: false } }, { fields: { _id: 1 } }).fetch();
+ } else {
+ rcSettings = TempSettings.find({ group, section }, { fields: { _id: 1 } }).fetch();
+ }
+ rcSettings.forEach(function(setting) {
+ const defaultValue = getDefaultSetting(setting._id);
+ setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor);
+ TempSettings.update({ _id: setting._id }, {
+ $set: {
+ value: defaultValue.packageValue,
+ changed: settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue,
+ },
+ });
+ });
+ },
+ 'click .rc-header__section-button .save'() {
+ const group = FlowRouter.getParam('group');
+ const query = { group, changed: true };
+ const rcSettings = TempSettings.find(query, { fields: { _id: 1, value: 1, editor: 1 } }).fetch() || [];
+ if (rcSettings.length === 0) {
+ return;
+ }
+
+ settings.batchSet(rcSettings, (err) => {
+ if (err) {
+ return handleError(err);
+ }
+
+ TempSettings.update({ changed: true }, { $unset: { changed: 1 } });
+
+ if (rcSettings.some(({ _id }) => _id === 'Language')) {
+ const lng = Meteor.user().language
+ || rcSettings.filter(({ _id }) => _id === 'Language').shift().value
+ || 'en';
+ return TAPi18n._loadLanguage(lng).then(() => toastr.success(TAPi18n.__('Settings_updated', { lng })));
+ }
+ toastr.success(TAPi18n.__('Settings_updated'));
+ });
+
+ },
+ 'click .rc-header__section-button .refresh-clients'() {
+ Meteor.call('refreshClients', function() {
+ toastr.success(TAPi18n.__('Clients_will_refresh_in_a_few_seconds'));
+ });
+ },
+ 'click .rc-header__section-button .add-custom-oauth'() {
+ const config = {
+ title: TAPi18n.__('Add_custom_oauth'),
+ text: TAPi18n.__('Give_a_unique_name_for_the_custom_oauth'),
+ type: 'input',
+ showCancelButton: true,
+ closeOnConfirm: true,
+ inputPlaceholder: TAPi18n.__('Custom_oauth_unique_name'),
+ };
+ modal.open(config, function(inputValue) {
+ if (inputValue === false) {
+ return false;
+ }
+ if (inputValue === '') {
+ modal.showInputError(TAPi18n.__('Name_cant_be_empty'));
+ return false;
+ }
+ Meteor.call('addOAuthService', inputValue, function(err) {
+ if (err) {
+ handleError(err);
+ }
+ });
+ });
+ },
+ 'click .rc-header__section-button .refresh-oauth'() {
+ toastr.info(TAPi18n.__('Refreshing'));
+ return Meteor.call('refreshOAuthService', function(err) {
+ if (err) {
+ return handleError(err);
+ } else {
+ return toastr.success(TAPi18n.__('Done'));
+ }
+ });
+ },
+ 'click .remove-custom-oauth'() {
+ const name = this.section.replace('Custom OAuth: ', '');
+ const config = {
+ title: TAPi18n.__('Are_you_sure'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#DD6B55',
+ confirmButtonText: TAPi18n.__('Yes_delete_it'),
+ cancelButtonText: TAPi18n.__('Cancel'),
+ closeOnConfirm: true,
+ };
+ modal.open(config, function() {
+ Meteor.call('removeOAuthService', name);
+ });
+ },
+ 'click .delete-asset'() {
+ Meteor.call('unsetAsset', this.asset);
+ },
+ 'change input[type=file]'(ev) {
+ const e = ev.originalEvent || ev;
+ let { files } = e.target;
+ if (!files || files.length === 0) {
+ if (e.dataTransfer && e.dataTransfer.files) {
+ files = e.dataTransfer.files;
+ } else {
+ files = [];
+ }
+ }
+
+ Object.keys(files).forEach((key) => {
+ const blob = files[key];
+ toastr.info(TAPi18n.__('Uploading_file'));
+ const reader = new FileReader();
+ reader.readAsBinaryString(blob);
+ reader.onloadend = () => Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) {
+ if (err != null) {
+ handleError(err);
+ console.log(err);
+ return;
+ }
+ return toastr.success(TAPi18n.__('File_uploaded'));
+ });
+ });
+ },
+ 'click .expand'(e) {
+ const sectionTitle = e.currentTarget;
+ const section = sectionTitle.closest('.section');
+ const button = sectionTitle.querySelector('button');
+ const i = button.querySelector('i');
+
+ sectionTitle.classList.remove('expand');
+ sectionTitle.classList.add('collapse');
+ section.classList.remove('section-collapsed');
+ button.setAttribute('title', TAPi18n.__('Collapse'));
+ i.className = 'icon-angle-up';
+
+ $('.CodeMirror').each(function(index, codeMirror) {
+ codeMirror.CodeMirror.refresh();
+ });
+ },
+ 'click .collapse'(e) {
+ const sectionTitle = e.currentTarget;
+ const section = sectionTitle.closest('.section');
+ const button = sectionTitle.querySelector('button');
+ const i = button.querySelector('i');
+
+ sectionTitle.classList.remove('collapse');
+ sectionTitle.classList.add('expand');
+ section.classList.add('section-collapsed');
+ button.setAttribute('title', TAPi18n.__('Expand'));
+ i.className = 'icon-angle-down';
+ },
+ 'click button.action'() {
+ if (this.type !== 'action') {
+ return;
+ }
+ Meteor.call(this.value, function(err, data) {
+ if (err != null) {
+ err.details = _.extend(err.details || {}, {
+ errorTitle: 'Error',
+ });
+ handleError(err);
+ return;
+ }
+ const args = [data.message].concat(data.params);
+ toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success'));
+ });
+ },
+ 'click .button-fullscreen'() {
+ const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`);
+ codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color');
+ codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
+ },
+ 'click .button-restore'() {
+ const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`);
+ codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color');
+ codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
+ },
+ 'autocompleteselect .autocomplete'(event, instance, doc) {
+ const selectedRooms = instance.selectedRooms.get();
+ selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc);
+ instance.selectedRooms.set(selectedRooms);
+ const value = selectedRooms[this.id];
+ TempSettings.update({ _id: this.id }, { $set: { value, changed: JSON.stringify(settings.collectionPrivate.findOne(this.id).value) !== JSON.stringify(value) } });
+ event.currentTarget.value = '';
+ event.currentTarget.focus();
+ },
+ 'click .remove-room'(event, instance) {
+ const docId = this._id;
+ const settingId = event.currentTarget.getAttribute('data-setting');
+ const selectedRooms = instance.selectedRooms.get();
+ selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], function(setting) {
+ return setting._id === docId;
+ });
+ instance.selectedRooms.set(selectedRooms);
+ const value = selectedRooms[settingId];
+ TempSettings.update({ _id: settingId }, {
+ $set: {
+ value,
+ changed: JSON.stringify(settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value),
+ },
+ });
+ },
+});
+
+Template.admin.onRendered(function() {
+ Tracker.afterFlush(function() {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+ Tracker.autorun(function() {
+ const hasColor = TempSettings.find({
+ group: FlowRouter.getParam('group'),
+ type: 'color',
+ }, { fields: { _id: 1, editor: 1 } }).fetch().length;
+ if (hasColor) {
+ Meteor.setTimeout(function() {
+ $('.colorpicker-input').each(function(index, el) {
+ if (!el._jscLinkedInstance) {
+ new jscolor(el); //eslint-disable-line
+ }
+ });
+ }, 400);
+ }
+ });
+});
diff --git a/packages/rocketchat-ui-admin/client/adminFlex.html b/app/ui-admin/client/adminFlex.html
similarity index 89%
rename from packages/rocketchat-ui-admin/client/adminFlex.html
rename to app/ui-admin/client/adminFlex.html
index cb8b16ff24ff..0ab4fb2f7739 100644
--- a/packages/rocketchat-ui-admin/client/adminFlex.html
+++ b/app/ui-admin/client/adminFlex.html
@@ -2,11 +2,9 @@