262 lines
8.8 KiB
TypeScript
262 lines
8.8 KiB
TypeScript
import axios from 'axios';
|
|
import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS } from '@config';
|
|
import { PolledThingieValue } from '@/lib/polled-thingie-value';
|
|
import logger from '@/lib/logger';
|
|
import * as Utils from '@/utils';
|
|
|
|
/** @see https://api.onlyoffice.com/editors/basic */
|
|
export enum ErrorCode {
|
|
SUCCESS = 0,
|
|
KEY_MISSING_OR_DOC_NOT_FOUND = 1,
|
|
INVALID_CALLBACK_URL = 2,
|
|
INTERNAL_SERVER_ERROR = 3,
|
|
FORCE_SAVE_BUT_NO_CHANGES_TO_APPLY = 4,
|
|
COMMAND_NOT_CORRECT = 5,
|
|
INVALID_TOKEN = 6,
|
|
}
|
|
/** 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');
|
|
|
|
/** @see https://api.onlyoffice.com/editors/callback */
|
|
export namespace Callback {
|
|
enum ActionType {
|
|
USER_DISCONNECTED = 0,
|
|
USER_CONNECTED = 1,
|
|
USER_INITIATED_FORCE_SAVE = 2,
|
|
}
|
|
|
|
interface Action {
|
|
type: ActionType;
|
|
userid: string;
|
|
}
|
|
|
|
enum ForceSaveType {
|
|
FROM_COMMAND_SERVICE = 0,
|
|
FORCE_SAVE_BUTTON_CLICKED = 1,
|
|
SERVER_TIMER = 2,
|
|
FORM_SUBMITTED = 3,
|
|
}
|
|
|
|
export enum Status {
|
|
BEING_EDITED = 1,
|
|
/** `url` field present with this status */
|
|
READY_FOR_SAVING = 2,
|
|
/** `url` field present with this status */
|
|
ERROR_SAVING = 3,
|
|
CLOSED_WITHOUT_CHANGES = 4,
|
|
/** `url` and `forcesavetype` fields present with this status */
|
|
BEING_EDITED_BUT_IS_SAVED = 6,
|
|
/** `url` and `forcesavetype` fields present with this status */
|
|
ERROR_FORCE_SAVING = 7,
|
|
}
|
|
|
|
/** Parameters given to the callback by the editing service */
|
|
export interface Parameters {
|
|
key: string;
|
|
status: Status;
|
|
filetype?: string;
|
|
forcesavetype?: ForceSaveType;
|
|
url?: string;
|
|
actions?: Action[];
|
|
users?: string[];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helpers to define the protocol of the OnlyOffice editor service command API
|
|
* @see https://api.onlyoffice.com/editors/command/
|
|
*/
|
|
namespace CommandService {
|
|
interface BaseResponse {
|
|
error: ErrorCode;
|
|
}
|
|
interface SuccessResponse extends BaseResponse {
|
|
error: ErrorCode.SUCCESS;
|
|
}
|
|
interface ErrorResponse extends BaseResponse {
|
|
error: Exclude<ErrorCode, ErrorCode.SUCCESS>;
|
|
}
|
|
|
|
export class CommandError extends Error {
|
|
constructor(errorCode: ErrorCode, req: any, res: any) {
|
|
super(
|
|
`OnlyOffice command service error ${ErrorCodeFromValue(errorCode)} (${errorCode}): Requested ${JSON.stringify(req)} got ${JSON.stringify(
|
|
res,
|
|
)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
abstract class BaseRequest<TSuccessResponse extends SuccessResponse> {
|
|
constructor(public readonly c: string) {}
|
|
|
|
/** POST this OnlyOffice command, does not check the `error` field of the response */
|
|
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)}`);
|
|
return result.data as ErrorResponse | TSuccessResponse;
|
|
}
|
|
|
|
/** POST this request, and return the result, or throws if the `errorCode` returned isn't `ErrorCode.SUCCESS` */
|
|
async post(): Promise<TSuccessResponse> {
|
|
const result = await this.postUnsafe();
|
|
if (result.error === ErrorCode.SUCCESS) return result;
|
|
throw new CommandError(result.error, this, result);
|
|
}
|
|
}
|
|
|
|
export namespace Version {
|
|
interface Response extends SuccessResponse {
|
|
version: string;
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor() {
|
|
super('version');
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace ForceSave {
|
|
interface Response extends SuccessResponse {
|
|
key: string;
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor(public readonly key: string, public readonly userdata: string = '') {
|
|
super('forcesave');
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace GetForgotten {
|
|
interface Response extends SuccessResponse {
|
|
key: string;
|
|
url: string;
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor(public readonly key: string) {
|
|
super('getForgotten');
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace GetForgottenList {
|
|
interface Response extends SuccessResponse {
|
|
keys: string[];
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor() {
|
|
super('getForgottenList');
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace DeleteForgotten {
|
|
interface Response extends SuccessResponse {
|
|
key: string;
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor(public readonly key: string) {
|
|
super('deleteForgotten');
|
|
}
|
|
}
|
|
}
|
|
|
|
export namespace License {
|
|
export interface Response extends SuccessResponse {
|
|
// @see https://api.onlyoffice.com/editors/command/license
|
|
license: object;
|
|
server: object;
|
|
quota: object;
|
|
}
|
|
export class Request extends BaseRequest<Response> {
|
|
constructor() {
|
|
super('license');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exposed OnlyOffice command service
|
|
* @see https://api.onlyoffice.com/editors/command/
|
|
*/
|
|
class OnlyOfficeService {
|
|
private readonly poller: PolledThingieValue<CommandService.License.Response>;
|
|
|
|
constructor() {
|
|
this.poller = new PolledThingieValue(
|
|
'Connect to Only Office',
|
|
async () => {
|
|
logger.info('Only Office license status');
|
|
return await this.getLicense();
|
|
},
|
|
onlyOfficeConnectivityCheckPeriodMS,
|
|
);
|
|
}
|
|
/** Get the latest Only Office licence status from polling. If the return is `undefined`
|
|
* it probably means there is a connection issue contacting the OnlyOffice server
|
|
* from the connector.
|
|
*/
|
|
public getLatestLicence() {
|
|
return this.poller.latest();
|
|
}
|
|
|
|
/**
|
|
* Iterate over all the forgotten files as returned by OnlyOffice, call the processor for each..
|
|
* @param processor Handler to process the forgotten file (available at `url`). If `true` is returned,
|
|
* the file is deleted from the forgotten file list in OnlyOffice. If false is returned, the
|
|
* same forgotten file will reappear in a future batch
|
|
* @returns The number of files processed and deleted
|
|
*/
|
|
public async processForgotten(processor: (key: string, url: string) => Promise<boolean>): Promise<number> {
|
|
const forgottenFiles = await this.getForgottenList();
|
|
if (forgottenFiles.length === 0) return 0;
|
|
Utils.fisherYattesShuffleInPlace(forgottenFiles);
|
|
logger.info(`Forgotten files found: ${forgottenFiles.length}`);
|
|
let deleted = 0;
|
|
for (const forgottenFileKey of forgottenFiles) {
|
|
const forgottenFileURL = await this.getForgotten(forgottenFileKey);
|
|
logger.info(`Forgotten file about to process: ${JSON.stringify(forgottenFileKey)}`, { url: forgottenFileURL });
|
|
const shouldDelete = await processor(forgottenFileKey, forgottenFileURL);
|
|
if (shouldDelete) {
|
|
logger.info(`Forgotten file about to be deleted: ${JSON.stringify(forgottenFileKey)}`);
|
|
await this.deleteForgotten(forgottenFileKey);
|
|
deleted++;
|
|
}
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
// 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()`
|
|
|
|
/** Return the version string of OnlyOffice */
|
|
async getVersion(): Promise<string> {
|
|
return new CommandService.Version.Request().post().then(response => response.version);
|
|
}
|
|
/** Return the version string of OnlyOffice */
|
|
async getLicense(): Promise<CommandService.License.Response> {
|
|
//TODO: When typing the response more fully, don't return the response object itself as here
|
|
return new CommandService.License.Request().post();
|
|
}
|
|
/** 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);
|
|
}
|
|
/** Return the keys of all forgotten documents in OnlyOffice's document editing service */
|
|
async getForgottenList(): Promise<string[]> {
|
|
return new CommandService.GetForgottenList.Request().post().then(response => response.keys);
|
|
}
|
|
/** Return the url of a forgotten document specified by its key in OnlyOffice's document editing service */
|
|
async getForgotten(key: string): Promise<string> {
|
|
return new CommandService.GetForgotten.Request(key).post().then(response => response.url);
|
|
}
|
|
/** Delete a forgotten document specified by its key in OnlyOffice's document editing service */
|
|
async deleteForgotten(key: string): Promise<string> {
|
|
return new CommandService.DeleteForgotten.Request(key).post().then(response => response.key);
|
|
}
|
|
}
|
|
|
|
export default new OnlyOfficeService();
|