♻️ oo-connector: wip on callbacks and key reactions, and minor cleanup (#525)
This commit is contained in:
+19
-24
@@ -7,18 +7,6 @@ interface RequestQuery {
|
|||||||
editing_session_key: string;
|
editing_session_key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ignoreMissingKeyErrorButNoneElse(res: Response, call: () => Promise<void>): Promise<void> {
|
|
||||||
try {
|
|
||||||
await call();
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof CommandError && e.errorCode == ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND) {
|
|
||||||
return void (await res.send({ info: 'Unknown editing_session_key' }));
|
|
||||||
}
|
|
||||||
logger.error('Running OO command for TDrive backend', e);
|
|
||||||
return void (await res.sendStatus(500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These routes are called by Twake Drive backend, for ex. before editing or retreiving a file,
|
* These routes are called by Twake Drive backend, for ex. before editing or retreiving a file,
|
||||||
* if it has an editing_session_key still, get the status of that and force a resolution.
|
* if it has an editing_session_key still, get the status of that and force a resolution.
|
||||||
@@ -30,34 +18,42 @@ export default class TwakeDriveBackendCallbackController {
|
|||||||
* be considered lost after an admin alert.
|
* be considered lost after an admin alert.
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
|
* - `{ status: 'unknown' }`: the key isn't known and maybe used for a new session
|
||||||
* - `{ status: 'updated' }`: the key needed updating but is now invalid
|
* - `{ status: 'updated' }`: the key needed updating but is now invalid
|
||||||
* - `{ status: 'expired' }`: the key can't be used (it is verified unknown)
|
* - `{ status: 'expired' }`: the key was already used in a finished session and can't be used again
|
||||||
* - `{ status: 'live' }`: the key is valid and current and should be used again for the same file
|
* - `{ status: 'live' }`: the key is valid and current and should be used again for the same file
|
||||||
* - `{ error: number }`: there was an error retreiving the status of the key, http status `!= 200`
|
* - `{ error: number }`: there was an error retreiving the status of the key, http status `!= 200`
|
||||||
*/
|
*/
|
||||||
public async checkSessionStatus(req: Request<RequestQuery>, res: Response): Promise<void> {
|
public async checkSessionStatus(req: Request<RequestQuery>, res: Response): Promise<void> {
|
||||||
//TODO: check there is auth from backend before this is ran
|
|
||||||
|
|
||||||
// have to get forgotten first, if it's there it's definitive,
|
|
||||||
// but if we paralelise it risks calling the callback
|
|
||||||
try {
|
try {
|
||||||
const forgottenURL = await onlyofficeService.getForgotten(req.params.editing_session_key);
|
const forgottenURL = await onlyofficeService.getForgotten(req.params.editing_session_key);
|
||||||
// Run upload before returning
|
try {
|
||||||
|
await driveService.endEditing(req.params.editing_session_key, forgottenURL);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`endEditing failed`, { error });
|
||||||
|
return void res.status(502).send({ error: -57649 });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await onlyofficeService.deleteForgotten(req.params.editing_session_key);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`deleteForgotten failed`, { error });
|
||||||
|
return void res.status(502).send({ error: -57650 });
|
||||||
|
}
|
||||||
return void res.send({ status: 'updated' });
|
return void res.send({ status: 'updated' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof CommandError && e.errorCode == ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND)) {
|
if (!(e instanceof CommandError && e.errorCode == ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND)) {
|
||||||
logger.error(`getForgotten failed`, e);
|
logger.error(`getForgotten failed`, { error: e });
|
||||||
return void res.status(e instanceof CommandError ? 502 : 500).send({ error: -51 });
|
return void res.status(e instanceof CommandError ? 502 : 500).send({ error: -57651 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const info = await onlyofficeService.getInfoAndWaitForCallbackUnsafe(req.params.editing_session_key);
|
const info = await onlyofficeService.getInfoAndWaitForCallbackUnsafe(req.params.editing_session_key);
|
||||||
if (info.error === ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND) {
|
if (info.error === ErrorCode.KEY_MISSING_OR_DOC_NOT_FOUND) {
|
||||||
// just cancel it
|
// just start using it
|
||||||
return void res.send({ status: 'expired' });
|
return void res.send({ status: 'unknown' });
|
||||||
}
|
}
|
||||||
if (info.error !== undefined) {
|
if (info.error !== undefined) {
|
||||||
logger.error(`getInfo failed`, { error: info });
|
logger.error(`getInfo failed`, { error: info });
|
||||||
return void res.status(502).send({ error: -52 });
|
return void res.status(502).send({ error: -57652 });
|
||||||
}
|
}
|
||||||
switch (info.result.status) {
|
switch (info.result.status) {
|
||||||
case Callback.Status.BEING_EDITED:
|
case Callback.Status.BEING_EDITED:
|
||||||
@@ -75,7 +71,6 @@ export default class TwakeDriveBackendCallbackController {
|
|||||||
|
|
||||||
case Callback.Status.READY_FOR_SAVING:
|
case Callback.Status.READY_FOR_SAVING:
|
||||||
// upload it, have to do it here for correct user stored in url in OO
|
// upload it, have to do it here for correct user stored in url in OO
|
||||||
//TODO: Need to fix so company_id is not needed by ooconnector but parsed from key server side
|
|
||||||
await driveService.endEditing(req.params.editing_session_key, info.result.url);
|
await driveService.endEditing(req.params.editing_session_key, info.result.url);
|
||||||
return void res.send({ status: 'updated' });
|
return void res.send({ status: 'updated' });
|
||||||
|
|
||||||
|
|||||||
@@ -65,61 +65,70 @@ class OnlyOfficeController {
|
|||||||
const { url, key } = req.body;
|
const { url, key } = req.body;
|
||||||
const { token } = req.query;
|
const { token } = req.query;
|
||||||
logger.info(
|
logger.info(
|
||||||
`OO callback status: ${Utils.getKeyForValueSafe(
|
`OO callback status: ${Utils.getKeyForValueSafe(req.body.status, OnlyOffice.Callback.Status, 'Callback.Status')} - ${JSON.stringify(
|
||||||
req.body.status,
|
req.body.userdata,
|
||||||
OnlyOffice.Callback.Status,
|
)}`,
|
||||||
'OnlyOffice.Callback.Status',
|
|
||||||
)} - ${JSON.stringify(req.body.userdata)}`,
|
|
||||||
req.body,
|
req.body,
|
||||||
);
|
);
|
||||||
const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken;
|
const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken;
|
||||||
const { preview, company_id, file_id, /* user_id, */ drive_file_id, in_page_token, editing_session_key } = officeTokenPayload;
|
const { preview, /* company_id, file_id, user_id, drive_file_id, */ in_page_token /* editing_session_key */ } = officeTokenPayload;
|
||||||
|
|
||||||
// Ignore errors generated by pending request
|
// Ignore errors generated by pending request
|
||||||
// try-catch not needed because it is async
|
// try-catch not needed because it is async
|
||||||
// there may be later reasons to wait for callbacks
|
// there may be later reasons to wait for callbacks
|
||||||
// to process and eventually respond accordingly to
|
// to process and eventually respond accordingly to
|
||||||
// OO an error for certain statuses
|
// OO an error for certain statuses
|
||||||
void OnlyOffice.default.ooCallbackCalled(req.body);
|
void OnlyOffice.default.ooCallbackCalled(req.body); // has to be single thread per key
|
||||||
|
|
||||||
// check token is an in_page_token and allow save
|
// check token is an in_page_token and allow save
|
||||||
if (!in_page_token) throw new Error('Invalid token, must be a in_page_token');
|
if (!in_page_token) throw new Error('OO Callback invalid token, must be a in_page_token');
|
||||||
if (preview) throw new Error('Invalid token, must not be a preview token for save operation');
|
if (preview) throw new Error('OO Callback invalid token, must not be a preview token for save operation');
|
||||||
|
|
||||||
switch (req.body.status) {
|
switch (req.body.status) {
|
||||||
case OnlyOffice.Callback.Status.BEING_EDITED:
|
case OnlyOffice.Callback.Status.BEING_EDITED:
|
||||||
// TODO this call back we recieve almost all the time, and here we save
|
// TODO this call back we recieve almost all the time, and here we save
|
||||||
// the user identifiers who start file editing and even control the amount of onlin users
|
// the user identifiers who start file editing and even control the amount of onlin users
|
||||||
// to have license constraint warning before OnlyOffice error about this
|
// to have license constraint warning before OnlyOffice error about this
|
||||||
case OnlyOffice.Callback.Status.BEING_EDITED_BUT_IS_SAVED:
|
|
||||||
// No-op
|
// No-op
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case OnlyOffice.Callback.Status.BEING_EDITED_BUT_IS_SAVED:
|
||||||
|
logger.info(`OO Callback force save for session ${key} for reason: ${OnlyOffice.Callback.ForceSaveTypeToString(req.body.forcesavetype)}`);
|
||||||
|
await driveService.addEditingSessionVersion(key, url); //, token); //TODO Fix user token (getting 401)
|
||||||
|
break;
|
||||||
|
|
||||||
case OnlyOffice.Callback.Status.READY_FOR_SAVING:
|
case OnlyOffice.Callback.Status.READY_FOR_SAVING:
|
||||||
logger.info(`New version for session ${editing_session_key} created`);
|
logger.info(`OO Callback new version for session ${key} created`);
|
||||||
return respondToOO();
|
await driveService.endEditing(key, url); //, token); //TODO Fix user token (getting 401)
|
||||||
await driveService.endEditing(editing_session_key, url);
|
break;
|
||||||
|
|
||||||
case OnlyOffice.Callback.Status.CLOSED_WITHOUT_CHANGES:
|
case OnlyOffice.Callback.Status.CLOSED_WITHOUT_CHANGES:
|
||||||
// Save end of transaction
|
await driveService.cancelEditing(key);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OnlyOffice.Callback.Status.ERROR_SAVING:
|
case OnlyOffice.Callback.Status.ERROR_SAVING:
|
||||||
// Save end of transaction
|
// TODO: Save end of transaction ?
|
||||||
logger.info(`Error saving file ${req.body.url} (key: ${req.body.key})`);
|
logger.error(`OO Callback with Status.ERROR_SAVING: ${req.body.url} (key: ${req.body.key})`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OnlyOffice.Callback.Status.ERROR_FORCE_SAVING:
|
case OnlyOffice.Callback.Status.ERROR_FORCE_SAVING:
|
||||||
// TODO: notify user ?
|
// TODO: notify user ?
|
||||||
logger.info(`Error force saving (reason: ${req.body.forcesavetype}) file ${req.body.url} (key: ${req.body.key})`);
|
logger.error(
|
||||||
return void res.send({ error: 0 });
|
`OO Callback with Status.ERROR_FORCE_SAVING (reason: ${OnlyOffice.Callback.ForceSaveTypeToString(req.body.forcesavetype)}) file ${
|
||||||
|
req.body.url
|
||||||
|
} (key: ${req.body.key})`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected OO Callback status field: ${req.body.status}`);
|
throw new Error(
|
||||||
|
`OO Callback unexpected status field: ${OnlyOffice.Callback.StatusToString(req.body.status)} in ${JSON.stringify(req.body)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return respondToOO();
|
return respondToOO(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
logger.error(`OO Callback root error`, { error });
|
||||||
|
next(error || 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export namespace Callback {
|
|||||||
USER_CONNECTED = 1,
|
USER_CONNECTED = 1,
|
||||||
USER_INITIATED_FORCE_SAVE = 2,
|
USER_INITIATED_FORCE_SAVE = 2,
|
||||||
}
|
}
|
||||||
|
export const ActionTypeToString = (value: number) => Utils.getKeyForValueSafe(value, ActionType, 'OnlyOffice.Callback.ActionType');
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
type: ActionType;
|
type: ActionType;
|
||||||
@@ -44,6 +45,7 @@ export namespace Callback {
|
|||||||
SERVER_TIMER = 2,
|
SERVER_TIMER = 2,
|
||||||
FORM_SUBMITTED = 3,
|
FORM_SUBMITTED = 3,
|
||||||
}
|
}
|
||||||
|
export const ForceSaveTypeToString = (value: number) => Utils.getKeyForValueSafe(value, ForceSaveType, 'OnlyOffice.Callback.ForceSaveType');
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
BEING_EDITED = 1,
|
BEING_EDITED = 1,
|
||||||
@@ -57,6 +59,7 @@ export namespace Callback {
|
|||||||
/** `url` and `forcesavetype` fields present with this status */
|
/** `url` and `forcesavetype` fields present with this status */
|
||||||
ERROR_FORCE_SAVING = 7,
|
ERROR_FORCE_SAVING = 7,
|
||||||
}
|
}
|
||||||
|
export const StatusToString = (value: number) => Utils.getKeyForValueSafe(value, Status, 'OnlyOffice.Callback.Status');
|
||||||
|
|
||||||
/** Parameters given to the callback by the editing service */
|
/** Parameters given to the callback by the editing service */
|
||||||
export interface Parameters {
|
export interface Parameters {
|
||||||
@@ -305,6 +308,7 @@ class OnlyOfficeService {
|
|||||||
return await new CommandService.Info.Request(key, userdata).postUnsafe().then(({ error }) => error);
|
return await new CommandService.Info.Request(key, userdata).postUnsafe().then(({ error }) => error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Send the info command, wait for the callback if warranted, and return the error or callback body */
|
||||||
async getInfoAndWaitForCallbackUnsafe(key: string): Promise<CallbackResponseFromCommand> {
|
async getInfoAndWaitForCallbackUnsafe(key: string): Promise<CallbackResponseFromCommand> {
|
||||||
// const userdata = randomUUID();
|
// const userdata = randomUUID();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user