Support mailbox in the support and admin apps

pull/1/head
yflory 6 years ago
parent 22c9af6961
commit 93b4dac8bb

@ -90,12 +90,21 @@
button.btn { button.btn {
@button-bg: @colortheme_sidebar-button-bg; @button-bg: @colortheme_sidebar-button-bg;
@button-red-bg: @colortheme_sidebar-button-red-bg; @button-red-bg: @colortheme_sidebar-button-red-bg;
@button-alt-bg: @colortheme_sidebar-button-alt-bg;
background-color: @button-bg; background-color: @button-bg;
border-color: darken(@button-bg, 10%); border-color: darken(@button-bg, 10%);
color: white; color: white;
&:hover { &:hover {
background-color: darken(@button-bg, 10%); background-color: darken(@button-bg, 10%);
} }
&.btn-secondary {
background-color: @button-alt-bg;
border-color: darken(@button-alt-bg, 10%);
color: black;
&:hover {
background-color: darken(@button-alt-bg, 10%);
}
}
&.btn-danger { &.btn-danger {
background-color: @button-red-bg; background-color: @button-red-bg;
border-color: darken(@button-red-bg, 10%); border-color: darken(@button-red-bg, 10%);

@ -0,0 +1,69 @@
@import (reference) "./colortheme-all.less";
.support_main () {
@ticket-bg: #F7F7F7;
@msg-bg: #eee;
@fromme-bg: #ddd;
.cp-support-container {
.cp-support-list-ticket {
display: flex;
flex-flow: column;
background-color: @ticket-bg;
padding: 10px;
width: 1200px;
max-width: 90%;
margin: 5px auto;
.cp-support-list-message {
background-color: @msg-bg;
margin: 2px;
padding: 2px 5px;
.cp-support-fromme {
background-color: @fromme-bg;
}
.cp-support-showdata {
cursor: pointer;
background-color: @fromme-bg;
.cp-support-message-data {
display: none;
cursor: default;
}
}
.cp-support-message-time {
float: right;
}
pre {
margin-bottom: 0;
&.cp-support-message-content {
margin-top: 10px;
margin-bottom: 10px;
}
}
}
.cp-support-list-actions {
order: 3;
.cp-support-hide {
display: none;
}
}
.cp-support-form-container {
order: 2;
}
&.cp-support-list-closed {
.cp-support-list-actions {
display: block !important;
.cp-support-answer, .cp-support-close {
display: none;
}
.cp-support-hide {
display: inline;
}
}
.cp-support-form-container {
display: none !important;
}
}
}
}
}

@ -1,12 +1,13 @@
/* jshint esversion: 6, node: true */
const Nacl = require('tweetnacl'); const Nacl = require('tweetnacl');
const Crypto = require('crypto');
const keyPair = Nacl.box.keyPair(); const keyPair = Nacl.box.keyPair();
console.log(keyPair); console.log(keyPair);
console.log("You've just generated a new key pair for your support mailbox."); console.log("You've just generated a new key pair for your support mailbox.");
console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server.") console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server.");
console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel."); console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel.");
console.log("You will have to send the private key to each administrator manually so that they can add it to their account."); console.log("You will have to send the private key to each administrator manually so that they can add it to their account.");
console.log(); console.log();

@ -1,5 +1,6 @@
@import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/support.less';
&.cp-app-admin { &.cp-app-admin {
@ -9,6 +10,11 @@
@color: @colortheme_admin-color @color: @colortheme_admin-color
); );
.sidebar-layout_main(); .sidebar-layout_main();
.support_main();
.cp-hidden {
display: none !important;
}
display: flex; display: flex;
flex-flow: column; flex-flow: column;

@ -9,6 +9,8 @@ define([
'/customize/messages.js', '/customize/messages.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js',
'/support/ui.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -23,7 +25,9 @@ define([
h, h,
Messages, Messages,
UI, UI,
Util Util,
Hash,
Support
) )
{ {
var APP = {}; var APP = {};
@ -41,6 +45,10 @@ define([
'cp-admin-active-pads', 'cp-admin-active-pads',
'cp-admin-registered', 'cp-admin-registered',
'cp-admin-disk-usage', 'cp-admin-disk-usage',
],
'support': [
'cp-admin-support-list',
'cp-admin-support-init'
] ]
}; };
@ -94,7 +102,6 @@ define([
sFrameChan.query('Q_ADMIN_RPC', { sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ACTIVE_SESSIONS', cmd: 'ACTIVE_SESSIONS',
}, function (e, data) { }, function (e, data) {
console.log(e, data);
var total = data[0]; var total = data[0];
var ips = data[1]; var ips = data[1];
$div.append(h('pre', total + ' (' + ips + ')')); $div.append(h('pre', total + ' (' + ips + ')'));
@ -160,6 +167,111 @@ define([
return $div; return $div;
}; };
var supportKey = ApiConfig.supportMailbox;
create['support-list'] = function () {
if (!supportKey || !APP.privateKey) { return; }
var $div = makeBlock('support-list');
$div.addClass('cp-support-container');
var hashesById = {};
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
onMessage: function (data) {
/*
Get ID of the ticket
If we already have a div for this ID
Push the message to the end of the ticket
If it's a new ticket ID
Make a new div for this ID
*/
var msg = data.content.msg;
var hash = data.content.hash;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(Support.makeCloseMessage(common, content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = Support.makeTicket($div, common, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
if (err) {
error = true;
console.error(err);
}
});
});
if (!error) { $ticket.remove(); }
});
}
$ticket.append(Support.makeMessage(common, content, hash, true));
}
});
return $div;
};
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);
};
create['support-init'] = function () {
var $div = makeBlock('support-init');
if (!supportKey) {
$div.append(h('p', Messages.admin_supportInitHelp || "Your server is not configured to have a support mailbox. If you want a support mailbox to receive messages from your users, you should ask your server administrator to run the script located in './scripts/generate-admin-keys.js', store the public key in the 'config.js' file, and send you the private key.")); // XXX
return $div;
}
if (!APP.privateKey || !checkAdminKey(APP.privateKey)) {
$div.append(h('p', Messages.admin_supportInitPrivate || "Your CryptPad instance is configured to use a support mailbox but your account doesn't have the correct private key to access it. Please use the following form to add or update the private key to your account")); // XXX
var error = h('div.cp-admin-support-error');
var input = h('input.cp-admin-add-private-key');
var button = h('button.btn.btn-primary', Messages.admin_supportAddKey || 'add key'); // XXX
if (APP.privateKey && !checkAdminKey(APP.privateKey)) {
$(error).text(Messages.admin_supportAddError || 'invalid'); // XXX
}
$div.append(h('div', [
error,
input,
button
]));
$(button).click(function () {
var key = $(input).val();
if (!checkAdminKey(key)) {
$(input).val('');
return void $(error).text(Messages.admin_supportAddError || 'invalid'); // XXX
}
sFrameChan.query("Q_ADMIN_MAILBOX", key, function () {
console.log(key);
console.log(arguments);
APP.privateKey = key;
console.log('ok');
$('.cp-admin-support-init').hide();
APP.$rightside.append(create['support-list']()); // TODO: check?
});
});
return $div;
}
return;
};
var hideCategories = function () { var hideCategories = function () {
APP.$rightside.find('> div').hide(); APP.$rightside.find('> div').hide();
}; };
@ -180,6 +292,7 @@ define([
var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); } if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); }
if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); } if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
if (key === 'support') { $category.append($('<span>', {'class': 'fa fa-life-ring'})); }
if (key === active) { if (key === active) {
$category.addClass('cp-leftside-active'); $category.addClass('cp-leftside-active');
@ -236,6 +349,7 @@ define([
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden'); return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
} }
APP.privateKey = privateData.supportPrivateKey;
APP.origin = privateData.origin; APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly; APP.readOnly = privateData.readOnly;

@ -38,6 +38,9 @@ define([
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
var addRpc = function (sframeChan, Cryptpad/*, Utils*/) { var addRpc = function (sframeChan, Cryptpad/*, Utils*/) {
// Adding a new avatar from the profile: pin it and store it in the object // Adding a new avatar from the profile: pin it and store it in the object
sframeChan.on('Q_ADMIN_MAILBOX', function (data, cb) {
Cryptpad.addAdminMailbox(data, cb);
});
sframeChan.on('Q_ADMIN_RPC', function (data, cb) { sframeChan.on('Q_ADMIN_RPC', function (data, cb) {
Cryptpad.adminRpc(data, cb); Cryptpad.adminRpc(data, cb);
}); });

@ -89,6 +89,18 @@ define([
if (!publicKey) { return; } if (!publicKey) { return; }
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16)); return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
}; };
Hash.getBoxPublicFromSecret = function (priv) {
if (!priv) { return; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return Hash.encodeBase64(pair.publicKey);
};
Hash.checkBoxKeyPair = function (priv, pub) {
if (!pub || !priv) { return false; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return pub === Hash.encodeBase64(pair.publicKey);
};
Hash.createRandomHash = function (type, password) { Hash.createRandomHash = function (type, password) {
var cryptor; var cryptor;

@ -620,6 +620,9 @@ define([
common.adminRpc = function (data, cb) { common.adminRpc = function (data, cb) {
postMessage("ADMIN_RPC", data, cb); postMessage("ADMIN_RPC", data, cb);
}; };
common.addAdminMailbox = function (data, cb) {
postMessage("ADMIN_ADD_MAILBOX", data, cb);
};
// Network // Network
common.onNetworkDisconnect = Util.mkEvent(); common.onNetworkDisconnect = Util.mkEvent();

@ -479,7 +479,8 @@ define([
thumbnails: disableThumbnails === false, thumbnails: disableThumbnails === false,
isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])), isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])),
support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']), support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
pendingFriends: store.proxy.friends_pending || {} pendingFriends: store.proxy.friends_pending || {},
supportPrivateKey: Util.find(store.proxy, ['mailboxes', 'supportadmin', 'keys', 'curvePrivate'])
} }
}; };
cb(JSON.parse(JSON.stringify(metadata))); cb(JSON.parse(JSON.stringify(metadata)));
@ -1060,6 +1061,26 @@ define([
cb(res); cb(res);
}); });
}; };
Store.addAdminMailbox = function (clientId, data, cb) {
var priv = data;
var pub = Hash.getBoxPublicFromSecret(priv);
if (!priv || !pub) { return void cb({error: 'EINVAL'}); }
var channel = Hash.getChannelIdFromKey(pub);
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
var box = mailboxes.supportadmin = {
channel: channel,
viewed: [],
lastKnownHash: '',
keys: {
curvePublic: pub,
curvePrivate: priv
}
};
store.mailbox.open('supportadmin', box, function () {
console.log('ready');
});
onSync(cb);
};
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
/////////////////////// PAD ////////////////////////////////////// /////////////////////// PAD //////////////////////////////////////

@ -10,6 +10,7 @@ define([
var TYPES = [ var TYPES = [
'notifications', 'notifications',
'supportadmin',
'support' 'support'
]; ];
var BLOCKING_TYPES = [ var BLOCKING_TYPES = [
@ -96,10 +97,10 @@ proxy.mailboxes = {
network.join(user.channel).then(function (wc) { network.join(user.channel).then(function (wc) {
wc.bcast(ciphertext).then(function () { wc.bcast(ciphertext).then(function () {
cb(); cb();
wc.leave();
// If we've just sent a message to one of our mailboxes, we have to trigger the handler manually // If we've just sent a message to one of our mailboxes, we have to trigger the handler manually
// (the server won't send back our message to us) // (the server won't send back our message to us)
// If it isn't one of our mailboxes, we can close it now
var box; var box;
if (Object.keys(ctx.boxes).some(function (t) { if (Object.keys(ctx.boxes).some(function (t) {
var _box = ctx.boxes[t]; var _box = ctx.boxes[t];
@ -110,6 +111,8 @@ proxy.mailboxes = {
})) { })) {
var hash = ciphertext.slice(0, 64); var hash = ciphertext.slice(0, 64);
box.onMessage(text, null, null, null, hash, user.curvePublic); box.onMessage(text, null, null, null, hash, user.curvePublic);
} else {
wc.leave();
} }
}); });
}, function (err) { }, function (err) {
@ -200,7 +203,7 @@ proxy.mailboxes = {
if (!Crypto.Mailbox) { if (!Crypto.Mailbox) {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes."); return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
} }
var keys = getMyKeys(ctx); var keys = m.keys || getMyKeys(ctx);
if (!keys) { return void console.error("missing asymmetric encryption keys"); } if (!keys) { return void console.error("missing asymmetric encryption keys"); }
var crypto = Crypto.Mailbox.createEncryptor(keys); var crypto = Crypto.Mailbox.createEncryptor(keys);
var cfg = { var cfg = {
@ -364,6 +367,7 @@ proxy.mailboxes = {
Object.keys(mailboxes).forEach(function (key) { Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; } if (TYPES.indexOf(key) === -1) { return; }
var m = mailboxes[key]; var m = mailboxes[key];
console.log(key, m);
if (BLOCKING_TYPES.indexOf(key) === -1) { if (BLOCKING_TYPES.indexOf(key) === -1) {
openChannel(ctx, key, m, function () { openChannel(ctx, key, m, function () {
@ -386,6 +390,11 @@ proxy.mailboxes = {
}); });
}; };
mailbox.open = function (key, m, cb) {
if (TYPES.indexOf(key) === -1) { return; }
openChannel(ctx, key, m, cb);
};
mailbox.dismiss = function (data, cb) { mailbox.dismiss = function (data, cb) {
dismiss(ctx, data, '', cb); dismiss(ctx, data, '', cb);
}; };

@ -84,6 +84,7 @@ define([
DELETE_ACCOUNT: Store.deleteAccount, DELETE_ACCOUNT: Store.deleteAccount,
// Admin // Admin
ADMIN_RPC: Store.adminRpc, ADMIN_RPC: Store.adminRpc,
ADMIN_ADD_MAILBOX: Store.addAdminMailbox,
}; };
Rpc.query = function (cmd, data, cb) { Rpc.query = function (cmd, data, cb) {

@ -123,7 +123,7 @@ define([
var onMessage = function (data) { var onMessage = function (data) {
// data = { type: 'type', content: {msg: 'msg', hash: 'hash'} } // data = { type: 'type', content: {msg: 'msg', hash: 'hash'} }
console.log(data.content); console.log(data.type, data.content);
pushMessage(data); pushMessage(data);
if (!history[data.type]) { history[data.type] = []; } if (!history[data.type]) { history[data.type] = []; }
history[data.type].push(data.content); history[data.type].push(data.content);

@ -1,5 +1,6 @@
@import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/support.less';
&.cp-app-support { &.cp-app-support {
.framework_min_main( .framework_min_main(
@ -8,6 +9,7 @@
@color: @colortheme_support-color @color: @colortheme_support-color
); );
.sidebar-layout_main(); .sidebar-layout_main();
.support_main();
.cp-hidden { .cp-hidden {
display: none !important; display: none !important;

@ -9,8 +9,8 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/customize/messages.js', '/customize/messages.js',
'/common/hyperscript.js', '/common/hyperscript.js',
'/support/ui.js',
'/api/config', '/api/config',
'/common/common-feedback.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -26,17 +26,15 @@ define([
Hash, Hash,
Messages, Messages,
h, h,
ApiConfig, Support,
Feedback ApiConfig
) )
{ {
var saveAs = window.saveAs;
var APP = window.APP = {}; var APP = window.APP = {};
var common; var common;
var metadataMgr; var metadataMgr;
var privateData; var privateData;
var sframeChan;
var categories = { var categories = {
'tickets': [ 'tickets': [
@ -47,9 +45,9 @@ define([
], ],
}; };
var supportKey = ApiConfig.supportMailbox; // XXX curvePublic var supportKey = ApiConfig.supportMailbox;
var supportChannel = Hash.getChannelIdFromKey(supportKey); // XXX var supportChannel = Hash.getChannelIdFromKey(supportKey);
if (true || !supportKey || !supportChannel) { if (!supportKey || !supportChannel) {
categories = { categories = {
'tickets': [ 'tickets': [
'cp-support-disabled' 'cp-support-disabled'
@ -75,138 +73,14 @@ define([
return $div; return $div;
}; };
var showError = function (form, msg) {
if (!msg) {
return void $(form).find('.cp-support-form-error').text('').hide();
}
$(form).find('.cp-support-form-error').text(msg).show();
};
var makeForm = function (cb, title) {
var button;
if (typeof(cb) === "function") {
button = h('button.btn.btn-primary.cp-support-list-send', Messages.support_send || 'Send'); // XXX
$(button).click(cb);
}
var content = [
h('hr'),
h('div.cp-support-form-error'),
h('label' + (title ? '.cp-hidden' : ''), Messages.support_formTitle || 'title...'), // XXX
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitlePlaceholder || 'title here...', // XXX
value: title
}),
cb ? undefined : h('br'),
h('label', Messages.support_formMessage || 'content...'), // XXX
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessagePlaceholder || 'describe your problem here...' // XXX
}),
h('hr'),
button
];
return h('div.cp-support-form-container', content);
};
var sendForm = function (id, form) {
var user = metadataMgr.getUserData();
privateData = metadataMgr.getPrivateData();
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var title = $title.val();
if (!title) {
return void showError(form, Messages.support_formTitleError || 'title error'); // XXX
}
var content = $content.val();
if (!content) {
return void showError(form, Messages.support_formContentError || 'content error'); // XXX
}
// Success: hide any error
showError(form, null);
$content.val('');
$title.val('');
common.mailbox.sendTo('TICKET', {
sender: {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic
},
title: title,
message: content,
id: id
}, {
channel: supportChannel,
curvePublic: supportKey
});
common.mailbox.sendTo('TICKET', {
sender: {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic
},
title: title,
message: content,
id: id
}, {
channel: privateData.support,
curvePublic: user.curvePublic
});
return true;
};
// List existing (open?) tickets // List existing (open?) tickets
create['list'] = function () { create['list'] = function () {
var key = 'list'; var key = 'list';
var $div = makeBlock(key); var $div = makeBlock(key);
$div.addClass('cp-support-container');
var makeTicket = function (content) { var hashesById = {};
var ticketTitle = content.id + ' - ' + content.title;
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer || 'Answer'); // XXX
var $ticket = $(h('div.cp-support-list-ticket', {
'data-id': content.id
}, [
h('h2', ticketTitle),
h('div.cp-support-list-actions', answer)
]));
$(answer).click(function () {
$div.find('.cp-support-form-container').remove();
$div.find('.cp-support-answer').show();
$(answer).hide();
var form = makeForm(function () {
var sent = sendForm(content.id, form);
if (sent) {
$(answer).show();
$(form).remove();
}
}, content.title);
$ticket.append(form);
});
$div.append($ticket);
return $ticket;
};
var makeMessage = function (content, hash) {
// Check content.sender to see if it comes from us or from an admin
// XXX admins should send their personal public key?
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
return h('div.cp-support-list-message', [
h('p.cp-support-message-from' + fromMe ? '.cp-support-fromme' : '',
//Messages._getKey('support_from', [content.sender.name])), // XXX
[h('b', 'From: '), content.sender.name]),
h('pre.cp-support-message-content', content.message)
]);
};
// Register to the "support" mailbox // Register to the "support" mailbox
common.mailbox.subscribe(['support'], { common.mailbox.subscribe(['support'], {
@ -220,23 +94,39 @@ define([
*/ */
var msg = data.content.msg; var msg = data.content.msg;
var hash = data.content.hash; var hash = data.content.hash;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') { if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins... // A ticket has been closed by the admins...
// TODO: add a "closed" class to the ticket in the UI if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(Support.makeCloseMessage(common, content, hash));
return;
} }
if (msg.type !== 'TICKET') { return; } if (msg.type !== 'TICKET') { return; }
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
if (!$ticket.length) { if (!$ticket.length) {
$ticket = makeTicket(content); $ticket = Support.makeTicket($div, common, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
if (err) {
error = true;
console.error(err);
}
});
});
if (!error) { $ticket.remove(); }
});
} }
$ticket.append(makeMessage(content, hash)); $ticket.append(Support.makeMessage(common, content, hash, false));
},
onViewed: function (data) {
// Remove the ticket with this hash
// If the ticket div is empty, remove the ticket div
} }
}); });
return $div; return $div;
@ -247,14 +137,20 @@ define([
var key = 'form'; var key = 'form';
var $div = makeBlock(key, true); var $div = makeBlock(key, true);
var form = makeForm(); var form = Support.makeForm();
$div.find('button').before(form); $div.find('button').before(form);
var id = Util.uid(); var id = Util.uid();
$div.find('button').click(function () { $div.find('button').click(function () {
var sent = sendForm(id, form); var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
var sent = Support.sendForm(common, id, form, {
channel: privateData.support,
curvePublic: user.curvePublic
});
if (sent) { if (sent) {
$('.cp-sidebarlayout-category[data-category="tickets"]').click(); $('.cp-sidebarlayout-category[data-category="tickets"]').click();
} }
@ -338,7 +234,7 @@ define([
APP.$toolbar = $('#cp-toolbar'); APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container); APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel(); var sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor()); sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
createToolbar(); createToolbar();

@ -36,8 +36,6 @@ define([
}; };
window.addEventListener('message', onMsg); window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
var addRpc = function (sframeChan, Cryptpad, Utils) {
};
var category; var category;
if (window.location.hash) { if (window.location.hash) {
category = window.location.hash.slice(1); category = window.location.hash.slice(1);
@ -48,7 +46,6 @@ define([
}; };
SFCommonO.start({ SFCommonO.start({
noRealtime: true, noRealtime: true,
addRpc: addRpc,
addData: addData addData: addData
}); });
}); });

@ -0,0 +1,203 @@
define([
'jquery',
'/api/config',
'/common/hyperscript.js',
'/common/common-hash.js',
'/common/common-util.js',
'/customize/messages.js',
], function ($, ApiConfig, h, Hash, Util, Messages) {
var showError = function (form, msg) {
if (!msg) {
return void $(form).find('.cp-support-form-error').text('').hide();
}
$(form).find('.cp-support-form-error').text(msg).show();
};
var send = function (common, id, type, data, dest) {
var supportKey = ApiConfig.supportMailbox;
var supportChannel = Hash.getChannelIdFromKey(supportKey);
var metadataMgr = common.getMetadataMgr();
var user = metadataMgr.getUserData();
var privateData = metadataMgr.getPrivateData();
data = data || {};
data.sender = {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic
};
data.id = id;
data.time = +new Date();
// Send the message to the admin mailbox and to the user mailbox
common.mailbox.sendTo(type, data, {
channel: supportChannel,
curvePublic: supportKey
});
common.mailbox.sendTo(type, data, {
channel: dest.channel,
curvePublic: dest.curvePublic
});
};
var sendForm = function (common, id, form, dest) {
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var title = $title.val();
if (!title) {
return void showError(form, Messages.support_formTitleError || 'title error'); // XXX
}
var content = $content.val();
if (!content) {
return void showError(form, Messages.support_formContentError || 'content error'); // XXX
}
// Success: hide any error
showError(form, null);
$content.val('');
$title.val('');
send(common, id, 'TICKET', {
title: title,
message: content,
}, dest);
return true;
};
var makeForm = function (cb, title) {
var button;
if (typeof(cb) === "function") {
button = h('button.btn.btn-primary.cp-support-list-send', Messages.support_send || 'Send'); // XXX
$(button).click(cb);
}
var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined;
var content = [
h('hr'),
h('div.cp-support-form-error'),
h('label' + (title ? '.cp-hidden' : ''), Messages.support_formTitle || 'title...'), // XXX
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitlePlaceholder || 'title here...', // XXX
value: title || ''
}),
cb ? undefined : h('br'),
h('label', Messages.support_formMessage || 'content...'), // XXX
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessagePlaceholder || 'describe your problem here...' // XXX
}),
h('hr'),
button,
cancel
];
var form = h('div.cp-support-form-container', content);
$(cancel).click(function () {
$(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').show();
$(form).remove();
});
return form;
};
var makeTicket = function ($div, common, content, onHide) {
var ticketTitle = content.id + ' - ' + content.title;
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer || 'Answer'); // XXX
var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close || 'Close'); // XXX
var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove || 'Remove'); // XXX
var actions = h('div.cp-support-list-actions', [
answer,
close,
hide
]);
var $ticket = $(h('div.cp-support-list-ticket', {
'data-id': content.id
}, [
h('h2', ticketTitle),
actions
]));
$(close).click(function () {
send(common, content.id, 'CLOSE', {}, content.sender);
});
$(hide).click(function () {
if (typeof(onHide) !== "function") { return; }
onHide();
});
$(answer).click(function () {
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(function () {
var sent = sendForm(common, content.id, form, content.sender);
if (sent) {
$(actions).show();
$(form).remove();
}
}, content.title);
$ticket.append(form);
});
$div.append($ticket);
return $ticket;
};
var makeMessage = function (common, content, hash, isAdmin) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
// Check content.sender to see if it comes from us or from an admin
// XXX admins should send their personal public key?
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
var userData = h('div.cp-support-showdata', [
Messages.support_showData || 'Show/hide data', // XXX
h('pre.cp-support-message-data', JSON.stringify(content.sender, 0, 2))
]);
$(userData).click(function () {
$(userData).find('pre').toggle();
});
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''),
//Messages._getKey('support_from', [content.sender.name, new Date(content.time)])), // XXX
[h('b', 'From: '), content.sender.name, h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')]),
h('pre.cp-support-message-content', content.message),
isAdmin ? userData : undefined,
]);
};
var makeCloseMessage = function (common, content, hash) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''),
//Messages._getKey('support_from', [content.sender.name, new Date(content.time)])), // XXX
[h('b', 'From: '), content.sender.name, h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')]),
h('pre.cp-support-message-content', Messages.support_closed || 'Ticket closed...') // XXX
]);
};
return {
sendForm: sendForm,
makeForm: makeForm,
makeTicket: makeTicket,
makeMessage: makeMessage,
makeCloseMessage: makeCloseMessage
};
});
Loading…
Cancel
Save