mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
feat(notes): make rtChannel module from office apps
This commit is contained in:
parent
444ea527d3
commit
37e6dbc632
@ -6,7 +6,7 @@ const factory = (UserObject, ProxyManager,
|
||||
Migrate, Hash, Util, Constants, Feedback,
|
||||
Realtime, Messaging, Pinpad, Rpc, Cryptget, Cache,
|
||||
SF, AccountTS, DriveTS, PadTS, Form, Cursor,
|
||||
Support, Integration, OnlyOffice,
|
||||
Support, Integration, RtChannel,
|
||||
Mailbox, Profile, Team, Messenger, History,
|
||||
Calendar, BadgeTS, LinkedTS, Block, NetConfig,
|
||||
Crypto, ChainPad, CpNetflux, Listmap,
|
||||
@ -1590,14 +1590,6 @@ const factory = (UserObject, ProxyManager,
|
||||
});
|
||||
};
|
||||
|
||||
// OnlyOffice
|
||||
Store.onlyoffice = {
|
||||
execCommand: function (clientId, data, cb) {
|
||||
if (!store.onlyoffice) { return void cb({error: 'OnlyOffice is disabled'}); }
|
||||
store.onlyoffice.execCommand(clientId, data, cb);
|
||||
}
|
||||
};
|
||||
|
||||
// Mailbox
|
||||
Store.mailbox = {
|
||||
execCommand: function (clientId, data, cb) {
|
||||
@ -2230,18 +2222,6 @@ const factory = (UserObject, ProxyManager,
|
||||
Store.pad?.removeClient?.(clientId);
|
||||
};
|
||||
|
||||
var loadOnlyOffice = function () {
|
||||
if (store.onlyoffice) { return; }
|
||||
store.onlyoffice = OnlyOffice.init(store, function (ev, data, clients) {
|
||||
clients.forEach(function (cId) {
|
||||
postMessage(cId, 'OO_EVENT', {
|
||||
ev: ev,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var loadMailbox = function (waitFor) {
|
||||
store.mailbox = Mailbox.init({
|
||||
Store: Store,
|
||||
@ -2432,7 +2412,7 @@ const factory = (UserObject, ProxyManager,
|
||||
loadUniversal(History, 'history', waitFor);
|
||||
loadUniversal(Badge, 'badge', waitFor);
|
||||
loadUniversal(LinkedDoc, 'linked-doc', waitFor);
|
||||
loadOnlyOffice();
|
||||
loadUniversal(RtChannel, 'rtchannel', waitFor);
|
||||
if (store) {
|
||||
store.messenger = store.modules['messenger'];
|
||||
}
|
||||
@ -3123,7 +3103,7 @@ module.exports = factory(
|
||||
require('./modules/cursor'),
|
||||
require('./modules/support'),
|
||||
require('./modules/integration'),
|
||||
require('./modules/onlyoffice'),
|
||||
require('./modules/rtchannel'),
|
||||
require('./modules/mailbox'),
|
||||
require('./modules/profile'),
|
||||
require('./modules/team'),
|
||||
|
||||
@ -462,7 +462,6 @@ const alwaysOnline = (ctx, chanId) => {
|
||||
const dropChannel = (ctx, chanId) => {
|
||||
const store = ctx.store;
|
||||
store.messenger?.leavePad?.(chanId);
|
||||
store.onlyoffice?.leavePad?.(chanId);
|
||||
Object.keys(store.modules).forEach(key => {
|
||||
store.modules[key]?.leavePad?.(chanId);
|
||||
});
|
||||
|
||||
@ -59,8 +59,6 @@ const factory = AStore => {
|
||||
SEND_FRIEND_REQUEST: Store.sendFriendRequest,
|
||||
// Team invitation
|
||||
ANON_GET_PREVIEW_CONTENT: Store.anonGetPreviewContent,
|
||||
// OnlyOffice
|
||||
OO_COMMAND: Store.onlyoffice.execCommand,
|
||||
// Mailbox
|
||||
MAILBOX_COMMAND: Store.mailbox.execCommand,
|
||||
// Universal
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const factory = (Feedback) => {
|
||||
var OO = {};
|
||||
var SP = {};
|
||||
|
||||
var getHistory = function (ctx, client, cb) {
|
||||
var c = ctx.clients[client];
|
||||
@ -29,22 +29,35 @@ const factory = (Feedback) => {
|
||||
var c = ctx.clients[client];
|
||||
var chan = ctx.channels[channel];
|
||||
|
||||
const onError = (err) => {
|
||||
// Callback all connecting clients
|
||||
ctx.channels[channel].clients.forEach(c => {
|
||||
ctx.client[c]?.cb(err);
|
||||
});
|
||||
};
|
||||
const onReady = () => {
|
||||
// Callback all connecting clients
|
||||
ctx.channels[channel].clients.forEach(c => {
|
||||
ctx.client[c]?.cb({ clients: chan.clients });
|
||||
});
|
||||
};
|
||||
|
||||
if (!c) { // new tab
|
||||
c = ctx.clients[client] = {
|
||||
channel: channel,
|
||||
cb: Util.once(cb),
|
||||
channel
|
||||
};
|
||||
} else if (c?.channel !== channel) { // new channel on existing tab
|
||||
// Remove client from existing chan
|
||||
// and disconnect from chan if needed
|
||||
c.cb({ error: 'EINVAL' }); // cancel previous attempt
|
||||
ctx.removeClient(client, true);
|
||||
c = ctx.clients[client] = {
|
||||
channel: channel,
|
||||
cb: Util.once(cb),
|
||||
channel
|
||||
};
|
||||
} else { // same channel existing tab
|
||||
setTimeout(() => {
|
||||
ctx.emit('READY', chan.clients, [client]);
|
||||
});
|
||||
return void cb();
|
||||
return void cb({ clients: chan.clients });
|
||||
}
|
||||
|
||||
if (chan) {
|
||||
@ -54,12 +67,12 @@ const factory = (Feedback) => {
|
||||
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
|
||||
|
||||
getHistory(ctx, client, function () {
|
||||
ctx.emit('READY', chan.clients, [client]);
|
||||
cb({ clients: chan.clients });
|
||||
});
|
||||
|
||||
// ==> And push the new tab to the list
|
||||
chan.clients.push(client);
|
||||
return void cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var txid = Math.floor(Math.random() * 1000000);
|
||||
@ -115,7 +128,6 @@ const factory = (Feedback) => {
|
||||
chan.clients = [client];
|
||||
chan.lastCpHash = obj.lastCpHash;
|
||||
first = false;
|
||||
cb();
|
||||
}
|
||||
|
||||
var hk = network.historyKeeper;
|
||||
@ -164,10 +176,9 @@ const factory = (Feedback) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// End of history: emit READY
|
||||
// End of history: callback as ready
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
ctx.emit('READY', chan.clients, chan.clients);
|
||||
return;
|
||||
return void onReady();
|
||||
}
|
||||
if (parsed.error && parsed.channel) {
|
||||
if (parsed.error === "EDELETED" && parsed.message) {
|
||||
@ -175,7 +186,7 @@ const factory = (Feedback) => {
|
||||
// document read-only
|
||||
chan.wc?.leave();
|
||||
Feedback.send('RTCHANNEL_DELETED', true);
|
||||
return ctx.emit('ERROR', { error: 'EDELETED', reason: parsed.message }, chan.clients);
|
||||
return onError({ error: 'EDELETED', reason: parsed.message });
|
||||
}
|
||||
if (parsed.error === "EUNKNOWN") {
|
||||
let hk = network.historyKeeper;
|
||||
@ -183,8 +194,7 @@ const factory = (Feedback) => {
|
||||
network.sendto(hk, JSON.stringify(msg));
|
||||
return;
|
||||
}
|
||||
ctx.emit('READY', chan.clients, chan.clients);
|
||||
return;
|
||||
return void onReady();
|
||||
}
|
||||
|
||||
// If there is a txid, make sure it's ours or abort
|
||||
@ -301,11 +311,11 @@ const factory = (Feedback) => {
|
||||
|
||||
var leaveChannel = function (ctx, padChan) {
|
||||
// Leave channel and prevent reconnect when we leave a pad
|
||||
Object.keys(ctx.channels).some(function (ooChan) {
|
||||
var channel = ctx.channels[ooChan];
|
||||
Object.keys(ctx.channels).some(function (rtChan) {
|
||||
var channel = ctx.channels[rtChan];
|
||||
if (channel.padChan !== padChan) { return; }
|
||||
if (channel.wc) { channel.wc.leave(); }
|
||||
delete ctx.channels[ooChan];
|
||||
delete ctx.channels[rtChan];
|
||||
return true;
|
||||
});
|
||||
};
|
||||
@ -339,22 +349,22 @@ const factory = (Feedback) => {
|
||||
|
||||
|
||||
|
||||
OO.init = function (store, emit) {
|
||||
var oo = {};
|
||||
SP.init = function (cfg, waitFor, emit) {
|
||||
var sp = {};
|
||||
var ctx = {
|
||||
store: store,
|
||||
store: cfg.store,
|
||||
emit: emit,
|
||||
channels: {},
|
||||
clients: {}
|
||||
};
|
||||
|
||||
oo.removeClient = ctx.removeClient = function (clientId, newChan) {
|
||||
sp.removeClient = ctx.removeClient = function (clientId, newChan) {
|
||||
removeClient(ctx, clientId, newChan);
|
||||
};
|
||||
oo.leavePad = function (padChan) {
|
||||
sp.leavePad = function (padChan) {
|
||||
leaveChannel(ctx, padChan);
|
||||
};
|
||||
oo.execCommand = function (clientId, obj, cb) {
|
||||
sp.execCommand = function (clientId, obj, cb) {
|
||||
var cmd = obj.cmd;
|
||||
var data = obj.data;
|
||||
if (cmd === 'SEND_MESSAGE') {
|
||||
@ -371,10 +381,10 @@ const factory = (Feedback) => {
|
||||
}
|
||||
};
|
||||
|
||||
return oo;
|
||||
return sp;
|
||||
};
|
||||
|
||||
return OO;
|
||||
return SP;
|
||||
};
|
||||
|
||||
module.exports = factory(
|
||||
@ -42,7 +42,7 @@ import * as Mailbox from './modules/mailbox.js';
|
||||
import * as Cursor from './modules/cursor.js';
|
||||
import * as Support from './modules/support.js';
|
||||
import * as Integration from './modules/integration.js';
|
||||
import * as OnlyOffice from './modules/onlyoffice.js';
|
||||
import * as RtChannel from './modules/rtchannel.js';
|
||||
import * as Profile from './modules/profile.js';
|
||||
import * as Team from './modules/team.js';
|
||||
import * as Messenger from './modules/messenger.js';
|
||||
|
||||
@ -1307,13 +1307,6 @@ define([
|
||||
postMessage("ANON_GET_PREVIEW_CONTENT", data, cb);
|
||||
};
|
||||
|
||||
// Onlyoffice
|
||||
var onlyoffice = common.onlyoffice = {};
|
||||
onlyoffice.execCommand = function (data, cb) {
|
||||
postMessage("OO_COMMAND", data, cb);
|
||||
};
|
||||
onlyoffice.onEvent = Util.mkEvent();
|
||||
|
||||
// Mailbox
|
||||
var mailbox = common.mailbox = {};
|
||||
mailbox.execCommand = function (data, cb) {
|
||||
@ -2536,8 +2529,6 @@ define([
|
||||
common.onNetworkReconnect.fire(data);
|
||||
});
|
||||
},
|
||||
// OnlyOffice
|
||||
OO_EVENT: common.onlyoffice.onEvent.fire,
|
||||
// Mailbox
|
||||
MAILBOX_EVENT: common.mailbox.onEvent.fire,
|
||||
// Universal
|
||||
|
||||
249
www/common/inner/rtchannel.js
Normal file
249
www/common/inner/rtchannel.js
Normal file
@ -0,0 +1,249 @@
|
||||
// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* This module is used on realtime applications using non-ChainPad patches
|
||||
* (Office, Notes, etc.). These apps use static checkpoints (blobs) paired
|
||||
* with a separate channel to store patches.
|
||||
* With a JSON "content, the checkpoints (blob + channel) are stored in
|
||||
* the value "content.hashes" listing all this document's checkpoints.
|
||||
*/
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'/components/nthen/index.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-ui-elements.js',
|
||||
'/common/hyperscript.js',
|
||||
'/api/config',
|
||||
'/customize/application_config.js',
|
||||
'/customize/messages.js',
|
||||
'/common/onlyoffice/history.js',
|
||||
|
||||
'/components/file-saver/FileSaver.min.js',
|
||||
], function (
|
||||
$,
|
||||
nThen,
|
||||
Hash,
|
||||
Util,
|
||||
UIElements,
|
||||
h,
|
||||
ApiConfig,
|
||||
AppConfig,
|
||||
Messages,
|
||||
History)
|
||||
{
|
||||
|
||||
let content;
|
||||
// Call init once sframe-common is initialized
|
||||
const init = (APP, common) => {
|
||||
const tools = {};
|
||||
|
||||
const onRTCEvent = Util.mkEvent();
|
||||
const sframeChan = common.getSframeChannel();
|
||||
|
||||
const linkedModule = common.makeUniversal('linked-doc');
|
||||
const rtcModule = common.makeUniversal('rtchannel', {
|
||||
onEvent: obj => {
|
||||
onRTCEvent.fire(obj);
|
||||
}
|
||||
});
|
||||
|
||||
const sortCpIndex = History.sortCpIndex;
|
||||
|
||||
// LINKED DOCUMENTS
|
||||
const addLinkedCheckpoint = (cpData, cb) => {
|
||||
let parsed = Hash.parsePadUrl(cpData.file);
|
||||
let secret = Hash.getSecrets('file', parsed.hash);
|
||||
linkedModule.execCommand('ADD_LINKED_DATA', {
|
||||
content: {
|
||||
type: 'checkpoints',
|
||||
data: {
|
||||
rtChannel: cpData.rtChannel,
|
||||
blob: secret.channel
|
||||
}
|
||||
}
|
||||
}, (obj) => {
|
||||
if (obj?.error) { console.error(obj.error); }
|
||||
const last = obj?.[0]?.checkpoints?.pop();
|
||||
if (last?.blob === secret.channel && last?.time) {
|
||||
cpData.time = last.time;
|
||||
APP.onLocal();
|
||||
}
|
||||
cb();
|
||||
});
|
||||
};
|
||||
const checkLinkedDocs = () => {
|
||||
const value = {
|
||||
checkpoints: []
|
||||
};
|
||||
// Get last 10 cps
|
||||
let hashes = content.hashes || {}; // checkpoints
|
||||
let sortedCp = sortCpIndex(hashes).slice(-10);
|
||||
sortedCp.forEach(cpIdx => {
|
||||
const cpData = hashes[cpIdx];
|
||||
let parsed = Hash.parsePadUrl(cpData.file);
|
||||
let secret = Hash.getSecrets('file', parsed.hash);
|
||||
if (!secret.channel) { return; }
|
||||
value.checkpoints.push({
|
||||
blob: secret.channel,
|
||||
rtChannel: cpData.rtChannel || content.channel
|
||||
});
|
||||
});
|
||||
// If < 10, add initial channel
|
||||
if (sortedCp.length < 10 && content.channel) {
|
||||
value.checkpoints.unshift({
|
||||
blob: 0,
|
||||
rtChannel: content.channel
|
||||
});
|
||||
}
|
||||
linkedModule.execCommand('CHECK_CURRENT_DOC', {
|
||||
// channel & signKey added in outer
|
||||
expectedJSON: value
|
||||
}, (obj) => {
|
||||
if (obj?.error) { console.error(obj.error); }
|
||||
});
|
||||
};
|
||||
|
||||
// CHECKPOINTS
|
||||
const getLastCpId = (oldHashes) => {
|
||||
const hashes = oldHashes || content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return 0; }
|
||||
const allIdx = sortCpIndex(hashes);
|
||||
return allIdx[allIdx.length - 1];
|
||||
};
|
||||
const getLastCp = () => {
|
||||
const hashes = content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return {}; }
|
||||
const idx = sortCpIndex(hashes);
|
||||
const lastIndex = idx[idx.length - 1];
|
||||
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(JSON.stringify(hashes[lastIndex]));
|
||||
};
|
||||
const deleteLastCp = (i) => {
|
||||
const hashes = content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return {}; }
|
||||
i = i || 0;
|
||||
const idx = sortCpIndex(hashes);
|
||||
const lastIndex = idx[idx.length - 1 - i];
|
||||
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
|
||||
return;
|
||||
}
|
||||
delete hashes[lastIndex];
|
||||
APP.onLocal();
|
||||
APP.realtime.onSettle(function () {
|
||||
UI.log(Messages.saved);
|
||||
});
|
||||
};
|
||||
|
||||
// RT_CHANNEL
|
||||
|
||||
const onRTCLeave = Util.mkEvent();
|
||||
const onRTCMessage = Util.mkEvent();
|
||||
const onRTCHistorySynced = Util.mkEvent();
|
||||
const openRtChannel = (cpData, cb) => {
|
||||
const channel = cpData?.rtChannel || content.channel;
|
||||
const lastCpHash = cpData?.hash;
|
||||
sframeChan.query('Q_RTC_OPENCHANNEL', {
|
||||
channel, lastCpHash
|
||||
}, function (err, obj) {
|
||||
if (obj?.error) {
|
||||
console.error(obj.error);
|
||||
return void cb(obj.error);
|
||||
}
|
||||
cb(void 0, obj?.clients);
|
||||
});
|
||||
onRTCEvent.reg(obj => {
|
||||
switch (obj.ev) {
|
||||
case 'LEAVE':
|
||||
onRTCLeave.fire(obj.data);
|
||||
break;
|
||||
case 'MESSAGE':
|
||||
onRTCMessage.fire(obj.data);
|
||||
break;
|
||||
case 'HISTORY_SYNCED':
|
||||
onRTCHistorySynced.fire()
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendCmd = (data, cb) => {
|
||||
if (APP.history) { return; }
|
||||
sframeChan.query('Q_RTC_COMMAND', data, cb);
|
||||
};
|
||||
const rtChannel = {
|
||||
getHistory: function (cb) {
|
||||
sendCmd({
|
||||
cmd: 'GET_HISTORY',
|
||||
data: {}
|
||||
}, cb);
|
||||
},
|
||||
sendMsg: function (msg, cp, cb) {
|
||||
sendCmd({
|
||||
cmd: 'SEND_MESSAGE',
|
||||
data: {
|
||||
msg: msg,
|
||||
isCp: cp
|
||||
}
|
||||
}, cb);
|
||||
},
|
||||
};
|
||||
|
||||
// XXX to add
|
||||
// onUploaded
|
||||
// fmConfig / APP.FM
|
||||
// new function "uploadCheckpoint" which calls APP.FM.handleFile
|
||||
// TODO/NOTE/MAYBE manage content.saveLock?
|
||||
// restoreLastCp
|
||||
// checkCheckpoint
|
||||
// loadLastDocument ? (download static file + decrypt)
|
||||
// loadDocument?
|
||||
// $historyButton, $snapshotButton
|
||||
|
||||
// Now or later?
|
||||
// openVersionHash
|
||||
// loadTemplate / openTemplatePicker
|
||||
|
||||
// XXX ???
|
||||
// EV_OO_DOC_READY
|
||||
// Integration channel?
|
||||
|
||||
|
||||
// Call setContent each time the content variable is overriden
|
||||
const setContent = _content => {
|
||||
content = _content;
|
||||
};
|
||||
|
||||
|
||||
tools.rtcModule = rtcModule;
|
||||
|
||||
// Linked docs
|
||||
tools.addLinkedCheckpoint = addLinkedCheckpoint;
|
||||
tools.checkLinkedDocs = checkLinkedDocs;
|
||||
|
||||
// Checkpoint
|
||||
tools.getLastCpId = getLastCpId;
|
||||
tools.getLastCp = getLastCp;
|
||||
tools.deleteLastCp = deleteLastCp;
|
||||
|
||||
// RtChannel
|
||||
tools.openRtChannel = openRtChannel;
|
||||
tools.onRTCLeave = onRTCLeave;
|
||||
tools.onRTCMessage = onRTCMessage;
|
||||
tools.onRTCHistorySynced = onRTCHistorySynced;
|
||||
tools.rtChannel = rtChannel;
|
||||
|
||||
|
||||
tools.setContent = setContent;
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
return { init };
|
||||
});
|
||||
@ -19,6 +19,7 @@ define([
|
||||
'/support/ui.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'/file/file-crypto.js',
|
||||
'/common/inner/rtchannel.js',
|
||||
'/common/onlyoffice/history.js',
|
||||
'/common/onlyoffice/oocell_base.js',
|
||||
'/common/onlyoffice/oodoc_base.js',
|
||||
@ -49,6 +50,7 @@ define([
|
||||
Support,
|
||||
ChainPad,
|
||||
FileCrypto,
|
||||
RtChannel,
|
||||
History,
|
||||
EmptyCell,
|
||||
EmptyDoc,
|
||||
@ -81,6 +83,7 @@ define([
|
||||
var stringify = Util.sortify;
|
||||
var toolbar;
|
||||
var cursor;
|
||||
const onRTCEvent = Util.mkEvent();
|
||||
|
||||
var andThen = function (common) {
|
||||
var Title;
|
||||
@ -98,6 +101,7 @@ define([
|
||||
mediasSources: {},
|
||||
version: privateData.ooForceVersion ? Number(privateData.ooForceVersion) : OOCurrentVersion.currentVersionNumber,
|
||||
};
|
||||
APP.rtcTools.setContent(content);
|
||||
content.originalVersion = content.version;
|
||||
var oldHashes = {};
|
||||
var oldIds = {};
|
||||
@ -255,80 +259,6 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var sortCpIndex = History.sortCpIndex;
|
||||
|
||||
const getOriginalVersion = () => {
|
||||
const hashes = content.hashes || {};
|
||||
const allIdx = sortCpIndex(hashes);
|
||||
let version;
|
||||
// Find the first checkpoint with a version.
|
||||
// If this version is smaller than content.version, use it
|
||||
// as original version (Math.min below)
|
||||
allIdx.some((id) => {
|
||||
const v = hashes[id]?.version;
|
||||
if (v) {
|
||||
version = v;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (version) { version = Math.min(version, content.version || 1); }
|
||||
return version || content.version || 1;
|
||||
};
|
||||
|
||||
const addLinkedCheckpoint = (cpData, cb) => {
|
||||
let parsed = Hash.parsePadUrl(cpData.file);
|
||||
let secret = Hash.getSecrets('file', parsed.hash);
|
||||
APP.linkedModule.execCommand('ADD_LINKED_DATA', {
|
||||
content: {
|
||||
type: 'checkpoints',
|
||||
data: {
|
||||
rtChannel: cpData.rtChannel,
|
||||
blob: secret.channel
|
||||
}
|
||||
}
|
||||
}, (obj) => {
|
||||
if (obj?.error) { console.error(obj.error); }
|
||||
const last = obj?.[0]?.checkpoints?.pop();
|
||||
if (last?.blob === secret.channel && last?.time) {
|
||||
cpData.time = last.time;
|
||||
APP.onLocal();
|
||||
}
|
||||
cb();
|
||||
});
|
||||
};
|
||||
const checkLinkedDocs = () => {
|
||||
const value = {
|
||||
checkpoints: []
|
||||
};
|
||||
// Get last 10 cps
|
||||
let hashes = content.hashes || {}; // checkpoints
|
||||
let sortedCp = sortCpIndex(hashes).slice(-10);
|
||||
sortedCp.forEach(cpIdx => {
|
||||
const cpData = hashes[cpIdx];
|
||||
let parsed = Hash.parsePadUrl(cpData.file);
|
||||
let secret = Hash.getSecrets('file', parsed.hash);
|
||||
if (!secret.channel) { return; }
|
||||
value.checkpoints.push({
|
||||
blob: secret.channel,
|
||||
rtChannel: cpData.rtChannel || content.channel
|
||||
});
|
||||
});
|
||||
// If < 10, add initial channel
|
||||
if (sortedCp.length < 10 && content.channel) {
|
||||
value.checkpoints.unshift({
|
||||
blob: 0,
|
||||
rtChannel: content.channel
|
||||
});
|
||||
}
|
||||
APP.linkedModule.execCommand('CHECK_CURRENT_DOC', {
|
||||
// channel & signKey added in outer
|
||||
expectedJSON: value
|
||||
}, (obj) => {
|
||||
if (obj?.error) { console.error(obj.error); }
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var getFileType = function () {
|
||||
var priv = common.getMetadataMgr().getPrivateData();
|
||||
var type = priv.ooType;
|
||||
@ -366,71 +296,24 @@ define([
|
||||
|
||||
var now = function () { return +new Date(); };
|
||||
|
||||
const getLastCpId = (old) => {
|
||||
const hashes = old ? oldHashes : content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return 0; }
|
||||
const allIdx = sortCpIndex(hashes);
|
||||
return allIdx[allIdx.length - 1];
|
||||
};
|
||||
var getLastCp = function (old, i) {
|
||||
var hashes = old ? oldHashes : content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return {}; }
|
||||
i = i || 0;
|
||||
var idx = sortCpIndex(hashes);
|
||||
var lastIndex = idx[idx.length - 1 - i];
|
||||
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
|
||||
return {};
|
||||
}
|
||||
var last = JSON.parse(JSON.stringify(hashes[lastIndex]));
|
||||
return last;
|
||||
};
|
||||
var deleteLastCp = function (i) {
|
||||
var hashes = content.hashes;
|
||||
if (!hashes || !Object.keys(hashes).length) { return {}; }
|
||||
i = i || 0;
|
||||
var idx = sortCpIndex(hashes);
|
||||
var lastIndex = idx[idx.length - 1 - i];
|
||||
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
|
||||
return;
|
||||
}
|
||||
delete hashes[lastIndex];
|
||||
APP.onLocal();
|
||||
APP.realtime.onSettle(function () {
|
||||
UI.log(Messages.saved);
|
||||
});
|
||||
};
|
||||
var sortCpIndex = History.sortCpIndex;
|
||||
|
||||
var rtChannel = {
|
||||
ready: false,
|
||||
readyCb: undefined,
|
||||
sendCmd: function (data, cb) {
|
||||
if (APP.history) { return; }
|
||||
sframeChan.query('Q_OO_COMMAND', data, cb);
|
||||
},
|
||||
getHistory: function (cb) {
|
||||
rtChannel.sendCmd({
|
||||
cmd: 'GET_HISTORY',
|
||||
data: {}
|
||||
}, function () {
|
||||
APP.onHistorySynced = cb;
|
||||
});
|
||||
},
|
||||
sendMsg: function (msg, cp, cb) {
|
||||
evOnPatch.fire();
|
||||
rtChannel.sendCmd({
|
||||
cmd: 'SEND_MESSAGE',
|
||||
data: {
|
||||
msg: msg,
|
||||
isCp: cp
|
||||
}
|
||||
}, function (err, h) {
|
||||
if (!err) {
|
||||
evOnSync.fire();
|
||||
evIntegrationSave.fire();
|
||||
}
|
||||
cb(err, h);
|
||||
});
|
||||
},
|
||||
const getOriginalVersion = () => {
|
||||
const hashes = content.hashes || {};
|
||||
const allIdx = sortCpIndex(hashes);
|
||||
let version;
|
||||
// Find the first checkpoint with a version.
|
||||
// If this version is smaller than content.version, use it
|
||||
// as original version (Math.min below)
|
||||
allIdx.some((id) => {
|
||||
const v = hashes[id]?.version;
|
||||
if (v) {
|
||||
version = v;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (version) { version = Math.min(version, content.version || 1); }
|
||||
return version || content.version || 1;
|
||||
};
|
||||
|
||||
var ooChannel = {
|
||||
@ -511,6 +394,47 @@ define([
|
||||
]))
|
||||
};
|
||||
|
||||
const {
|
||||
addLinkedCheckpoint, checkLinkedDocs,
|
||||
getLastCpId, getLastCp, deleteLastCp, openRtChannel,
|
||||
onRTCLeave, onRTCMessage, onRTCHistorySynced,
|
||||
rtChannel,
|
||||
} = APP.rtcTools;
|
||||
|
||||
const sendRTCMessage = (msg, cp, cb) => {
|
||||
evOnPatch.fire();
|
||||
rtChannel.sendMsg(msg, cp, (err, h) => {
|
||||
if (!err) {
|
||||
evOnSync.fire();
|
||||
evIntegrationSave.fire();
|
||||
}
|
||||
cb(err, h);
|
||||
});
|
||||
};
|
||||
|
||||
onRTCLeave.reg(removeClient);
|
||||
onRTCHistorySynced.reg(data => {
|
||||
if (typeof(APP.onHistorySynced) !== "function") { return; }
|
||||
APP.onHistorySynced();
|
||||
delete APP.onHistorySynced;
|
||||
});
|
||||
onRTCMessage.reg(data => {
|
||||
if (APP.history) {
|
||||
ooChannel.historyLastHash = data.hash;
|
||||
ooChannel.currentIndex++;
|
||||
return;
|
||||
}
|
||||
if (ooChannel.ready) {
|
||||
ooChannel.send(data.msg);
|
||||
ooChannel.lastHash = data.hash;
|
||||
ooChannel.cpIndex++;
|
||||
common.notify();
|
||||
} else {
|
||||
ooChannel.queue.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var onUploaded = function (ev, data, err) {
|
||||
if (!ev && err) {
|
||||
console.error(err);
|
||||
@ -615,131 +539,6 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
const sendDebugSupportTicket = (message) => {
|
||||
const title = "[Automatic] Office document locked";
|
||||
|
||||
APP.supportModule.execCommand('MAKE_TICKET', {
|
||||
channel: Hash.createChannelId(),
|
||||
title,
|
||||
ticket: APP.support.getDebuggingData({
|
||||
title,
|
||||
message
|
||||
})
|
||||
}, () => {});
|
||||
};
|
||||
const onRtChannelError = (err, channel) => {
|
||||
const wasReadOnly = readOnly;
|
||||
readOnly = true;
|
||||
offline = true;
|
||||
|
||||
const message = JSON.stringify({
|
||||
error: err?.error,
|
||||
reason: err?.reason,
|
||||
channel: privateData.channel,
|
||||
rtChannel: channel
|
||||
}, 0, 2);
|
||||
|
||||
let txt = Messages.oo_rtChannelMissing;
|
||||
let value;
|
||||
let f = UI.confirm;
|
||||
let cb = (yes) => {
|
||||
if (!yes) { return; }
|
||||
|
||||
// Set flag if support has already been contacted
|
||||
content.missingRtChannel = +new Date();
|
||||
readOnly = wasReadOnly;
|
||||
APP.onLocal();
|
||||
readOnly = true;
|
||||
|
||||
sendDebugSupportTicket(message);
|
||||
};
|
||||
|
||||
let btnText = content.missingRtChannel ? Messages.sent : Messages.support_formButton;
|
||||
let opts = {
|
||||
ok: [
|
||||
Icons.get('send'),
|
||||
h('span', btnText)
|
||||
],
|
||||
cancel: Messages.filePicker_close
|
||||
};
|
||||
|
||||
if (content.missingRtChannel) {
|
||||
value = h('strong', Messages._getKey('oo_rtChannelMissingDate', [
|
||||
new Date(content.missingRtChannel).toLocaleDateString()
|
||||
]));
|
||||
setTimeout(() => {
|
||||
const $b = UI.findOKButton();
|
||||
$b.attr('disabled', 'disabled');
|
||||
});
|
||||
}
|
||||
|
||||
if (!ApiConfig.supportMailboxKey) {
|
||||
txt = Messages.oo_rtChannelMissingNoSupport;
|
||||
value = UI.getPreCopy(message);
|
||||
f = UI.alert;
|
||||
opts = undefined;
|
||||
cb = undefined;
|
||||
}
|
||||
|
||||
let div = h('div', [
|
||||
h('p', txt),
|
||||
value
|
||||
]);
|
||||
f(div, cb, opts);
|
||||
};*/
|
||||
|
||||
var openRtChannel = function (cpData, cb) {
|
||||
const channel = cpData?.rtChannel || content.channel;
|
||||
const lastCpHash = cpData?.hash;
|
||||
sframeChan.query('Q_OO_OPENCHANNEL', {
|
||||
channel, lastCpHash
|
||||
}, function (err, obj) {
|
||||
if (obj?.error) { console.error(obj.error); }
|
||||
// XXX an error loading a checkpoint was ignored, causing a sheet
|
||||
// to load incorrectly. There's a risk of a new checkpoint being created
|
||||
// with the resulting (incorrect) state. Errors like this should be reported
|
||||
// to the user so they realize something is wrong.
|
||||
});
|
||||
sframeChan.on('EV_OO_EVENT', function (obj) {
|
||||
switch (obj.ev) {
|
||||
case 'ERROR':
|
||||
//onRtChannelError(obj.data, channel);
|
||||
cb();
|
||||
break;
|
||||
case 'READY':
|
||||
checkClients(obj.data);
|
||||
cb();
|
||||
break;
|
||||
case 'LEAVE':
|
||||
removeClient(obj.data);
|
||||
break;
|
||||
case 'MESSAGE':
|
||||
if (APP.history) {
|
||||
ooChannel.historyLastHash = obj.data.hash;
|
||||
ooChannel.currentIndex++;
|
||||
return;
|
||||
}
|
||||
if (ooChannel.ready) {
|
||||
ooChannel.send(obj.data.msg);
|
||||
ooChannel.lastHash = obj.data.hash;
|
||||
ooChannel.cpIndex++;
|
||||
common.notify();
|
||||
} else {
|
||||
ooChannel.queue.push(obj.data);
|
||||
}
|
||||
break;
|
||||
case 'HISTORY_SYNCED':
|
||||
if (typeof(APP.onHistorySynced) !== "function") { return; }
|
||||
APP.onHistorySynced();
|
||||
delete APP.onHistorySynced;
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var fmConfig = {
|
||||
noHandlers: true,
|
||||
noStore: true,
|
||||
@ -792,7 +591,8 @@ define([
|
||||
return void startOO(blob, type, true);
|
||||
}
|
||||
|
||||
openRtChannel(cpData, Util.once(() => {
|
||||
openRtChannel(cpData, Util.once((err, data) => {
|
||||
if (!err) { checkClients(data); }
|
||||
startOO(blob, type, true);
|
||||
}));
|
||||
};
|
||||
@ -1434,7 +1234,7 @@ define([
|
||||
|
||||
// Send the changes
|
||||
content.locks = content.locks || {};
|
||||
rtChannel.sendMsg({
|
||||
sendRTCMessage({
|
||||
type: "saveChanges",
|
||||
changes: parseChanges(changes, obj.type === "cp_theme"),
|
||||
changesIndex: ooChannel.cpIndex || 0,
|
||||
@ -1548,7 +1348,7 @@ define([
|
||||
};
|
||||
|
||||
// Send the patch
|
||||
rtChannel.sendMsg(msg, null, function (err, hash) {
|
||||
sendRTCMessage(msg, null, function (err, hash) {
|
||||
if (err) {
|
||||
return void console.error(err);
|
||||
}
|
||||
@ -2159,7 +1959,7 @@ define([
|
||||
if (APP.unsavedChanges) {
|
||||
var unsaved = APP.unsavedChanges;
|
||||
delete APP.unsavedChanges;
|
||||
rtChannel.sendMsg(unsaved, null, function (err, hash) {
|
||||
sendRTCMessage(unsaved, null, function (err, hash) {
|
||||
if (err) { return void UI.alert(Messages.oo_lostEdits); }
|
||||
// This is supposed to be a "send" function to tell our OO
|
||||
// to unlock the cell. We use this to know that the patch was
|
||||
@ -3075,7 +2875,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
var type = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var file = getFileType();
|
||||
if (!noCp) {
|
||||
var lastCp = getLastCp(false);
|
||||
var lastCp = getLastCp();
|
||||
// If the last checkpoint is empty, load the "initial" doc instead
|
||||
if (!lastCp?.file) {
|
||||
return void loadDocument(true, useNewDefault, cb);
|
||||
@ -3145,7 +2945,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
// Extract the channel id from the source href
|
||||
return src.slice(src.lastIndexOf('/') + 1);
|
||||
}).filter(Boolean);
|
||||
sframeChan.query('EV_OO_PIN_IMAGES', toPin);
|
||||
sframeChan.query('EV_RTC_PIN_IMAGES', toPin);
|
||||
}
|
||||
};
|
||||
|
||||
@ -3320,6 +3120,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
return void sframeChan.event('EV_OOIFRAME_DONE', '');
|
||||
}
|
||||
content = json.content;
|
||||
APP.rtcTools.setContent(content);
|
||||
readOnly = true;
|
||||
if (!content.version || content.version <= 7) {
|
||||
return void sframeChan.event('EV_OOIFRAME_DONE', {
|
||||
@ -3456,8 +3257,10 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
// Fill the queue and then load the last CP
|
||||
|
||||
rtChannel.getHistory(function () {
|
||||
var lastCp = getLastCp();
|
||||
loadCheckpoint(lastCp, true);
|
||||
APP.onHistorySynced = () => {
|
||||
var lastCp = getLastCp();
|
||||
loadCheckpoint(lastCp, true);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -3491,7 +3294,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
APP.realtime.onSettle(cb);
|
||||
};
|
||||
var loadSnapshot = function (hash) {
|
||||
sframeChan.event('EV_OO_OPENVERSION', {
|
||||
sframeChan.event('EV_RTC_OPENVERSION', {
|
||||
hash: hash
|
||||
});
|
||||
};
|
||||
@ -3704,6 +3507,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
throw new Error(errorText);
|
||||
}
|
||||
content = hjson.content || content;
|
||||
APP.rtcTools.setContent(content);
|
||||
|
||||
// Support old checkpoints
|
||||
var newLatest = getLastCp();
|
||||
@ -3797,7 +3601,8 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
var useNewDefault = content.version && content.version >= 2;
|
||||
|
||||
loadDocument(newDoc, useNewDefault, (cpObj, cpData) => {
|
||||
openRtChannel(cpData, Util.once(function () {
|
||||
openRtChannel(cpData, Util.once(function (err, data) {
|
||||
if (!err) { checkClients(data); }
|
||||
setMyId();
|
||||
oldHashes = JSON.parse(JSON.stringify(content.hashes));
|
||||
initializing = false;
|
||||
@ -4104,6 +3909,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
//var integrationSave = content.integrationSave;
|
||||
|
||||
content = json.content;
|
||||
APP.rtcTools.setContent(content);
|
||||
|
||||
if (APP.history) { return; }
|
||||
|
||||
@ -4125,7 +3931,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
|
||||
var editor = getEditor();
|
||||
if (content.hashes) {
|
||||
var oldLastCp = getLastCpId(true);
|
||||
var oldLastCp = getLastCpId(oldHashes);
|
||||
var newLastCp = getLastCpId();
|
||||
if (newLastCp > oldLastCp) {
|
||||
ooChannel.queue = [];
|
||||
@ -4215,6 +4021,9 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
}));
|
||||
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
|
||||
}).nThen(function (waitFor) {
|
||||
APP.rtcTools = RtChannel.init(APP, common);
|
||||
}).nThen(function (waitFor) {
|
||||
// XXX
|
||||
common.getSframeChannel().on('EV_OO_TEMPLATE', function (data) {
|
||||
APP.startWithTemplate = data;
|
||||
});
|
||||
@ -4222,9 +4031,6 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
//noTemplates: true
|
||||
});
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
APP.supportModule = common.makeUniversal('support');
|
||||
APP.support = Support.create(common, false);
|
||||
APP.linkedModule = common.makeUniversal('linked-doc');
|
||||
andThen(common);
|
||||
});
|
||||
};
|
||||
|
||||
@ -8,8 +8,9 @@ define([
|
||||
'/api/config',
|
||||
'/common/dom-ready.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/sframe-common-outer.js'
|
||||
], function (nThen, ApiConfig, DomReady, Hash, SFCommonO) {
|
||||
'/common/sframe-common-outer.js',
|
||||
'/common/outer/rtchannel.js'
|
||||
], function (nThen, ApiConfig, DomReady, Hash, SFCommonO, RTC) {
|
||||
|
||||
var isIntegration = Boolean(window.CP_integration_outer);
|
||||
var integration = window.CP_integration_outer || {};
|
||||
@ -54,87 +55,7 @@ define([
|
||||
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
|
||||
// If we don't have "channels" values, it means we're not
|
||||
// loading an "old" checkpoint so we can clean attributes
|
||||
if (!channels?.lastVersion) {
|
||||
channels.linked = true;
|
||||
Cryptpad.setPadAttribute('linked', true, () => {});
|
||||
Cryptpad.setPadAttribute('rtChannel', void 0, () => {});
|
||||
Cryptpad.setPadAttribute('lastVersion', void 0, () => {});
|
||||
Cryptpad.setPadAttribute('lastCpHash', void 0, () => {});
|
||||
}
|
||||
|
||||
Cryptpad.onlyoffice.execCommand({
|
||||
cmd: 'OPEN_CHANNEL',
|
||||
data: {
|
||||
channel: data.channel,
|
||||
lastCpHash: data.lastCpHash,
|
||||
padChan: Utils.secret.channel, // metadata inherited from this pad
|
||||
validateKey: Utils.secret.keys.validateKey
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
sframeChan.on('EV_OO_PIN_IMAGES', function (list) {
|
||||
Cryptpad.getPadAttribute('ooImages', function (err, res) {
|
||||
if (err) { return; }
|
||||
if (!res || !Array.isArray(res)) { res = []; }
|
||||
var toPin = [];
|
||||
var toUnpin = [];
|
||||
res.forEach(function (id) {
|
||||
if (list.indexOf(id) === -1) {
|
||||
toUnpin.push(id);
|
||||
}
|
||||
});
|
||||
list.forEach(function (id) {
|
||||
if (res.indexOf(id) === -1) {
|
||||
toPin.push(id);
|
||||
}
|
||||
});
|
||||
toPin = Utils.Util.deduplicateString(toPin);
|
||||
toUnpin = Utils.Util.deduplicateString(toUnpin);
|
||||
if (toPin.length) { Cryptpad.pinPads(toPin, function () {}); }
|
||||
if (toUnpin.length) { Cryptpad.unpinPads(toUnpin, function () {}); }
|
||||
if (!toPin.length && !toUnpin.length) { return; }
|
||||
Cryptpad.setPadAttribute('ooImages', list, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_OO_COMMAND', function (obj, cb) {
|
||||
if (obj.cmd === 'SEND_MESSAGE') {
|
||||
obj.data.msg = Utils.crypto.encrypt(JSON.stringify(obj.data.msg));
|
||||
var hash = obj.data.msg.slice(0,64);
|
||||
var _cb = cb;
|
||||
cb = function () {
|
||||
_cb(hash);
|
||||
};
|
||||
}
|
||||
Cryptpad.onlyoffice.execCommand(obj, cb);
|
||||
});
|
||||
sframeChan.on('EV_OO_OPENVERSION', function (obj) {
|
||||
if (!obj || !obj.hash) { return; }
|
||||
var parsed = Hash.parsePadUrl(window.location.href);
|
||||
var opts = parsed.getOptions();
|
||||
opts.versionHash = obj.hash;
|
||||
window.open(parsed.getUrl(opts));
|
||||
});
|
||||
Cryptpad.onlyoffice.onEvent.reg(function (obj) {
|
||||
if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) {
|
||||
try {
|
||||
var validateKey = obj.data.validateKey || true;
|
||||
var skipCheck = validateKey === true;
|
||||
var msg = obj.data.msg;
|
||||
obj.data = {
|
||||
msg: JSON.parse(Utils.crypto.decrypt(msg, validateKey, skipCheck)),
|
||||
hash: msg.slice(0,64)
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
sframeChan.event('EV_OO_EVENT', obj);
|
||||
});
|
||||
RTC.addRpc(sframeChan, Cryptpad, Utils, channels);
|
||||
|
||||
// X2T
|
||||
sframeChan.on('Q_OO_CONVERT', function (obj, cb) {
|
||||
|
||||
113
www/common/outer/rtchannel.js
Normal file
113
www/common/outer/rtchannel.js
Normal file
@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
||||
define([
|
||||
'/components/nthen/index.js',
|
||||
'/api/config',
|
||||
], function (nThen, ApiConfig) {
|
||||
const RTC = {};
|
||||
|
||||
RTC.addRpc = (sframeChan, Cryptpad, Utils, channels) => {
|
||||
|
||||
const execCommand = (obj, cb) => {
|
||||
Cryptpad.universal.execCommand({
|
||||
type: 'rtchannel',
|
||||
data: obj,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
sframeChan.on('Q_RTC_OPENCHANNEL', function (data, cb) {
|
||||
// If we don't have "channels" values, it means we're not
|
||||
// loading an "old" checkpoint so we can clean attributes
|
||||
if (!channels?.lastVersion) {
|
||||
channels.linked = true;
|
||||
Cryptpad.setPadAttribute('linked', true, () => {});
|
||||
Cryptpad.setPadAttribute('rtChannel', void 0, () => {});
|
||||
Cryptpad.setPadAttribute('lastVersion', void 0, () => {});
|
||||
Cryptpad.setPadAttribute('lastCpHash', void 0, () => {});
|
||||
}
|
||||
|
||||
execCommand({
|
||||
cmd: 'OPEN_CHANNEL',
|
||||
data: {
|
||||
channel: data.channel,
|
||||
lastCpHash: data.lastCpHash,
|
||||
padChan: Utils.secret.channel, // metadata inherited
|
||||
validateKey: Utils.secret.keys.validateKey
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_RTC_COMMAND', function (obj, cb) {
|
||||
if (obj.cmd === 'SEND_MESSAGE') {
|
||||
obj.data.msg = Utils.crypto.encrypt(JSON.stringify(obj.data.msg));
|
||||
var hash = obj.data.msg.slice(0,64);
|
||||
var _cb = cb;
|
||||
cb = function () {
|
||||
_cb(hash);
|
||||
};
|
||||
}
|
||||
execCommand(obj, cb);
|
||||
});
|
||||
|
||||
Cryptpad.universal.onEvent.reg(function (data) {
|
||||
if (data?.type !== 'rtchannel') { return; }
|
||||
const obj = data?.data || {};
|
||||
if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) {
|
||||
try {
|
||||
let validateKey = obj.data.validateKey || true;
|
||||
let skipCheck = validateKey === true;
|
||||
let msg = obj.data.msg;
|
||||
let str = Utils.crypto.decrypt(msg, validateKey, skipCheck);
|
||||
obj.data = {
|
||||
msg: JSON.parse(str),
|
||||
hash: msg.slice(0,64)
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
sframeChan.event('EV_UNIVERSAL_EVENT', data);
|
||||
});
|
||||
|
||||
sframeChan.on('EV_RTC_OPENVERSION', function (obj) {
|
||||
if (!obj || !obj.hash) { return; }
|
||||
var parsed = Hash.parsePadUrl(window.location.href);
|
||||
var opts = parsed.getOptions();
|
||||
opts.versionHash = obj.hash;
|
||||
window.open(parsed.getUrl(opts));
|
||||
});
|
||||
|
||||
sframeChan.on('EV_RTC_PIN_IMAGES', function (list) {
|
||||
Cryptpad.getPadAttribute('ooImages', function (err, res) {
|
||||
if (err) { return; }
|
||||
if (!res || !Array.isArray(res)) { res = []; }
|
||||
var toPin = [];
|
||||
var toUnpin = [];
|
||||
res.forEach(function (id) {
|
||||
if (list.indexOf(id) === -1) {
|
||||
toUnpin.push(id);
|
||||
}
|
||||
});
|
||||
list.forEach(function (id) {
|
||||
if (res.indexOf(id) === -1) {
|
||||
toPin.push(id);
|
||||
}
|
||||
});
|
||||
toPin = Utils.Util.deduplicateString(toPin);
|
||||
toUnpin = Utils.Util.deduplicateString(toUnpin);
|
||||
if (toPin.length) { Cryptpad.pinPads(toPin, function () {}); }
|
||||
if (toUnpin.length) { Cryptpad.unpinPads(toUnpin, function () {}); }
|
||||
if (!toPin.length && !toUnpin.length) { return; }
|
||||
Cryptpad.setPadAttribute('ooImages', list, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return RTC;
|
||||
});
|
||||
|
||||
@ -955,6 +955,7 @@ define([
|
||||
});
|
||||
|
||||
Cryptpad.universal.onEvent.reg(function (data) {
|
||||
if (data?.type === 'rtchannel') { return; }
|
||||
sframeChan.event('EV_UNIVERSAL_EVENT', data);
|
||||
});
|
||||
sframeChan.on('Q_UNIVERSAL_COMMAND', function (content, cb) {
|
||||
@ -2427,6 +2428,9 @@ define([
|
||||
if (burnAfterReading) {
|
||||
nThen(w => {
|
||||
Cryptpad.padRpc.onReadyEvent.reg(w());
|
||||
// XXX XXX XXX
|
||||
// XXX XXX XXX
|
||||
// XXX XXX XXX
|
||||
if (isOO) { sframeChan.on('EV_OO_DOC_READY', w()); }
|
||||
}).nThen(() => {
|
||||
Cryptpad.burnPad({
|
||||
|
||||
2
www/common/worker.bundle.min.js
vendored
2
www/common/worker.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
35
www/notes/app-notes.less
Normal file
35
www/notes/app-notes.less
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
@import (reference) "../../customize/src/less2/include/framework.less";
|
||||
@import (reference) "../../customize/src/less2/include/tools.less";
|
||||
@import (reference) "../../customize/src/less2/include/avatar.less";
|
||||
|
||||
// body
|
||||
&.cp-app-notes {
|
||||
.framework_main(
|
||||
@bg-color: @colortheme_apps[notes],
|
||||
);
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-height: 100%;
|
||||
min-height: auto;
|
||||
color: @cp_app-fg;
|
||||
background-color: @cp_app-bg;
|
||||
|
||||
#cp-app-notes-editor {
|
||||
display: flex;
|
||||
|
||||
#cp-notes-content {
|
||||
flex: 1;
|
||||
textarea {
|
||||
height: 300px;
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
www/notes/index.html
Normal file
29
www/notes/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
|
||||
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
|
||||
<link id="favicon-ico" type="image/x-icon" rel="icon"
|
||||
data-main-favicon="/customize/favicon/main-favicon.ico"
|
||||
data-alt-favicon="/customize/favicon/alt-favicon.ico"
|
||||
href="/customize/favicon/main-favicon.ico">
|
||||
<link id="favicon" type="image/png" rel="icon"
|
||||
data-main-favicon="/customize/favicon/main-favicon.png"
|
||||
data-alt-favicon="/customize/favicon/alt-favicon.png"
|
||||
href="/customize/favicon/main-favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<noscript></noscript>
|
||||
<iframe-placeholder>
|
||||
27
www/notes/inner.html
Normal file
27
www/notes/inner.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="cp-app-noscroll">
|
||||
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/notes/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.7"></script>
|
||||
<style>
|
||||
.loading-hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="cp-app-notes">
|
||||
<div id="cp-toolbar" class="cp-toolbar-container"></div>
|
||||
<div id="cp-app-notes-editor"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
85
www/notes/inner.js
Normal file
85
www/notes/inner.js
Normal file
@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// This is the initialization loading the CryptPad libraries
|
||||
define([
|
||||
'jquery',
|
||||
'/common/sframe-app-framework.js',
|
||||
'/customize/messages.js', // translation keys
|
||||
'/common/hyperscript.js',
|
||||
'less!/notes/app-notes.less'
|
||||
/* Here you can add your own javascript or css to load */
|
||||
], function (
|
||||
$,
|
||||
Framework,
|
||||
Messages,
|
||||
h,
|
||||
) {
|
||||
|
||||
|
||||
// This is the main initialization loop
|
||||
let onFrameworkReady = function (framework) {
|
||||
let $container = $('#cp-app-notes-editor');
|
||||
|
||||
let $content = $(h('div#cp-notes-content')).appendTo($container);
|
||||
let $textarea = $(h('textarea'));
|
||||
$content.append($textarea);
|
||||
let oldVal = '';
|
||||
$textarea.on('change keyup paste', function () {
|
||||
var currentVal = $textarea.val();
|
||||
if (currentVal === oldVal) { return; } // Nothing to do
|
||||
oldVal = currentVal;
|
||||
framework.localChange();
|
||||
});
|
||||
|
||||
let getContent = () => {
|
||||
return $textarea.val();
|
||||
};
|
||||
let setContent = (value) => {
|
||||
return $textarea.val(value);
|
||||
};
|
||||
|
||||
let content = {};
|
||||
|
||||
// OPTIONAL: cursor management
|
||||
framework.setCursorGetter(() => {
|
||||
let myCursor = {};
|
||||
// Get your cursor position here
|
||||
return myCursor;
|
||||
});
|
||||
framework.onCursorUpdate(data => {
|
||||
console.log("Other user cursor", data);
|
||||
});
|
||||
|
||||
framework.onContentUpdate(function (newContent) {
|
||||
console.log('New content received from others', newContent.content);
|
||||
content = newContent.content;
|
||||
setContent(content);
|
||||
});
|
||||
|
||||
framework.setContentGetter(function () {
|
||||
let content = getContent();
|
||||
console.log('Sync my content with others', content);
|
||||
return {
|
||||
content: content
|
||||
};
|
||||
});
|
||||
|
||||
framework.onReady(function () {
|
||||
// Document is ready, you can initialize your app
|
||||
console.log('Document is ready:', content);
|
||||
});
|
||||
|
||||
// Start the framework
|
||||
framework.start();
|
||||
};
|
||||
|
||||
// Framework initialization
|
||||
Framework.create({
|
||||
toolbarContainer: '#cp-toolbar',
|
||||
contentContainer: '#cp-app-notes-editor'
|
||||
}, (framework) => {
|
||||
onFrameworkReady(framework);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user