♻️🎨 connector: light cleanup, load (#525)
renaming logger, adding health endpoint, working with old API, removing aggressive force save stuff, missing forgotten files etc, documentation
This commit is contained in:
committed by
ericlinagora
parent
16dbf8077b
commit
92aad866d5
@@ -5,31 +5,34 @@ import {
|
||||
IApiServiceApplicationTokenResponse,
|
||||
} from '@/interfaces/api.interface';
|
||||
import axios, { Axios, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET, ONLY_OFFICE_SERVER } from '@config';
|
||||
import loggerService from './logger.service';
|
||||
import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET } from '@config';
|
||||
import logger from '../lib/logger';
|
||||
import * as Utils from '@/utils';
|
||||
import { PolledThingieValue } from '@/lib/polled-thingie-value';
|
||||
|
||||
/** Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters) */
|
||||
/**
|
||||
* Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters).
|
||||
* Periodically updates authorization and adds to requests.
|
||||
*/
|
||||
class ApiService implements IApiService {
|
||||
private axios: Axios;
|
||||
private initialized: Promise<string>;
|
||||
private readonly poller: PolledThingieValue<Axios>;
|
||||
|
||||
constructor() {
|
||||
this.initialized = this.refreshToken();
|
||||
this.initialized.catch(error => {
|
||||
loggerService.error('failed to init API', error);
|
||||
});
|
||||
this.poller = new PolledThingieValue('Refresh Twake Drive token', async () => this.refreshToken(), 1000 * 60); //TODO: should be Every 10 minutes
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
this.initialized = this.refreshToken();
|
||||
loggerService.info('Refreshing token 🪙');
|
||||
}, 1000 * 60); //TODO: should be Every 10 minutes
|
||||
public async hasToken() {
|
||||
return (await this.poller.latestValueWithTry()) !== undefined;
|
||||
}
|
||||
|
||||
private requireAxios() {
|
||||
return this.poller.requireLatestValueWithTry('Token Kind 538 not ready');
|
||||
}
|
||||
|
||||
public get = async <T>(params: IApiServiceRequestParams<T>): Promise<T> => {
|
||||
const { url, token, responseType, headers } = params;
|
||||
|
||||
await this.initialized;
|
||||
const axiosWithToken = await this.requireAxios();
|
||||
|
||||
const config: AxiosRequestConfig = {};
|
||||
|
||||
@@ -43,35 +46,35 @@ class ApiService implements IApiService {
|
||||
if (responseType) {
|
||||
config['responseType'] = responseType;
|
||||
}
|
||||
|
||||
return await this.axios.get(url, config);
|
||||
return await axiosWithToken.get(url, config);
|
||||
};
|
||||
|
||||
public post = async <T, R>(params: IApiServiceRequestParams<T>): Promise<R> => {
|
||||
const { url, payload, headers } = params;
|
||||
|
||||
await this.initialized;
|
||||
const axiosWithToken = await this.requireAxios();
|
||||
|
||||
try {
|
||||
return await this.axios.post(url, payload, {
|
||||
return await axiosWithToken.post(url, payload, {
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to post: ', error.message);
|
||||
logger.error('Failed to post to Twake drive: ', error.stack);
|
||||
this.refreshToken();
|
||||
}
|
||||
};
|
||||
|
||||
private handleErrors = (error: any): Promise<any> => {
|
||||
loggerService.error('Failed Request', error.message);
|
||||
logger.error('Failed Request to Twake drive', error.stack);
|
||||
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
private handleResponse = <T>({ data }: AxiosResponse): T => data;
|
||||
|
||||
private refreshToken = async (): Promise<string> => {
|
||||
private refreshToken = async (): Promise<Axios> => {
|
||||
try {
|
||||
const response = await axios.post<IApiServiceApplicationTokenRequestParams, { data: IApiServiceApplicationTokenResponse }>(
|
||||
Utils.joinURL([CREDENTIALS_ENDPOINT, '/api/console/v1/login']),
|
||||
@@ -92,25 +95,24 @@ class ApiService implements IApiService {
|
||||
},
|
||||
} = response.data;
|
||||
|
||||
this.axios = axios.create({
|
||||
const axiosWithToken = axios.create({
|
||||
baseURL: CREDENTIALS_ENDPOINT,
|
||||
headers: {
|
||||
Authorization: `Bearer ${value}`,
|
||||
},
|
||||
});
|
||||
|
||||
this.axios.interceptors.response.use(this.handleResponse, this.handleErrors);
|
||||
axiosWithToken.interceptors.response.use(this.handleResponse, this.handleErrors);
|
||||
|
||||
return value;
|
||||
return axiosWithToken;
|
||||
} catch (error) {
|
||||
loggerService.error('failed to get application token', error.message);
|
||||
loggerService.info('Using token ', CREDENTIALS_ID, CREDENTIALS_SECRET);
|
||||
loggerService.info(`POST ${CREDENTIALS_ENDPOINT.replace(/\/$/, '')}/api/console/v1/login`);
|
||||
loggerService.info(`Basic ${Buffer.from(`${CREDENTIALS_ID}:${CREDENTIALS_SECRET}`).toString('base64')}`);
|
||||
logger.error('failed to get application token from Twake drive', error.stack);
|
||||
logger.info('Using token ', CREDENTIALS_ID, CREDENTIALS_SECRET);
|
||||
logger.info(`POST ${CREDENTIALS_ENDPOINT.replace(/\/$/, '')}/api/console/v1/login`);
|
||||
logger.info(`Basic ${Buffer.from(`${CREDENTIALS_ID}:${CREDENTIALS_SECRET}`).toString('base64')}`);
|
||||
throw Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default new ApiService();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DriveFileType, IDriveService } from '@/interfaces/drive.interface';
|
||||
import apiService from './api.service';
|
||||
import loggerService from './logger.service';
|
||||
import logger from '../lib/logger';
|
||||
|
||||
/** Client for the Twake Drive backend API dealing with `DriveItem`s */
|
||||
/** Client for Twake Drive's APIs dealing with `DriveItem`s, using {@see apiService}
|
||||
* to handle authorization
|
||||
*/
|
||||
class DriveService implements IDriveService {
|
||||
public get = async (params: { company_id: string; drive_file_id: string; user_token?: string }): Promise<DriveFileType> => {
|
||||
try {
|
||||
@@ -14,7 +16,7 @@ class DriveService implements IDriveService {
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to fetch file metadata: ', error.message);
|
||||
logger.error('Failed to fetch file metadata: ', error.stack);
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
@@ -41,7 +43,7 @@ class DriveService implements IDriveService {
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to create version: ', error.message);
|
||||
logger.error('Failed to create version: ', error.stack);
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { FileRequestParams, FileType, IFileService } from '@/interfaces/file.interface';
|
||||
import apiService from './api.service';
|
||||
import loggerService from './logger.service';
|
||||
import logger from '../lib/logger';
|
||||
import { Stream } from 'stream';
|
||||
import FormData from 'form-data';
|
||||
import * as Utils from '@/utils';
|
||||
|
||||
/** Client for Twake Drive's file related APIs, using {@see apiService}
|
||||
* to handle authorization
|
||||
*/
|
||||
class FileService implements IFileService {
|
||||
|
||||
public get = async (params: FileRequestParams): Promise<FileType> => {
|
||||
try {
|
||||
const { company_id, file_id } = params;
|
||||
@@ -16,7 +18,7 @@ class FileService implements IFileService {
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to fetch file metadata: ', error.message);
|
||||
logger.error('Failed to fetch file metadata from Twake Drive: ', error.stack);
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
@@ -32,7 +34,7 @@ class FileService implements IFileService {
|
||||
|
||||
return file;
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to download file: ', error.message);
|
||||
logger.error('Failed to download file from Twake Drive: ', error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +68,7 @@ class FileService implements IFileService {
|
||||
filename,
|
||||
});
|
||||
|
||||
loggerService.info('Saving file version: ', filename);
|
||||
logger.info('Saving file version to Twake Drive: ', filename);
|
||||
|
||||
return await apiService.post<any, { resource: FileType }>({
|
||||
url: create_new
|
||||
@@ -76,7 +78,7 @@ class FileService implements IFileService {
|
||||
headers: form.getHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to save file: ', error.message);
|
||||
logger.error('Failed to save file: ', error.stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Logger } from 'tslog';
|
||||
|
||||
export default new Logger({
|
||||
name: 'twake-onlyoffice-plugin',
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios, { Axios, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { ONLY_OFFICE_SERVER } from '@config';
|
||||
import loggerService from './logger.service';
|
||||
import { PolledThingieValue } from '@/lib/polled-thingie-value';
|
||||
import logger from '@/lib/logger';
|
||||
import * as Utils from '@/utils';
|
||||
|
||||
/** @see https://api.onlyoffice.com/editors/basic */
|
||||
@@ -14,7 +15,7 @@ export enum ErrorCode {
|
||||
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");
|
||||
export const ErrorCodeFromValue = (value: number) => Utils.getKeyForValueSafe(value, ErrorCode, 'OnlyOffice.ErrorCode');
|
||||
|
||||
/** @see https://api.onlyoffice.com/editors/callback */
|
||||
export namespace Callback {
|
||||
@@ -22,7 +23,7 @@ export namespace Callback {
|
||||
USER_DISCONNECTED = 0,
|
||||
USER_CONNECTED = 1,
|
||||
USER_INITIATED_FORCE_SAVE = 2,
|
||||
};
|
||||
}
|
||||
|
||||
enum ForceSaveType {
|
||||
FROM_COMMAND_SERVICE = 0,
|
||||
@@ -31,7 +32,7 @@ export namespace Callback {
|
||||
FORM_SUBMITTED = 3,
|
||||
}
|
||||
|
||||
enum Status {
|
||||
export enum Status {
|
||||
BEING_EDITED = 1,
|
||||
/** `url` field present with this status */
|
||||
READY_FOR_SAVING = 2,
|
||||
@@ -49,7 +50,7 @@ export namespace Callback {
|
||||
userid: string;
|
||||
}
|
||||
/** Parameters given to the callback by the editing service */
|
||||
interface Parameters {
|
||||
export interface Parameters {
|
||||
key: string;
|
||||
status: Status;
|
||||
filetype?: string;
|
||||
@@ -65,13 +66,23 @@ export namespace Callback {
|
||||
* @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>; }
|
||||
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)}`);
|
||||
super(
|
||||
`OnlyOffice command service error ${ErrorCodeFromValue(errorCode)} (${errorCode}): Requested ${JSON.stringify(req)} got ${JSON.stringify(
|
||||
res,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,44 +91,74 @@ namespace CommandService {
|
||||
|
||||
/** POST this OnlyOffice command, does not check the `error` field of the response */
|
||||
async postUnsafe(): Promise<ErrorResponse | TSuccessResponse> {
|
||||
loggerService.silly(`OnlyOffice command ${this.c} sent: ${JSON.stringify(this)}`);
|
||||
const result = (await axios.post(`${ONLY_OFFICE_SERVER}coauthoring/CommandService.ashx`, this));
|
||||
loggerService.info(`OnlyOffice command ${this.c} response: ${result.status}: ${JSON.stringify(result.data)}`);
|
||||
logger.silly(`OnlyOffice command ${this.c} sent: ${JSON.stringify(this)}`);
|
||||
const result = await axios.post(`${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 throw if the `errorCode` returned isn't 0 */
|
||||
async post(): Promise<TSuccessResponse> {
|
||||
const result = await this.postUnsafe();
|
||||
if (result.error === ErrorCode.SUCCESS)
|
||||
return result;
|
||||
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"); } }
|
||||
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 key: string, public userdata: string = "") { super("forcesave"); } }
|
||||
interface Response extends SuccessResponse {
|
||||
key: string;
|
||||
}
|
||||
export class Request extends BaseRequest<Response> {
|
||||
constructor(public key: string, public userdata: string = '') {
|
||||
super('forcesave');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace GetForgotten {
|
||||
interface Response extends SuccessResponse { key: string; url: string; }
|
||||
export class Request extends BaseRequest<Response> { constructor(public key: string) { super("getForgotten"); } }
|
||||
interface Response extends SuccessResponse {
|
||||
key: string;
|
||||
url: string;
|
||||
}
|
||||
export class Request extends BaseRequest<Response> {
|
||||
constructor(public key: string) {
|
||||
super('getForgotten');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace GetForgottenList {
|
||||
interface Response extends SuccessResponse { keys: string[]; }
|
||||
export class Request extends BaseRequest<Response> { constructor() { super("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 key: string) { super("deleteForgotten"); } }
|
||||
interface Response extends SuccessResponse {
|
||||
key: string;
|
||||
}
|
||||
export class Request extends BaseRequest<Response> {
|
||||
constructor(public key: string) {
|
||||
super('deleteForgotten');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +167,20 @@ namespace CommandService {
|
||||
* @see https://api.onlyoffice.com/editors/command/
|
||||
*/
|
||||
class OnlyOfficeService {
|
||||
private readonly poller: PolledThingieValue<string>;
|
||||
constructor() {
|
||||
this.poller = new PolledThingieValue('Connect to Only Office', () => this.getVersion(), 10 * 1000 * 60);
|
||||
}
|
||||
/** Get the latest Only Office version */
|
||||
public getLatestVersion() {
|
||||
return this.poller.latest();
|
||||
}
|
||||
/** Return the version string of OnlyOffice */
|
||||
async getVersion(): Promise<string> {
|
||||
return new CommandService.Version.Request().post().then(response => response.version);
|
||||
}
|
||||
/** Force a save in the editing session key provided. `userdata` will be forwarded to the callback */
|
||||
async forceSave(key: string, userdata: string = ""): Promise<string> {
|
||||
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 */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IuserService, UserType } from '@/interfaces/user.interface';
|
||||
import apiService from './api.service';
|
||||
import loggerService from './logger.service';
|
||||
import logger from '../lib/logger';
|
||||
|
||||
class UserService implements IuserService {
|
||||
public getCurrentUser = async (token: string): Promise<UserType> => {
|
||||
@@ -12,7 +12,7 @@ class UserService implements IuserService {
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
loggerService.error('Failed to fetch the current user', error.message);
|
||||
logger.error('Failed to fetch the current user from Twake Drive', error.stack);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user