Catching "no licence" microsoft account (#12143)
Catching "no licence - removed" microsoft message channels. Current behabiour > ` MessageImportException [Error]: The mailbox is either inactive, soft-deleted, or is hosted on-premise.` Goal: better track errors VS user mistakes Context: A similar logic was already implemented for the calendar channels. I just replicated it to message channels
This commit is contained in:
@ -4,6 +4,7 @@ import {
|
|||||||
CalendarEventImportDriverException,
|
CalendarEventImportDriverException,
|
||||||
CalendarEventImportDriverExceptionCode,
|
CalendarEventImportDriverExceptionCode,
|
||||||
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
|
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
|
||||||
|
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
|
||||||
|
|
||||||
export const parseGaxiosError = (
|
export const parseGaxiosError = (
|
||||||
error: GaxiosError,
|
error: GaxiosError,
|
||||||
@ -11,11 +12,11 @@ export const parseGaxiosError = (
|
|||||||
const { code } = error;
|
const { code } = error;
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'ECONNRESET':
|
case MessageNetworkExceptionCode.ECONNRESET:
|
||||||
case 'ENOTFOUND':
|
case MessageNetworkExceptionCode.ENOTFOUND:
|
||||||
case 'ECONNABORTED':
|
case MessageNetworkExceptionCode.ECONNABORTED:
|
||||||
case 'ETIMEDOUT':
|
case MessageNetworkExceptionCode.ETIMEDOUT:
|
||||||
case 'ERR_NETWORK':
|
case MessageNetworkExceptionCode.ERR_NETWORK:
|
||||||
return new CalendarEventImportDriverException(
|
return new CalendarEventImportDriverException(
|
||||||
error.message,
|
error.message,
|
||||||
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
|
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
export enum MessageNetworkExceptionCode {
|
||||||
|
ECONNRESET = 'ECONNRESET',
|
||||||
|
ENOTFOUND = 'ENOTFOUND',
|
||||||
|
ECONNABORTED = 'ECONNABORTED',
|
||||||
|
ETIMEDOUT = 'ETIMEDOUT',
|
||||||
|
ERR_NETWORK = 'ERR_NETWORK',
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
MessageImportDriverException,
|
MessageImportDriverException,
|
||||||
MessageImportDriverExceptionCode,
|
MessageImportDriverExceptionCode,
|
||||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||||
|
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
|
||||||
|
|
||||||
export const parseGaxiosError = (
|
export const parseGaxiosError = (
|
||||||
error: GaxiosError,
|
error: GaxiosError,
|
||||||
@ -11,11 +12,11 @@ export const parseGaxiosError = (
|
|||||||
const { code } = error;
|
const { code } = error;
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'ECONNRESET':
|
case MessageNetworkExceptionCode.ECONNRESET:
|
||||||
case 'ENOTFOUND':
|
case MessageNetworkExceptionCode.ENOTFOUND:
|
||||||
case 'ECONNABORTED':
|
case MessageNetworkExceptionCode.ECONNABORTED:
|
||||||
case 'ETIMEDOUT':
|
case MessageNetworkExceptionCode.ETIMEDOUT:
|
||||||
case 'ERR_NETWORK':
|
case MessageNetworkExceptionCode.ERR_NETWORK:
|
||||||
return new MessageImportDriverException(
|
return new MessageImportDriverException(
|
||||||
error.message,
|
error.message,
|
||||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
|
|
||||||
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
||||||
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
||||||
import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils';
|
import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftFetchByBatchService {
|
export class MicrosoftFetchByBatchService {
|
||||||
private readonly logger = new Logger(MicrosoftFetchByBatchService.name);
|
private readonly logger = new Logger(MicrosoftFetchByBatchService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly microsoftClientProvider: MicrosoftClientProvider,
|
private readonly microsoftClientProvider: MicrosoftClientProvider,
|
||||||
|
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async fetchAllByBatches(
|
async fetchAllByBatches(
|
||||||
@ -52,25 +52,9 @@ export class MicrosoftFetchByBatchService {
|
|||||||
|
|
||||||
batchResponses.push(batchResponse);
|
batchResponses.push(batchResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
this.microsoftHandleErrorService.handleMicrosoftMessageFetchByBatchError(
|
||||||
error.body &&
|
error,
|
||||||
typeof error.body === 'string' &&
|
);
|
||||||
isMicrosoftClientTemporaryError(error.body)
|
|
||||||
) {
|
|
||||||
// TODO: remove this log once we catch better the error codes
|
|
||||||
this.logger.error(
|
|
||||||
`Error temporary (${error.code}) fetching messages for account ${connectedAccount.id.slice(0, 8)}`,
|
|
||||||
);
|
|
||||||
this.logger.log(error);
|
|
||||||
throw new MicrosoftImportDriverException(error.body, error.code, 429);
|
|
||||||
} else {
|
|
||||||
// TODO: remove this log once we catch better the error codes
|
|
||||||
this.logger.error(
|
|
||||||
`Error unknown (${error.code}) fetching messages for account ${connectedAccount.id.slice(0, 8)}`,
|
|
||||||
);
|
|
||||||
this.logger.log(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,12 @@ export class MicrosoftGetMessageListService {
|
|||||||
.headers({
|
.headers({
|
||||||
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
|
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
|
||||||
})
|
})
|
||||||
.get();
|
.get()
|
||||||
|
.catch((error) => {
|
||||||
|
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const callback: PageIteratorCallback = (data) => {
|
const callback: PageIteratorCallback = (data) => {
|
||||||
messageExternalIds.push(data.id);
|
messageExternalIds.push(data.id);
|
||||||
@ -92,7 +97,9 @@ export class MicrosoftGetMessageListService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await pageIterator.iterate().catch((error) => {
|
await pageIterator.iterate().catch((error) => {
|
||||||
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
|
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
|
||||||
|
error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -195,7 +202,12 @@ export class MicrosoftGetMessageListService {
|
|||||||
.headers({
|
.headers({
|
||||||
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
|
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
|
||||||
})
|
})
|
||||||
.get();
|
.get()
|
||||||
|
.catch((error) => {
|
||||||
|
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const callback: PageIteratorCallback = (data) => {
|
const callback: PageIteratorCallback = (data) => {
|
||||||
if (data['@removed']) {
|
if (data['@removed']) {
|
||||||
@ -214,7 +226,9 @@ export class MicrosoftGetMessageListService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await pageIterator.iterate().catch((error) => {
|
await pageIterator.iterate().catch((error) => {
|
||||||
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
|
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
|
||||||
|
error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
|
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
|
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
|
||||||
|
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
|
||||||
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
|
||||||
import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
||||||
import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util';
|
import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util';
|
||||||
@ -43,7 +44,7 @@ export class MicrosoftGetMessagesService {
|
|||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
|
this.microsoftHandleErrorService.handleMicrosoftGetMessagesError(error);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -69,8 +70,10 @@ export class MicrosoftGetMessagesService {
|
|||||||
|
|
||||||
const messages = parsedResponses.map((response) => {
|
const messages = parsedResponses.map((response) => {
|
||||||
if ('error' in response) {
|
if ('error' in response) {
|
||||||
this.microsoftHandleErrorService.throwMicrosoftBatchError(
|
throw new MicrosoftImportDriverException(
|
||||||
response.error,
|
response.error.message,
|
||||||
|
response.error.code,
|
||||||
|
response.error.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,33 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MessageImportDriverException,
|
MessageImportDriverException,
|
||||||
MessageImportDriverExceptionCode,
|
MessageImportDriverExceptionCode,
|
||||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||||
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
|
import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils';
|
||||||
|
import { parseMicrosoftMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftHandleErrorService {
|
export class MicrosoftHandleErrorService {
|
||||||
|
private readonly logger = new Logger(MicrosoftHandleErrorService.name);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public handleMicrosoftMessageFetchError(error: any): void {
|
public handleMicrosoftMessageFetchByBatchError(error: any): void {
|
||||||
|
// TODO: remove this log once we catch better the error codes
|
||||||
|
this.logger.error(`Error temporary (${error.code}) fetching messages`);
|
||||||
|
this.logger.log(error);
|
||||||
|
|
||||||
|
const isBodyString = error.body && typeof error.body === 'string';
|
||||||
|
const isTemporaryError =
|
||||||
|
isBodyString && isMicrosoftClientTemporaryError(error.body);
|
||||||
|
|
||||||
|
if (isTemporaryError) {
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
`code: ${error.code} - body: ${error.body}`,
|
||||||
|
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!error.statusCode) {
|
if (!error.statusCode) {
|
||||||
throw new MessageImportDriverException(
|
throw new MessageImportDriverException(
|
||||||
`Microsoft Graph API unknown error: ${error}`,
|
`Microsoft Graph API unknown error: ${error}`,
|
||||||
@ -17,25 +35,10 @@ export class MicrosoftHandleErrorService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.statusCode === 401) {
|
const exception = parseMicrosoftMessagesImportError(error);
|
||||||
throw new MessageImportDriverException(
|
|
||||||
'Unauthorized access to Microsoft Graph API',
|
|
||||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.statusCode === 403) {
|
if (exception) {
|
||||||
throw new MessageImportDriverException(
|
throw exception;
|
||||||
'Forbidden access to Microsoft Graph API',
|
|
||||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.statusCode === 429) {
|
|
||||||
throw new MessageImportDriverException(
|
|
||||||
`Microsoft Graph API ${error.code} ${error.statusCode} error: ${error.message}`,
|
|
||||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new MessageImportDriverException(
|
throw new MessageImportDriverException(
|
||||||
@ -45,11 +48,44 @@ export class MicrosoftHandleErrorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public throwMicrosoftBatchError(error: any): void {
|
public handleMicrosoftGetMessageListError(error: any): void {
|
||||||
throw new MicrosoftImportDriverException(
|
if (!error.statusCode) {
|
||||||
error.message,
|
throw new MessageImportDriverException(
|
||||||
error.code,
|
`Microsoft Graph API unknown error: ${error}`,
|
||||||
error.statusCode,
|
MessageImportDriverExceptionCode.UNKNOWN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exception = parseMicrosoftMessagesImportError(error);
|
||||||
|
|
||||||
|
if (exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
`Microsoft driver error: ${error.message}`,
|
||||||
|
MessageImportDriverExceptionCode.UNKNOWN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public handleMicrosoftGetMessagesError(error: any): void {
|
||||||
|
if (!error.statusCode) {
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
`Microsoft Graph API unknown error: ${error}`,
|
||||||
|
MessageImportDriverExceptionCode.UNKNOWN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exception = parseMicrosoftMessagesImportError(error);
|
||||||
|
|
||||||
|
if (exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MessageImportDriverException(
|
||||||
|
`Microsoft driver error: ${error.message}`,
|
||||||
|
MessageImportDriverExceptionCode.UNKNOWN,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
MessageImportDriverException,
|
||||||
|
MessageImportDriverExceptionCode,
|
||||||
|
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||||
|
|
||||||
|
export const parseMicrosoftMessagesImportError = (error: {
|
||||||
|
statusCode: number;
|
||||||
|
message?: string;
|
||||||
|
code?: string;
|
||||||
|
}): MessageImportDriverException | undefined => {
|
||||||
|
if (error.statusCode === 401) {
|
||||||
|
return new MessageImportDriverException(
|
||||||
|
'Unauthorized access to Microsoft Graph API',
|
||||||
|
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 403) {
|
||||||
|
return new MessageImportDriverException(
|
||||||
|
'Forbidden access to Microsoft Graph API',
|
||||||
|
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 404) {
|
||||||
|
if (
|
||||||
|
error.message?.includes(
|
||||||
|
'The mailbox is either inactive, soft-deleted, or is hosted on-premise.',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return new MessageImportDriverException(
|
||||||
|
`Disabled, deleted, inactive or no licence Microsoft account - code:${error.code}`,
|
||||||
|
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new MessageImportDriverException(
|
||||||
|
`Not found - code:${error.code}`,
|
||||||
|
MessageImportDriverExceptionCode.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 429) {
|
||||||
|
return new MessageImportDriverException(
|
||||||
|
`Microsoft Graph API ${error.code} ${error.statusCode} error: ${error.message}`,
|
||||||
|
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
MessageImportDriverException,
|
MessageImportDriverException,
|
||||||
MessageImportDriverExceptionCode,
|
MessageImportDriverExceptionCode,
|
||||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||||
|
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
|
||||||
import {
|
import {
|
||||||
MessageImportException,
|
MessageImportException,
|
||||||
MessageImportExceptionCode,
|
MessageImportExceptionCode,
|
||||||
@ -51,6 +52,11 @@ export class MessageImportExceptionHandlerService {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
|
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
|
||||||
|
case MessageNetworkExceptionCode.ECONNABORTED:
|
||||||
|
case MessageNetworkExceptionCode.ENOTFOUND:
|
||||||
|
case MessageNetworkExceptionCode.ECONNRESET:
|
||||||
|
case MessageNetworkExceptionCode.ETIMEDOUT:
|
||||||
|
case MessageNetworkExceptionCode.ERR_NETWORK:
|
||||||
await this.handleTemporaryException(
|
await this.handleTemporaryException(
|
||||||
syncStep,
|
syncStep,
|
||||||
messageChannel,
|
messageChannel,
|
||||||
|
|||||||
Reference in New Issue
Block a user