diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js
index 7cab7a10977b..c4892b409ad0 100644
--- a/app/api/server/v1/rooms.js
+++ b/app/api/server/v1/rooms.js
@@ -222,7 +222,7 @@ API.v1.addRoute('rooms.leave', { authRequired: true }, {
API.v1.addRoute('rooms.createDiscussion', { authRequired: true }, {
post() {
- const { prid, pmid, reply, t_name, users } = this.bodyParams;
+ const { prid, pmid, reply, t_name, users, t } = this.bodyParams;
if (!prid) {
return API.v1.failure('Body parameter "prid" is required.');
}
@@ -238,6 +238,7 @@ API.v1.addRoute('rooms.createDiscussion', { authRequired: true }, {
pmid,
t_name,
reply,
+ t,
users: users || [],
}));
diff --git a/app/articles/README.md b/app/articles/README.md
new file mode 100644
index 000000000000..f117eed00aab
--- /dev/null
+++ b/app/articles/README.md
@@ -0,0 +1 @@
+Readme file for articles.
diff --git a/app/articles/server/api/api.js b/app/articles/server/api/api.js
new file mode 100644
index 000000000000..9f6de329926b
--- /dev/null
+++ b/app/articles/server/api/api.js
@@ -0,0 +1,103 @@
+import { Restivus } from 'meteor/nimble:restivus';
+import _ from 'underscore';
+
+import { processWebhookMessage } from '../../../lib';
+import { API } from '../../../api';
+import { settings } from '../../../settings';
+import * as Models from '../../../models';
+
+const Api = new Restivus({
+ enableCors: true,
+ apiPath: 'ghooks/',
+ auth: {
+ user() {
+ const payloadKeys = Object.keys(this.bodyParams);
+ const payloadIsWrapped = (this.bodyParams && this.bodyParams.payload) && payloadKeys.length === 1;
+ if (payloadIsWrapped && this.request.headers['content-type'] === 'application/x-www-form-urlencoded') {
+ try {
+ this.bodyParams = JSON.parse(this.bodyParams.payload);
+ } catch ({ message }) {
+ return {
+ error: {
+ statusCode: 400,
+ body: {
+ success: false,
+ error: message,
+ },
+ },
+ };
+ }
+ }
+
+ this.announceToken = settings.get('Announcement_Token');
+ const token = decodeURIComponent(this.request.params.token);
+
+ if (this.announceToken !== token) {
+ return {
+ error: {
+ statusCode: 404,
+ body: {
+ success: false,
+ error: 'Invalid token provided.',
+ },
+ },
+ };
+ }
+
+ const user = this.bodyParams.userId ? Models.Users.findOne({ _id: this.bodyParams.userId }) : Models.Users.findOne({ username: this.bodyParams.username });
+
+ return { user };
+ },
+ },
+});
+
+function executeAnnouncementRest() {
+ const defaultValues = {
+ channel: this.bodyParams.channel,
+ alias: this.bodyParams.alias,
+ avatar: this.bodyParams.avatar,
+ emoji: this.bodyParams.emoji,
+ };
+
+ // TODO: Turn this into an option on the integrations - no body means a success
+ // TODO: Temporary fix for https://github.com/RocketChat/Rocket.Chat/issues/7770 until the above is implemented
+ if (!this.bodyParams || (_.isEmpty(this.bodyParams) && !this.integration.scriptEnabled)) {
+ // return RocketChat.API.v1.failure('body-empty');
+ return API.v1.success();
+ }
+
+ try {
+ const message = processWebhookMessage(this.bodyParams, this.user, defaultValues);
+ if (_.isEmpty(message)) {
+ return API.v1.failure('unknown-error');
+ }
+
+ return API.v1.success();
+ } catch ({ error, message }) {
+ return API.v1.failure(error || message);
+ }
+}
+
+function executefetchUserRest() {
+ try {
+ const { _id, name, username, emails } = this.user;
+ const user = { _id, name, username, emails };
+
+ return API.v1.success({ user });
+ } catch ({ error, message }) {
+ return API.v1.failure(error || message);
+ }
+}
+
+Api.addRoute(':token', { authRequired: true }, {
+ post: executeAnnouncementRest,
+ get: executeAnnouncementRest,
+});
+
+// If a user is editor/admin in Ghost but is not an admin in RC,
+// then the e-mail will not be provided to that user
+// This method will allow user to fetch user with email.
+Api.addRoute(':token/getUser', { authRequired: true }, {
+ post: executefetchUserRest,
+ get: executefetchUserRest,
+});
diff --git a/app/articles/server/index.js b/app/articles/server/index.js
new file mode 100644
index 000000000000..b9804b9805f2
--- /dev/null
+++ b/app/articles/server/index.js
@@ -0,0 +1,6 @@
+import './settings';
+import './methods/admin';
+import './methods/user';
+import './api/api';
+import './lib/triggerHandler';
+import './triggers';
diff --git a/app/articles/server/lib/triggerHandler.js b/app/articles/server/lib/triggerHandler.js
new file mode 100644
index 000000000000..3a198f4312ee
--- /dev/null
+++ b/app/articles/server/lib/triggerHandler.js
@@ -0,0 +1,90 @@
+import { ghostAPI } from '../utils/ghostAPI';
+
+export const triggerHandler = new class ArticlesSettingsHandler {
+ eventNameArgumentsToObject(...args) {
+ const argObject = {
+ event: args[0],
+ };
+ switch (argObject.event) {
+ case 'userEmail':
+ case 'userRealname':
+ case 'userName':
+ case 'deleteUser':
+ if (args.length >= 2) {
+ argObject.user = args[1];
+ }
+ break;
+ case 'roomType':
+ case 'roomName':
+ if (args.length >= 2) {
+ argObject.room = args[1];
+ }
+ break;
+ case 'siteTitle':
+ argObject.article = args[1];
+ break;
+ default:
+ argObject.event = undefined;
+ break;
+ }
+ return argObject;
+ }
+
+ mapEventArgsToData(data, { event, room, user, article }) {
+ data.event = event;
+ switch (event) {
+ case 'userEmail':
+ case 'userRealname':
+ case 'userName':
+ case 'deleteUser':
+ data.user_id = user._id;
+
+ if (user.name) {
+ data.name = user.name;
+ }
+
+ if (user.email) {
+ data.email = user.email;
+ }
+
+ if (user.username) {
+ data.username = user.username;
+ }
+ break;
+ case 'roomType':
+ case 'roomName':
+ data.room_id = room.rid;
+
+ if (room.name) {
+ data.name = room.name;
+ }
+
+ if (room.type) {
+ data.type = room.type;
+ }
+ break;
+ case 'siteTitle':
+ if (article && article.title) {
+ data.title = article.title;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ executeTrigger(...args) {
+ const argObject = this.eventNameArgumentsToObject(...args);
+ const { event } = argObject;
+
+ if (!event) {
+ return;
+ }
+
+ const data = {};
+
+ this.mapEventArgsToData(data, argObject);
+
+ ghostAPI.executeTriggerUrl(data, 0);
+ }
+}();
diff --git a/app/articles/server/logoutCleanUp.js b/app/articles/server/logoutCleanUp.js
new file mode 100644
index 000000000000..688e80c205f9
--- /dev/null
+++ b/app/articles/server/logoutCleanUp.js
@@ -0,0 +1,13 @@
+import { settings } from '../../settings';
+import { ghostAPI } from './utils/ghostAPI';
+
+export function ghostCleanUp(cookie) {
+ try {
+ if (settings.get('Articles_Enabled')) {
+ ghostAPI.deleteSesseion(cookie);
+ }
+ } catch (e) {
+ // Do nothing if failed to logout from Ghost.
+ // Error will be because user has not logged in to Ghost.
+ }
+}
diff --git a/app/articles/server/methods/admin.js b/app/articles/server/methods/admin.js
new file mode 100644
index 000000000000..dee3a8785e21
--- /dev/null
+++ b/app/articles/server/methods/admin.js
@@ -0,0 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+
+import { hasPermission } from '../../../authorization';
+import { ghostAPI } from '../utils/ghostAPI';
+import { settings } from '../../../settings';
+
+Meteor.methods({
+ articlesAdminPanel(loginToken) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'articlesAdminPanel',
+ });
+ }
+
+ const enabled = settings.get('Articles_Enabled');
+
+ if (!enabled) {
+ throw new Meteor.Error('error-articles-disabled', 'Articles are disabled', {
+ method: 'articlesAdminPanel',
+ });
+ }
+ let errMsg = 'Unable to connect to Ghost. Make sure Ghost is running';
+
+ try {
+ let response = ghostAPI.isSetup();
+
+ if (response.data.setup[0].status) { // Ghost site is already setup
+ return ghostAPI.redirectToGhostLink;
+ }
+
+ if (!hasPermission(Meteor.userId(), 'setup-ghost')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Setting up Ghost is not allowed', {
+ method: 'articlesAdminPanel',
+ });
+ }
+
+ // Setup Ghost Site and set title
+ response = ghostAPI.setupGhost(loginToken);
+
+ errMsg = 'Unable to setup. Make sure Ghost is running';
+
+ if (response.statusCode === 201 && response.content) {
+ return ghostAPI.redirectToGhostLink;
+ }
+
+ throw new Meteor.Error(errMsg);
+ } catch (e) {
+ throw new Meteor.Error(e.error || errMsg);
+ }
+ },
+});
diff --git a/app/articles/server/methods/user.js b/app/articles/server/methods/user.js
new file mode 100644
index 000000000000..092fa4280c8e
--- /dev/null
+++ b/app/articles/server/methods/user.js
@@ -0,0 +1,79 @@
+import { Meteor } from 'meteor/meteor';
+
+import { ghostAPI } from '../utils/ghostAPI';
+import { settings } from '../../../settings';
+
+Meteor.methods({
+ redirectUserToArticles(loginToken) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+ method: 'redirectUserToArticles',
+ });
+ }
+
+ const enabled = settings.get('Articles_Enabled');
+
+ if (!enabled) {
+ throw new Meteor.Error('error-articles-disabled', 'Articles are disabled', {
+ method: 'redirectUserToArticles',
+ });
+ }
+
+ let errMsg = 'Ghost is not set up. Setup can be done from Admin Panel';
+
+ try {
+ const response = ghostAPI.isSetup();
+
+ if (response.data.setup[0].status) { // Ghost site is already setup
+ const u = ghostAPI.userExistInGhost(Meteor.userId()).users[0];
+
+ if (u.exist && u.status === 'active') {
+ return ghostAPI.redirectToGhostLink;
+ }
+
+ if (u.exist) { // user exist but suspended
+ throw new Meteor.Error('error-articles-user-suspended', 'You are suspended from Ghost', {
+ method: 'redirectUserToArticles',
+ });
+ }
+
+ const inviteOnly = ghostAPI.inviteSettingInGhost();
+
+ // create user account in ghost
+ if (!inviteOnly && ghostAPI.createUserAccount(loginToken).statusCode === 200) {
+ return ghostAPI.redirectToGhostLink;
+ }
+
+ errMsg = inviteOnly ? 'You are not a member of Ghost. Ask admin to add' : 'Unable to setup your account';
+ }
+
+ // Cannot setup Ghost from sidenav
+ throw new Meteor.Error(errMsg);
+ } catch (e) {
+ throw new Meteor.Error(e.error || 'Unable to connect to Ghost.');
+ }
+ },
+
+ redirectToUsersArticles(_id) {
+ const enabled = settings.get('Articles_Enabled');
+
+ if (!enabled) {
+ throw new Meteor.Error('error-articles-disabled', 'Articles are disabled', {
+ method: 'redirectToUsersArticles',
+ });
+ }
+ const errMsg = 'User is not a member of Ghost';
+
+ try {
+ const response = ghostAPI.userExistInGhost(_id).users[0];
+
+ if (response.exist) {
+ return ghostAPI.redirectToPublicLink(response.slug);
+ }
+
+ throw new Meteor.Error(errMsg);
+ } catch (e) {
+ throw new Meteor.Error(e.error || errMsg);
+ }
+ },
+});
diff --git a/app/articles/server/settings.js b/app/articles/server/settings.js
new file mode 100644
index 000000000000..cfa9c7d8a794
--- /dev/null
+++ b/app/articles/server/settings.js
@@ -0,0 +1,66 @@
+import { Meteor } from 'meteor/meteor';
+
+import { settings } from '../../settings';
+
+Meteor.startup(() => {
+ settings.addGroup('Articles', function() {
+ this.add('Articles_Enabled', false, {
+ type: 'boolean',
+ i18nLabel: 'Enable',
+ public: true,
+ });
+
+ this.add('Article_Site_Title', 'Rocket.Chat', {
+ type: 'string',
+ enableQuery: {
+ _id: 'Articles_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Article_Site_Title',
+ public: true,
+ });
+
+ this.add('Article_Site_Url', 'http://localhost:2368', {
+ type: 'string',
+ enableQuery: {
+ _id: 'Articles_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Article_Site_Url',
+ public: true,
+ });
+
+ this.add('Announcement_Token', 'announcement_token', {
+ type: 'string',
+ readonly: true,
+ enableQuery: {
+ _id: 'Articles_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Announcement_Token',
+ public: true,
+ });
+
+ this.add('Settings_Token', 'articles_settings_token', {
+ type: 'string',
+ readonly: true,
+ enableQuery: {
+ _id: 'Articles_Enabled',
+ value: true,
+ },
+ i18nLabel: 'Settings_Token',
+ public: true,
+ });
+
+ this.add('Articles_Admin_Panel', 'articlesAdminPanel', {
+ type: 'link',
+ enableQuery: {
+ _id: 'Articles_Enabled',
+ value: true,
+ },
+ linkText: 'Article_Admin_Panel',
+ i18nLabel: 'Article_Admin_Panel',
+ i18nDescription: 'Article_Admin_Panel_Description',
+ });
+ });
+});
diff --git a/app/articles/server/triggers.js b/app/articles/server/triggers.js
new file mode 100644
index 000000000000..045c1c292f25
--- /dev/null
+++ b/app/articles/server/triggers.js
@@ -0,0 +1,23 @@
+import { callbacks } from '../../callbacks';
+import { triggerHandler } from './lib/triggerHandler';
+import { settings } from '../../settings';
+
+const callbackHandler = function _callbackHandler(eventType) {
+ return function _wrapperFunction(...args) {
+ return triggerHandler.executeTrigger(eventType, ...args);
+ };
+};
+
+const priority = settings.get('Articles_Enabled') ? callbacks.priority.HIGH : callbacks.priority.LOW;
+
+callbacks.add('afterUserEmailChange', callbackHandler('userEmail'), priority);
+callbacks.add('afterUserRealNameChange', callbackHandler('userRealname'), priority);
+callbacks.add('afterUsernameChange', callbackHandler('userName'), priority);
+callbacks.add('afterRoomTypeChange', callbackHandler('roomType'), priority);
+callbacks.add('afterDeleteUser', callbackHandler('deleteUser'), priority);
+// TODO: find why roomName have no arg passed even with High priority.
+callbacks.add('afterRoomNameChange', callbackHandler('roomName'), priority);
+
+settings.get('Article_Site_Title', (key, value) => {
+ triggerHandler.executeTrigger('siteTitle', { title: value });
+});
diff --git a/app/articles/server/utils/ghostAPI.js b/app/articles/server/utils/ghostAPI.js
new file mode 100644
index 000000000000..561341bb9a29
--- /dev/null
+++ b/app/articles/server/utils/ghostAPI.js
@@ -0,0 +1,139 @@
+import { Meteor } from 'meteor/meteor';
+import { HTTP } from 'meteor/http';
+import { Random } from 'meteor/random';
+
+import { settings } from '../../../settings';
+
+export const ghostAPI = new class {
+ constructor() {
+ this._getBaseUrl = settings.get('Article_Site_Url').replace(/\/$/, '');
+ this.adminApi = '/ghost/api/v2/admin';
+ this.rhooks = this.buildAPIUrl('rhooks', settings.get('Settings_Token'));
+ this.setupUrl = this.buildAPIUrl('authentication', 'setup');
+ this.sessionUrl = this.buildAPIUrl('session');
+ this.inviteUrl = this.buildAPIUrl('invitesetting');
+ this.createUserAccountUrl = this.buildAPIUrl('authentication', 'adduser');
+ this.userExistUrl = this.buildAPIUrl('userexist');
+ this.siteUrl = `${ this._getBaseUrl }/ghost`;
+ this.redirectToGhostLink = { link: this.siteUrl };
+ }
+
+ buildAPIUrl(type, subtype = '') {
+ const dir = `/${ type }/${ subtype }`;
+ return this._getBaseUrl + this.adminApi + dir;
+ }
+
+ authorUrl(slug) {
+ return `${ this._getBaseUrl }/author/${ slug }`;
+ }
+
+ isSetup() {
+ return HTTP.call('GET', this.setupUrl);
+ }
+
+ redirectToPublicLink(slug) {
+ return {
+ link: this.authorUrl(slug),
+ };
+ }
+
+ setupGhost(loginToken) {
+ const rcUrl = Meteor.absoluteUrl().replace(/\/$/, '');
+ const blogTitle = settings.get('Article_Site_Title');
+ const announceToken = Random.secret(30);
+ const settingsToken = Random.secret(30);
+ settings.updateById('Announcement_Token', announceToken);
+ settings.updateById('Settings_Token', settingsToken);
+ const data = {
+ setup: [{
+ rc_url: rcUrl,
+ rc_id: Meteor.userId(),
+ rc_token: loginToken,
+ announce_token: announceToken,
+ settings_token: settingsToken,
+ blogTitle,
+ }],
+ };
+ return HTTP.call('POST', this.setupUrl, { data, headers: { 'Content-Type': 'application/json' } });
+ }
+
+ createUserAccount(loginToken) {
+ const { username } = Meteor.user();
+ const data = {
+ user: [{
+ rc_username: username,
+ role: 'Author', // User can add itself only as Author, even if he/she is admin in RC
+ rc_uid: Meteor.userId(),
+ rc_token: loginToken,
+ }],
+ };
+ return HTTP.call('POST', this.createUserAccountUrl, { data, headers: { 'Content-Type': 'application/json' } });
+ }
+
+ userExistInGhost(_id) {
+ const data = {
+ user: [{
+ rc_uid: _id,
+ }],
+ };
+ const response = HTTP.call('GET', this.userExistUrl, { data, headers: { 'Content-Type': 'application/json' } });
+ return response.data;
+ }
+
+ inviteSettingInGhost() {
+ const response = HTTP.call('GET', this.inviteUrl);
+ const { settings } = response.data;
+
+ if (settings && settings[0] && settings[0].key === 'invite_only') {
+ return settings[0].value;
+ }
+ // default value in Ghost
+ return false;
+ }
+
+ executeTriggerUrl(data, tries = 0) {
+ if (!settings.get('Articles_Enabled')) {
+ return;
+ }
+
+ const retryCount = 5;
+
+ const opts = {
+ params: {},
+ method: 'POST',
+ url: this.rhooks,
+ data,
+ auth: undefined,
+ npmRequestOptions: {
+ rejectUnauthorized: !settings.get('Allow_Invalid_SelfSigned_Certs'),
+ strictSSL: !settings.get('Allow_Invalid_SelfSigned_Certs'),
+ },
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36',
+ },
+ };
+
+ if (!opts.url || !opts.method) {
+ return;
+ }
+
+ HTTP.call(opts.method, opts.url, opts, (error, result) => {
+ // if the result contained nothing or wasn't a successful statusCode
+ if (!result) {
+ if (tries < retryCount) {
+ // 2 seconds, 4 seconds, 8 seconds
+ const waitTime = Math.pow(2, tries + 1) * 1000;
+
+ Meteor.setTimeout(() => {
+ this.executeTriggerUrl(data, tries + 1);
+ }, waitTime);
+ }
+ }
+ });
+ }
+
+ deleteSesseion(cookie) {
+ const rcUrl = Meteor.absoluteUrl().replace(/\/$/, '');
+ HTTP.call('DELETE', this.sessionUrl, { headers: { cookie, referer: rcUrl } });
+ }
+}();
diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js
index 8d57052b66db..dfe7442ef4ef 100644
--- a/app/authorization/server/startup.js
+++ b/app/authorization/server/startup.js
@@ -60,6 +60,7 @@ Meteor.startup(function() {
{ _id: 'set-owner', roles: ['admin', 'owner'] },
{ _id: 'send-many-messages', roles: ['admin', 'bot'] },
{ _id: 'set-leader', roles: ['admin', 'owner'] },
+ { _id: 'setup-ghost', roles: ['admin', 'owner'] },
{ _id: 'unarchive-room', roles: ['admin'] },
{ _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'anonymous'] },
{ _id: 'user-generate-access-token', roles: ['admin'] },
diff --git a/app/channel-settings/server/functions/saveRoomType.js b/app/channel-settings/server/functions/saveRoomType.js
index 9bf6cb4503ba..6831e239e9d8 100644
--- a/app/channel-settings/server/functions/saveRoomType.js
+++ b/app/channel-settings/server/functions/saveRoomType.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { TAPi18n } from 'meteor/tap:i18n';
+import { callbacks } from '../../../callbacks';
import { Rooms, Subscriptions, Messages } from '../../../models';
import { settings } from '../../../settings';
@@ -43,5 +44,6 @@ export const saveRoomType = function(rid, roomType, user, sendMessage = true) {
}
Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_privacy', rid, message, user);
}
+ callbacks.run('afterRoomTypeChange', { rid, type: roomType });
return result;
};
diff --git a/app/discussion/server/methods/createDiscussion.js b/app/discussion/server/methods/createDiscussion.js
index 78535311ad9d..63cd7470483d 100644
--- a/app/discussion/server/methods/createDiscussion.js
+++ b/app/discussion/server/methods/createDiscussion.js
@@ -33,7 +33,7 @@ const mentionMessage = (rid, { _id, username, name }, message_embedded) => {
return Messages.insert(welcomeMessage);
};
-const create = ({ prid, pmid, t_name, reply, users }) => {
+const create = ({ prid, pmid, t_name, reply, t, users }) => {
// if you set both, prid and pmid, and the rooms doesnt match... should throw an error)
let message = false;
if (pmid) {
@@ -86,8 +86,10 @@ const create = ({ prid, pmid, t_name, reply, users }) => {
// auto invite the replied message owner
const invitedUsers = message ? [message.u.username, ...users] : users;
- // discussions are always created as private groups
- const discussion = createRoom('p', name, user.username, [...new Set(invitedUsers)], false, {
+ // discussions are created as private groups, if t is not given as 'c'
+ const type = t === 'c' ? 'c' : 'p';
+
+ const discussion = createRoom(type, name, user.username, [...new Set(invitedUsers)], false, {
fname: t_name,
description: message.msg, // TODO discussions remove
topic: p_room.name, // TODO discussions remove
@@ -121,7 +123,7 @@ Meteor.methods({
* @param {string} t_name - discussion name
* @param {string[]} users - users to be added
*/
- createDiscussion({ prid, pmid, t_name, reply, users }) {
+ createDiscussion({ prid, pmid, t_name, reply, t, users }) {
if (!settings.get('Discussion_enabled')) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
@@ -135,6 +137,6 @@ Meteor.methods({
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
- return create({ uid, prid, pmid, t_name, reply, users });
+ return create({ uid, prid, pmid, t_name, reply, t, users });
},
});
diff --git a/app/file-upload/client/lib/fileUploadHandler.js b/app/file-upload/client/lib/fileUploadHandler.js
index fe13fdcbb965..b584a98da269 100644
--- a/app/file-upload/client/lib/fileUploadHandler.js
+++ b/app/file-upload/client/lib/fileUploadHandler.js
@@ -27,7 +27,12 @@ export const fileUploadHandler = (directive, meta, file) => {
Tracker.autorun(function() {
if (Meteor.userId()) {
- document.cookie = `rc_uid=${ escape(Meteor.userId()) }; path=/`;
- document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; path=/`;
+ let domain = Meteor.absoluteUrl().replace(/.*\/\//, '');
+ domain = domain.replace(/:.*$/, '');
+ domain = domain.replace(/\/$/, '');
+ domain = `.${ domain }`;
+
+ document.cookie = `rc_uid=${ escape(Meteor.userId()) }; domain=${ domain }; path=/`;
+ document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; domain=${ domain }; path=/`;
}
});
diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js
index 537732d1617f..9bf5b96922be 100644
--- a/app/lib/server/functions/deleteUser.js
+++ b/app/lib/server/functions/deleteUser.js
@@ -6,6 +6,7 @@ import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers }
import { hasRole, getUsersInRole } from '../../../authorization';
import { settings } from '../../../settings';
import { Notifications } from '../../../notifications';
+import { callbacks } from '../../../callbacks';
export const deleteUser = function(userId) {
const user = Users.findOneById(userId, {
@@ -97,6 +98,7 @@ export const deleteUser = function(userId) {
}
Users.removeById(userId); // Remove user from users database
+ callbacks.run('afterDeleteUser', { _id: userId });
// Refresh the peers list
FederationPeers.refreshPeers();
diff --git a/app/lib/server/functions/insertMessage.js b/app/lib/server/functions/insertMessage.js
index 35c7ccec1501..c673c130ee8c 100644
--- a/app/lib/server/functions/insertMessage.js
+++ b/app/lib/server/functions/insertMessage.js
@@ -40,6 +40,8 @@ const validateAttachmentsActions = (attachmentActions) => {
webview_height_ratio: String,
msg: String,
msg_in_chat_window: Boolean,
+ open_room_by_id: Boolean,
+ rid: String,
}));
};
diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js
index 290218f14e3d..0335dbac0943 100644
--- a/app/lib/server/functions/sendMessage.js
+++ b/app/lib/server/functions/sendMessage.js
@@ -63,6 +63,8 @@ const validateAttachmentsActions = (attachmentActions) => {
webview_height_ratio: String,
msg: String,
msg_in_chat_window: Boolean,
+ open_room_by_id: Boolean,
+ rid: String,
}));
};
diff --git a/app/lib/server/functions/setEmail.js b/app/lib/server/functions/setEmail.js
index 982194ae78e5..4a06a5622d3b 100644
--- a/app/lib/server/functions/setEmail.js
+++ b/app/lib/server/functions/setEmail.js
@@ -4,6 +4,7 @@ import s from 'underscore.string';
import { Users } from '../../../models';
import { hasPermission } from '../../../authorization';
import { RateLimiter, validateEmailDomain } from '../lib';
+import { callbacks } from '../../../callbacks';
import { checkEmailAvailability } from '.';
@@ -21,7 +22,7 @@ const _setEmail = function(userId, email, shouldSendVerificationEmail = true) {
const user = Users.findOneById(userId);
- // User already has desired username, return
+ // User already has desired email, return
if (user.emails && user.emails[0] && user.emails[0].address === email) {
return user;
}
@@ -37,6 +38,7 @@ const _setEmail = function(userId, email, shouldSendVerificationEmail = true) {
if (shouldSendVerificationEmail === true) {
Meteor.call('sendConfirmationEmail', user.email);
}
+ callbacks.run('afterUserEmailChange', { _id: user._id, email });
return user;
};
diff --git a/app/lib/server/functions/setRealName.js b/app/lib/server/functions/setRealName.js
index 40262fd612bf..23efbc52f085 100644
--- a/app/lib/server/functions/setRealName.js
+++ b/app/lib/server/functions/setRealName.js
@@ -6,6 +6,7 @@ import { settings } from '../../../settings';
import { Notifications } from '../../../notifications';
import { hasPermission } from '../../../authorization';
import { RateLimiter } from '../lib';
+import { callbacks } from '../../../callbacks';
export const _setRealName = function(userId, name) {
name = s.trim(name);
@@ -41,6 +42,7 @@ export const _setRealName = function(userId, name) {
username: user.username,
});
}
+ callbacks.run('afterUserRealNameChange', { _id: user._id, name });
return user;
};
diff --git a/app/lib/server/functions/setUsername.js b/app/lib/server/functions/setUsername.js
index baed79351cf8..cf2e0a2a1be4 100644
--- a/app/lib/server/functions/setUsername.js
+++ b/app/lib/server/functions/setUsername.js
@@ -8,6 +8,7 @@ import { Users, Messages, Subscriptions, Rooms, LivechatDepartmentAgents } from
import { hasPermission } from '../../../authorization';
import { RateLimiter } from '../lib';
import { Notifications } from '../../../notifications/server';
+import { callbacks } from '../../../callbacks';
import { checkUsernameAvailability, setUserAvatar, getAvatarSuggestionForUser } from '.';
@@ -92,6 +93,7 @@ export const _setUsername = function(userId, u) {
name: user.name,
username: user.username,
});
+ callbacks.run('afterUsernameChange', { _id: user._id, username });
return user;
};
diff --git a/app/message-action/client/messageAction.html b/app/message-action/client/messageAction.html
index 8d6c73566772..b212c51b062c 100644
--- a/app/message-action/client/messageAction.html
+++ b/app/message-action/client/messageAction.html
@@ -13,6 +13,11 @@
{{/if}}
+ {{#if open_room_by_id}}
+
+ {{/if}}
{{/if}}
{{#if text}}
{{#if url}}
@@ -25,6 +30,11 @@
{{text}}
{{/if}}
+ {{#if open_room_by_id}}
+
+ {{/if}}
{{/if}}
{{/if}}
diff --git a/app/ui-admin/client/admin.html b/app/ui-admin/client/admin.html
index 1eaa5c0c81a0..25d7118ba457 100644
--- a/app/ui-admin/client/admin.html
+++ b/app/ui-admin/client/admin.html
@@ -153,6 +153,14 @@
{{/if}}
{{/if}}
+ {{#if $eq type 'link'}}
+ {{#if hasChanges section}}
+ {{_ "Save_to_enable_this_action"}}
+ {{else}}
+
+ {{/if}}
+ {{/if}}
+
{{#if $eq type 'asset'}}
{{#if value.url}}