ooconnector: callback command retreival system (#525)

This commit is contained in:
Eric Doughty-Papassideris
2024-09-15 01:25:59 +02:00
parent 1d81c94d47
commit 5cf1fa581e
3 changed files with 209 additions and 4 deletions
@@ -1,6 +1,8 @@
import { randomUUID } from 'crypto';
import axios from 'axios';
import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS } from '@config';
import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS, onlyOfficeCallbackTimeoutMS } from '@config';
import { PolledThingieValue } from '@/lib/polled-thingie-value';
import { PendingRequestQueue, PendingRequestCallback } from '@/lib/pending-request-matcher';
import logger from '@/lib/logger';
import * as Utils from '@/utils';
@@ -14,6 +16,12 @@ export enum ErrorCode {
COMMAND_NOT_CORRECT = 5,
INVALID_TOKEN = 6,
}
/** Error generated by this connector. Should be string to share field with {@link ErrorCode} */
export enum ConnectorErrorCode {
CALLBACK_TIMEOUT = 'callback_timeout',
}
/** Return the name of the error code in the `ErrorCode` enum if recognised, or a descript string */
export const ErrorCodeFromValue = (value: number) => Utils.getKeyForValueSafe(value, ErrorCode, 'OnlyOffice.ErrorCode');
@@ -59,6 +67,7 @@ export namespace Callback {
url?: string;
actions?: Action[];
users?: string[];
userdata?: string;
}
}
@@ -93,7 +102,7 @@ namespace CommandService {
async postUnsafe(): Promise<ErrorResponse | TSuccessResponse> {
logger.silly(`OnlyOffice command ${this.c} sent: ${JSON.stringify(this)}`);
const result = await axios.post(Utils.joinURL([ONLY_OFFICE_SERVER, 'coauthoring/CommandService.ashx']), this);
logger.info(`OnlyOffice command ${this.c} response: ${result.status}: ${JSON.stringify(result.data)}`);
logger.info(`OnlyOffice command ${this.c} response ${result.status}: ${ErrorCodeFromValue(result.data.error)}: ${JSON.stringify(result.data)}`);
return result.data as ErrorResponse | TSuccessResponse;
}
@@ -174,6 +183,32 @@ namespace CommandService {
}
}
}
export namespace Info {
export type Response = SuccessResponse;
export class Request extends BaseRequest<Response> {
constructor(public readonly key: string, public readonly userdata: string = '') {
super('info');
}
}
}
}
/**
* This object holds possible outcomes for commands like `info` who's result
* is sent to the callback instead of replied to the request
*/
class CallbackResponseFromCommand {
/**
* If the `info` command returned an error code, this is it, or it can
* be an internal error to this connector (eg.: timeout waiting for callback).
* (If the `info` command returned {@link ErrorCode.SUCCESS} then this field will be `undefined`)
*/
public readonly error: Exclude<ErrorCode, ErrorCode.SUCCESS> | ConnectorErrorCode | undefined;
public constructor(error: ErrorCode | ConnectorErrorCode, public readonly result?: Callback.Parameters) {
this.error = error === ErrorCode.SUCCESS ? undefined : error;
}
}
/**
@@ -182,6 +217,8 @@ namespace CommandService {
*/
class OnlyOfficeService {
private readonly poller: PolledThingieValue<CommandService.License.Response>;
// Technically the timeout field is from the PendingRequestQueue but avoid 2 classes
private readonly pendingRequests = new PendingRequestQueue<CallbackResponseFromCommand>(onlyOfficeCallbackTimeoutMS);
constructor() {
this.poller = new PolledThingieValue(
@@ -227,6 +264,20 @@ class OnlyOfficeService {
return deleted;
}
/** Generates and returns a random UUID that has a matching pending task enqueued for */
private enqueuePendingCallback(key: string, callback: PendingRequestCallback<CallbackResponseFromCommand>): string {
const userdata = randomUUID();
this.pendingRequests.enqueue(key, userdata, callback);
return userdata;
}
/** Called by the OnlyOffice controller when the OO document editing services uses our callback */
async ooCallbackCalled(result: Callback.Parameters) {
if (!result.userdata) return;
logger.info('OO Callback pending request response received', result);
return this.pendingRequests.gotResult(result.key, result.userdata, new CallbackResponseFromCommand(ErrorCode.SUCCESS, result));
}
// Note that `async` is important in the functions below. While they avoid the overhead
// of `await`, the `async` is still required to catch the throw in `.post()`
@@ -239,6 +290,40 @@ class OnlyOfficeService {
//TODO: When typing the response more fully, don't return the response object itself as here
return new CommandService.License.Request().post();
}
/**
* Requests a document status and the list of the identifiers of the users who opened the document for editing.
* The response will be sent to the callback handler.
* This method just sends the command. The response from the callback will be ignored if
* this called by itself
*
* *Warning*: returns non succesful error codes instead of throwing errors like the other
* methods. This is because the immediate response is likely to be itself usefull
* for detecting errors.
*/
async getInfoUnsafe(key: string, userdata: string): Promise<ErrorCode> {
return await new CommandService.Info.Request(key, userdata).postUnsafe().then(({ error }) => error);
}
async getInfoAndWaitForCallbackUnsafe(key: string): Promise<CallbackResponseFromCommand> {
// const userdata = randomUUID();
return new Promise((resolve, reject) => {
const userdata = this.enqueuePendingCallback(key, async (timeout, result) => {
// The callback has called, unless timeout = true, or result is undefined (cancelled request)
return resolve(timeout ? new CallbackResponseFromCommand(ConnectorErrorCode.CALLBACK_TIMEOUT) : result);
});
void this.getInfoUnsafe(key, userdata).then(
response => {
// The command service responded to the `info` command request
if (response !== ErrorCode.SUCCESS) return this.pendingRequests.gotResult(key, userdata, new CallbackResponseFromCommand(response));
// If it succeded, just wait for timeout or resolution by the callback
},
error => {
this.pendingRequests.cancelPending(key, userdata).then(() => reject(error));
},
);
});
}
/** Force a save in the editing session key provided. `userdata` will be forwarded to the callback */
async forceSave(key: string, userdata = ''): Promise<string> {
return new CommandService.ForceSave.Request(key, userdata).post().then(response => response.key);