New support: create ticket from admin side

This commit is contained in:
yflory 2024-02-28 17:25:32 +01:00
parent 47284dc1ec
commit 0056603486
9 changed files with 242 additions and 51 deletions

View file

@ -91,6 +91,12 @@
.cp-support-message-data {
display: none;
cursor: default;
position: relative;
.cp-support-copydata {
position: absolute;
right: 0;
margin-right: 0px;
}
}
}
.cp-support-message-time {

View file

@ -50,7 +50,7 @@ define([
icon ? h('i', { 'class': icon }) : undefined,
h('span', text)
]);
}
};
blocks.nav = (buttons) => {
return h('nav', buttons);
};
@ -221,12 +221,16 @@ define([
var isActive = key === active ? '.cp-leftside-active' : '';
var item = h('li.cp-sidebarlayout-category'+isActive, {
'role': 'menuitem',
'tabindex': 0
'tabindex': 0,
'data-category': key
}, [
icon,
Messages[`${app}_cat_${key}`] || key,
]);
var $item = $(item).appendTo(container);
category.open = function () {
$item.click();
};
Util.onClickEnter($item, function () {
if (!Array.isArray(category.content) && category.onClick) {
@ -245,6 +249,10 @@ define([
$leftside.append(container);
};
sidebar.openCategory = name => {
$(`.cp-sidebarlayout-category[data-category="${name}"]`).click();
};
sidebar.disableItem = (key) => {
$(items[key]).remove();
delete items[key];

View file

@ -846,15 +846,17 @@ define([
handlers['NEW_TICKET'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
content.time = data.time;
var i = 0;
var handle = function () {
var support = Util.find(ctx, ['store', 'modules', 'support']);
if (!support && i++ < 100) { setTimeout(handle, 600); }
if (!support) { return; }
support.addAdminTicket(content, cb);
};
handle();
if (!content.time) { content.time = data.time; }
var support = Util.find(ctx, ['store', 'modules', 'support']);
// Admin to user
if (content.isAdmin) {
support.addUserTicket(content, cb);
}
// User to admin
support.addAdminTicket(content, cb);
};
var supportNotif, adminSupportNotif;
handlers['NOTIF_TICKET'] = function (ctx, box, data, cb) {

View file

@ -39,6 +39,7 @@ define([
if (res.error) { console.error(res); }
});
}
// XXX XXX XXX no need for the "support" mailbox anymore
if (!mailboxes['support'] && ctx.loggedIn) {
mailboxes.support = {
channel: Hash.createChannelId(),

View file

@ -41,6 +41,7 @@ define([
if (isAdmin) {
return ctx.adminRdyEvt.reg(() => {
cb(null, {
supportKey: supportKey,
myCurve: data.adminCurvePrivate || Util.find(ctx.store.proxy, [
'mailboxes', 'supportteam', 'keys', 'curvePrivate']),
theirPublic: data.curvePublic,
@ -50,8 +51,9 @@ define([
}
cb(null, {
theirPublic: data.curvePublic || supportKey, // old tickets may use deprecated key
supportKey: supportKey,
myCurve: ctx.store.proxy.curvePrivate,
theirPublic: data.curvePublic || supportKey, // old tickets may use deprecated key
notifKey: supportKey
});
});
@ -108,7 +110,7 @@ define([
});
};
var makeTicket = function (ctx, data, cId, cb) {
var makeTicket = function (ctx, data, isAdmin, cb) {
var mailbox = Util.find(ctx, [ 'store', 'mailbox' ]);
var anonRpc = Util.find(ctx, [ 'store', 'anon_rpc' ]);
if (!mailbox) { return void cb({error: 'E_NOT_READY'}); }
@ -117,34 +119,57 @@ define([
var channel = data.channel;
var title = data.title;
var ticket = data.ticket;
var supportKey, myCurve;
var supportKey, theirPublic, myCurve, notifKey;
var time = +new Date();
nThen((waitFor) => {
// Send ticket to the admins and call back
getKeys(ctx, false, data, waitFor((err, obj) => {
getKeys(ctx, isAdmin, data, waitFor((err, obj) => {
if (err) {
waitFor.abort();
return void cb({error: err});
}
supportKey = obj.theirPublic;
supportKey = obj.supportKey;
theirPublic = obj.theirPublic;
myCurve = obj.myCurve;
notifKey = obj.notifKey;
}));
}).nThen((waitFor) => {
// Create ticket mailbox
var keys = Crypto.Curve.deriveKeys(supportKey, myCurve);
var keys = Crypto.Curve.deriveKeys(theirPublic, myCurve);
var crypto = Crypto.Curve.createEncryptor(keys);
var text = JSON.stringify(ticket);
var ciphertext = crypto.encrypt(text);
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
channel,
ciphertext
], waitFor((err) => {
], waitFor((err, res) => {
if (err) {
waitFor.abort();
return void cb({error: err});
}
time = res && res[0];
}));
}).nThen((waitFor) => {
// Store in our worker
if (!isAdmin) { return; }
// ADMIN: Store in chainpad
var doc = ctx.adminDoc.proxy;
var active = doc.tickets.active;
if (active[channel]) {
waitFor.abort();
return void cb({error: 'EEXISTS'});
}
active[channel] = {
title: ticket.title,
premium: false,
time: time,
author: data.name,
supportKey: supportKey, // Store current support key
lastAdmin: true,
authorKey: data.curvePublic
};
}).nThen((waitFor) => {
if (isAdmin) { return; }
// USER: Store in my worker
ctx.supportData[channel] = {
time: +new Date(),
title: title,
@ -152,31 +177,41 @@ define([
};
ctx.Store.onSync(null, waitFor());
}).nThen(() => {
var supportChannel = Hash.getChannelIdFromKey(supportKey);
// XXX create tickt as an admin
// XXX isAdmin
var notifChannel = isAdmin ? data.notifications
: Hash.getChannelIdFromKey(supportKey);
// First message to deal with the new ticket (store it in the list)
mailbox.sendTo('NEW_TICKET', {
title: title,
channel: channel,
premium: Util.find(ctx, ['store', 'account', 'plan'])
time,
isAdmin,
supportKey: supportKey, // Store current support key
premium: isAdmin ? '' : Util.find(ctx, ['store', 'account', 'plan']),
user: Util.find(data.ticket, ['sender', 'curvePublic']) ? undefined : {
supportTeam: true
}
}, {
channel: supportChannel,
curvePublic: supportKey
channel: notifChannel,
curvePublic: theirPublic
}, (obj) => {
console.error(obj);
// Don't store the ticket in case of error
if (obj && obj.error) { delete ctx.supportData[channel]; }
cb(obj);
});
// XXX create tickt as an admin
// XXX isAdmin
// Second message is only a notification to warn the user/admins
mailbox.sendTo('NOTIF_TICKET', {
title: title,
channel: channel,
time,
isAdmin,
isNewTicket: true,
user: Util.find(data.ticket, ['sender', 'curvePublic']) ? undefined : {
supportTeam: true
}
}, {
channel: supportChannel,
curvePublic: supportKey
channel: notifChannel,
curvePublic: theirPublic
}, () => {});
});
};
@ -403,6 +438,9 @@ define([
cb({tickets: all});
});
};
var makeMyTicket = function (ctx, data, cId, cb) {
makeTicket(ctx, data, false, cb);
};
var replyMyTicket = function (ctx, data, cId, cb) {
replyTicket(ctx, data, false, (err) => {
if (err) { return void cb({error: err}); }
@ -479,6 +517,13 @@ define([
});
};
var makeTicketAdmin = function (ctx, data, cId, cb) {
if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); }
ctx.adminRdyEvt.reg(() => {
makeTicket(ctx, data, true, cb);
});
};
var replyTicketAdmin = function (ctx, data, cId, cb) {
if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); }
let supportKey = data.supportKey;
@ -561,12 +606,12 @@ define([
let supportKey;
nThen((waitFor) => {
// Send ticket to the admins and call back
getKeys(ctx, false, data, waitFor((err, obj) => {
getKeys(ctx, true, data, waitFor((err, obj) => {
if (err) {
waitFor.abort();
return void cb(true);
}
supportKey = obj.theirPublic;
supportKey = obj.supportKey;
}));
}).nThen(() => {
// random timeout to avoid duplication wiht multiple admins
@ -581,7 +626,7 @@ define([
premium: data.premium,
time: data.time,
author: data.user && data.user.displayName,
supportKey: supportKey, // Store current support key
supportKey: data.supportKey || supportKey, // Store current support key
authorKey: data.user && data.user.curvePublic
};
Realtime.whenRealtimeSyncs(ctx.adminDoc.realtime, function () {
@ -629,6 +674,19 @@ define([
cb(exists);
});
};
var addUserTicket = function (ctx, data, cb) {
if (!ctx.supportData) { return void cb(true); }
let channel = data.channel;
ctx.supportData[channel] = {
time: data.time,
title: data.title,
curvePublic: data.supportKey // Old tickets still use previous keys
};
ctx.Store.onSync(null, function () {
cb(true);
});
};
var updateUserTicket = function (ctx, data) {
notifyClient(ctx, false, 'UPDATE_TICKET', data.channel);
if (data.isClose) {
@ -1013,6 +1071,9 @@ define([
support.checkAdminTicket = function (content, cb) {
checkAdminTicket(ctx, content, cb);
};
support.addUserTicket = function (content, cb) {
addUserTicket(ctx, content, cb);
};
support.updateUserTicket = function (content) {
updateUserTicket(ctx, content);
};
@ -1021,7 +1082,7 @@ define([
var data = obj.data;
// User commands
if (cmd === 'MAKE_TICKET') {
return void makeTicket(ctx, data, clientId, cb);
return void makeMyTicket(ctx, data, clientId, cb);
}
if (cmd === 'GET_MY_TICKETS') {
return void getMyTickets(ctx, data, clientId, cb);
@ -1036,6 +1097,9 @@ define([
return void deleteMyTicket(ctx, data, clientId, cb);
}
// Moderator commands
if (cmd === 'MAKE_TICKET_ADMIN') {
return void makeTicketAdmin(ctx, data, clientId, cb);
}
if (cmd === 'LIST_TICKETS_ADMIN') {
return void listTicketsAdmin(ctx, data, clientId, cb);
}

View file

@ -49,7 +49,28 @@
margin-left: 10px;
}
}
}
.cp-moderation-userdata {
display: flex;
align-items: center;
input, textarea {
width: 25rem !important;
max-width: 25rem;
}
textarea {
margin: 0;
height: 5rem;
}
.cp-moderation-userdata-inputs {
display: flex;
flex-flow: column;
margin-right: 1rem;
:first-child {
margin-top: 0 !important;
}
}
}
}

View file

@ -59,8 +59,17 @@ define([
Messages.support_notificationsTitle = "Disable notifications";
Messages.support_notificationsHint = "Check this option to disable notifications on new or updated ticket";
Messages.support_openTicketTitle = "Open a ticket for a user";
Messages.support_openTicketHint = "Create a ticket for a user. They will receive a CryptPad notification to warn them. You can copy their user data from an existing support ticket, using the Copy button in the user data.";
Messages.support_userChannel = "User's notifications channel ID";
Messages.support_userKey = "User's curvePublic key";
Messages.support_invalChan = "Invalid notifications channel";
Messages.support_pasteUserData = "Paste user data here";
var andThen = function (common, $container, linkedTicket) {
const sidebar = Sidebar.create(common, 'support', $container);
const blocks = sidebar.blocks;
// Support panel functions
let open = [];
@ -177,7 +186,7 @@ define([
refreshAll();
});
};
const onMove = function (ticket, channel, data) {
const onMove = function (ticket, channel) {
APP.module.execCommand('MOVE_TICKET_ADMIN', {
channel: channel,
from: type,
@ -253,6 +262,12 @@ define([
'notifications'
]
},
'ticket': {
icon: undefined,
content: [
'open-ticket'
]
},
'refresh': {
icon: undefined,
onClick: refreshAll
@ -294,6 +309,74 @@ define([
}
});
sidebar.addItem('open-ticket', cb => {
let form = APP.support.makeForm();
let inputName = blocks.input({type: 'text', readonly: true});
let inputChan = blocks.input({type: 'text', readonly: true});
let inputKey = blocks.input({type: 'text', readonly: true});
let labelName = blocks.labelledInput(Messages.login_username, inputName);
let labelChan = blocks.labelledInput(Messages.support_userChannel, inputChan);
let labelKey = blocks.labelledInput(Messages.support_userKey, inputKey);
let send = blocks.button('primary', 'fa-paper-plane', Messages.support_formButton);
let nav = blocks.nav([send]);
let paste = blocks.textArea({
placeholder: Messages.support_pasteUserData
});
let inputs = h('div.cp-moderation-userdata-inputs', [ labelName, labelChan, labelKey ]);
let userData = h('div.cp-moderation-userdata', [inputs , paste]);
let $paste = $(paste).on('input', () => {
let text = $paste.val().trim();
let parsed = Util.tryParse(text);
$paste.val('');
if (!parsed || !parsed.name || !parsed.notifications || !parsed.curvePublic) {
return void UI.warn(Messages.error);
}
$(inputName).val(parsed.name);
$(inputChan).val(parsed.notifications);
$(inputKey).val(parsed.curvePublic);
$paste.hide();
});
[inputName, inputChan, inputKey].forEach(input => {
$(input).on('input', () => { $paste.show(); });
});
let $send = $(send);
Util.onClickEnter($send, function () {
let name = $(inputName).val().trim();
let chan = $(inputChan).val().trim();
let key = $(inputKey).val().trim();
let data = APP.support.getFormData(form);
if (!name) { return void UI.warn(Messages.login_invalUser); }
if (!Hash.isValidChannel(chan)) { return void UI.warn(Messages.support_invalChan); }
if (key.length !== 44) { return void UI.warn(Messages.admin_invalKey); }
$send.attr('disabled', 'disabled');
APP.module.execCommand('MAKE_TICKET_ADMIN', {
name: name,
notifications: chan,
curvePublic: key,
channel: Hash.createChannelId(),
title: data.title,
ticket: data
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
refreshAll();
sidebar.openCategory('open');
});
});
let div = blocks.form([userData, form], nav);
cb(div);
});
sidebar.makeLeftside(categories);
};

View file

@ -295,8 +295,6 @@ define([
var form = APP.support.makeForm();
var id = Util.uid();
$div.find('button').click(function () {
var data = APP.support.getFormData(form);
APP.supportModule.execCommand('MAKE_TICKET', {
@ -308,7 +306,6 @@ define([
console.error(obj.error);
return void UI.warn(Messages.error);
}
id = Util.uid();
events.UPDATE_TICKET.fire();
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
});

View file

@ -19,6 +19,7 @@ define([
Messages.support_answerAs = "Answer as <b>{0}</b>"; // XXX
Messages.support_movePending = "Move to pending";
Messages.support_moveActive = "Move to active";
Messages.support_copyUserData = "Copy user data";
var getDebuggingData = function (ctx, data) {
var common = ctx.common;
@ -31,12 +32,9 @@ define([
data.sender = {
name: user.name,
accountName: privateData.accountName,
drive: privateData.driveChannel,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic,
notifications: user.notifications,
notifications: user.notifications
};
if (ctx.isAdmin && ctx.anonymous) {
@ -53,6 +51,8 @@ define([
}
if (!ctx.isAdmin) {
data.sender.drive = privateData.driveChannel;
data.sender.accountName = privateData.accountName;
data.sender.userAgent = Util.find(window, ['navigator', 'userAgent']);
data.sender.vendor = Util.find(window, ['navigator', 'vendor']);
data.sender.appVersion = Util.find(window, ['navigator', 'appVersion']);
@ -223,7 +223,7 @@ define([
var content = [
h('hr'),
category,
catContainer,
!ctx.isAdmin ? catContainer : undefined,
notice,
//h('br'),
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
@ -328,11 +328,7 @@ define([
// Admin actions
let show = h('button.btn.btn-primary.cp-support-expand', Messages.admin_support_open);
let $show = $(show);
let url = h('button.btn', { title: Messages.share_linkCopy, }, [
h('i.fa.fa-link', {
'aria-hidden': true,
}),
]);
let url = h('button.btn.fa.fa-link', { title: Messages.share_linkCopy, });
$(url).click(function (e) {
e.stopPropagation();
var link = privateData.origin + privateData.pathname + '#' + 'active-' + id;
@ -368,7 +364,7 @@ define([
if (onMove) {
let text = onMove.isTicketActive ? Messages.support_movePending
: Messages.support_moveActive;
settings = h('button.btn.btn-secondary.fa.fa-hdd-o', { title: text });
settings = h('button.btn.btn-secondary.fa.fa-archive', { title: text });
Util.onClickEnter($(settings), function () {
onMove(ticket, id, content);
});
@ -439,13 +435,26 @@ define([
|| (!senderKey && content.sender.accountName === 'support'); // XXX anon key?
var fromPremium = Boolean(content.sender.plan || Util.find(content, ['sender', 'quota', 'plan']));
var copyUser = h('button.btn.btn-secondary.fa.fa-clipboard.cp-support-copydata', {
title: Messages.support_copyUserData
});
Util.onClickEnter($(copyUser), () => {
let data = JSON.stringify({
name: content.sender.name,
curvePublic: content.sender.curvePublic,
notifications: content.sender.notifications
});
Clipboard.copy(data, (err) => {
if (!err) { UI.log(Messages.genericCopySuccess); }
});
});
var userData = h('div.cp-support-showdata', [
Messages.support_showData,
h('pre.cp-support-message-data', JSON.stringify(content.sender, 0, 2))
h('pre.cp-support-message-data', [copyUser, JSON.stringify(content.sender, 0, 2)])
]);
$(userData).click(function () {
$(userData).find('pre').toggle();
}).find('pre').click(function (ev) {
$(userData).find('.cp-support-message-data').toggle();
}).find('*').click(function (ev) {
ev.stopPropagation();
});