Compare commits
10 commits
60d9cf9e9d
...
d1725d4af9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1725d4af9 | ||
|
|
365906a58d | ||
|
|
0bc2b4702c | ||
|
|
381e361814 | ||
|
|
6bb07609a3 | ||
|
|
7cdba2e77e | ||
|
|
3d120e7460 | ||
|
|
34e8c52ceb | ||
|
|
e9eb2f7c1e | ||
|
|
c3d6c7da5b |
11 changed files with 1099 additions and 129 deletions
23
AGENTS.md
Normal file
23
AGENTS.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# AGENTS
|
||||
|
||||
## Publicacao e atualizacao do pacote
|
||||
|
||||
- Sempre que a tarefa envolver publicar ou atualizar este community node, use `python scripts/publish_and_update.py`.
|
||||
- Nunca rode `npm publish` diretamente, exceto se o usuario pedir explicitamente para ignorar esse fluxo.
|
||||
- Para atualizar uma instalacao local do n8n baseada em diretorio, use `--target`.
|
||||
- Para atualizar uma instalacao em Docker Compose, use `--docker-service` e, quando necessario, `--docker-compose-file`.
|
||||
- Se houver bloqueio externo de politica, credencial, npm, Docker ou ambiente, explique isso claramente ao usuario e mantenha o script como fluxo padrao.
|
||||
|
||||
## Exemplos
|
||||
|
||||
Publicar e atualizar um n8n em diretorio local:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --target "C:\\caminho\\do\\n8n"
|
||||
```
|
||||
|
||||
Publicar e atualizar um n8n em Docker Compose:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --docker-service n8n --docker-compose-file "C:\\caminho\\docker-compose.yml"
|
||||
```
|
||||
74
README.md
74
README.md
|
|
@ -17,7 +17,7 @@ Node de comunidade para [n8n](https://n8n.io) para trabalhar com a API do Mega.
|
|||
- `Canned Response -> Get Many`, `Create`, `Update`, and `Delete` operations
|
||||
- `Custom Filter -> Get Many`, `Create`, `Get`, `Update`, and `Delete` operations
|
||||
- `Contact -> Get Many`, `Create`, `Create Note`, `Get`, `Update`, `Delete`, `Delete Note`, `Get Conversations`, `Search`, `Filter`, `Create Inbox`, `Get Contactable Inboxes`, and `Merge` operations
|
||||
- `Conversation -> Get Counts`, `Get Many`, `Create`, `Filter`, `Get`, `Update`, `Toggle Status`, `Toggle Priority`, `Toggle Typing Status`, `Set Custom Attributes`, `Get Labels`, `Set Labels`, `Get Reporting Events`, and `Assign` operations
|
||||
- `Conversation -> Get Counts`, `Get Many`, `Create`, `Create and Send Message`, `Filter`, `Get`, `Update`, `Toggle Status`, `Toggle Priority`, `Toggle Typing Status`, `Set Custom Attributes`, `Get Labels`, `Set Labels`, `Get Reporting Events`, and `Assign` operations
|
||||
- `Custom Attribute -> Get Many`, `Create`, `Get`, `Update`, and `Delete` operations
|
||||
- `Inbox -> Get Many`, `Get`, `Create`, `Update`, `Get Agent Bot`, `Set Agent Bot`, `Get Agents`, `Add Agent`, `Remove Agent`, and `Update Agents` operations
|
||||
- `Integration -> Get Many`, `Create`, `Update`, and `Delete` operations
|
||||
|
|
@ -57,6 +57,32 @@ Para instalar o pacote publicado no n8n:
|
|||
npm install @jessefreitas/n8n-nodes-mega
|
||||
```
|
||||
|
||||
## Publicacao e update
|
||||
|
||||
O fluxo padrao para publicar uma nova versao e atualizar uma instalacao do n8n deve usar:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py
|
||||
```
|
||||
|
||||
Exemplo para publicar e atualizar um n8n local por diretorio:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --target "C:\\caminho\\do\\n8n"
|
||||
```
|
||||
|
||||
Exemplo para publicar e atualizar um n8n em Docker Compose:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py --token "<NPM_TOKEN>" --docker-service n8n --docker-compose-file "C:\\caminho\\docker-compose.yml"
|
||||
```
|
||||
|
||||
Se voce ja publicou a versao e quer apenas atualizar o ambiente:
|
||||
|
||||
```bash
|
||||
python scripts/publish_and_update.py --skip-publish --version 0.4.12 --target "C:\\caminho\\do\\n8n"
|
||||
```
|
||||
|
||||
## Credenciais
|
||||
|
||||
Crie uma credencial `Mega API` no n8n com:
|
||||
|
|
@ -125,6 +151,22 @@ Importante:
|
|||
- `Mega Client` usa identificadores publicos como `inbox_identifier`, `contact_identifier`, and `conversation_id`
|
||||
- `CSAT Survey` usa uma rota publica `conversation_uuid` fora do padrao `/public/api/v1/inboxes/*`
|
||||
|
||||
## Campos JSON
|
||||
|
||||
Os campos do tipo `json` nos nodes `Mega`, `Mega Client` e `Mega Platform` esperam objetos JSON validos quando o payload do endpoint for estruturado. Exemplos de valor para o proprio campo:
|
||||
|
||||
```json
|
||||
{"name":"Maria"}
|
||||
```
|
||||
|
||||
ou:
|
||||
|
||||
```json
|
||||
{"crm_id":"123"}
|
||||
```
|
||||
|
||||
Quando o campo estiver vazio, o node envia `{}` ou omite o atributo opcional conforme o endpoint, evitando enviar strings como `"{}"` para a API.
|
||||
|
||||
|
||||
## Operacoes
|
||||
|
||||
|
|
@ -866,6 +908,8 @@ Campos suportados:
|
|||
- `After Message ID`
|
||||
- `Before Message ID`
|
||||
- `Content`
|
||||
- `Attachments Source`
|
||||
- `Attachment Binary Properties`
|
||||
- `Message Type`
|
||||
- `Private`
|
||||
- `Content Type`
|
||||
|
|
@ -873,6 +917,13 @@ Campos suportados:
|
|||
- `Campaign ID`
|
||||
- `Template Params`
|
||||
|
||||
`Message -> Create` supports optional multipart uploads using n8n binary properties.
|
||||
|
||||
- Use `Attachment Binary Properties` with a JSON array such as `["data", "audio", "pdf"]`
|
||||
- Multiple attachments are supported in the same message
|
||||
- Message text becomes optional when at least one attachment is provided
|
||||
- Files are sent as `attachments[]` in `multipart/form-data`
|
||||
|
||||
### Scheduled Message
|
||||
|
||||
Operacoes suportadas:
|
||||
|
|
@ -1000,6 +1051,7 @@ Operacoes suportadas:
|
|||
- `Get Counts`
|
||||
- `Get Many`
|
||||
- `Create`
|
||||
- `Create and Send Message`
|
||||
- `Filter`
|
||||
- `Get`
|
||||
- `Update`
|
||||
|
|
@ -1023,6 +1075,7 @@ POST /api/v1/accounts/{accountId}/conversations
|
|||
POST /api/v1/accounts/{accountId}/conversations/filter
|
||||
GET /api/v1/accounts/{accountId}/conversations/{id}
|
||||
PATCH /api/v1/accounts/{accountId}/conversations/{id}
|
||||
POST /api/v1/accounts/{accountId}/conversations/{id}/messages
|
||||
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_status
|
||||
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_priority
|
||||
POST /api/v1/accounts/{accountId}/conversations/{id}/toggle_typing_status
|
||||
|
|
@ -1050,6 +1103,10 @@ Campos suportados:
|
|||
- `Contact ID`
|
||||
- `Assignee ID`
|
||||
- `Initial Message`
|
||||
- `Message Content`
|
||||
- `Message Visibility`
|
||||
- `Attachments Source`
|
||||
- `Attachment Binary Properties`
|
||||
- `Additional Attributes`
|
||||
- `Custom Attributes`
|
||||
- `Priority`
|
||||
|
|
@ -1059,6 +1116,21 @@ Campos suportados:
|
|||
- `Typing Status`
|
||||
- `Private Note`
|
||||
|
||||
`Create` can send an optional normal first message in the conversation creation request.
|
||||
|
||||
`Create and Send Message` is the combined flow:
|
||||
|
||||
- `Normal` sends the message in `POST /conversations`
|
||||
- `Private` creates the conversation first and then sends a private note in `POST /conversations/{id}/messages`
|
||||
|
||||
When attachments are provided in `Create and Send Message`, the node creates the conversation first and then sends the message in a second request to `POST /conversations/{id}/messages`, for both `Normal` and `Private`.
|
||||
|
||||
- Use `Attachment Binary Properties` with a JSON array such as `["data", "audio", "pdf"]`
|
||||
- Multiple attachments are supported in the same message
|
||||
- Message text becomes optional when at least one attachment is provided
|
||||
- Files are sent as `attachments[]` in `multipart/form-data`
|
||||
- `Create a new message` also exposes optional advanced payload fields such as `Content Type`, `Content Attributes`, `Campaign ID`, and `Template Params`
|
||||
|
||||
## Validacao local
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
IBinaryData,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
|
|
@ -22,7 +23,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -39,7 +40,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -56,7 +57,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -75,7 +76,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -91,7 +92,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -107,7 +108,7 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -133,7 +134,70 @@ const conversationCreateProperties: INodeProperties[] = [
|
|||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['create'],
|
||||
operation: ['create', 'createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const conversationCreateAndSendProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Message Content',
|
||||
name: 'conversationCombinedMessageContent',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 4,
|
||||
},
|
||||
default: '',
|
||||
description: 'Message content to send after creating the conversation. Optional when attachments are provided.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Message Visibility',
|
||||
name: 'conversationCombinedMessageVisibility',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Normal', value: 'normal' },
|
||||
{ name: 'Private', value: 'private' },
|
||||
],
|
||||
default: 'normal',
|
||||
description: 'Whether to send a normal message or a private note',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments Source',
|
||||
name: 'conversationCombinedAttachmentsSource',
|
||||
type: 'options',
|
||||
options: [{ name: 'Binary Properties', value: 'binaryProperties' }],
|
||||
default: 'binaryProperties',
|
||||
description: 'Where to read attachments from',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachment Binary Properties',
|
||||
name: 'conversationCombinedAttachmentBinaryProperties',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
description: 'JSON array with binary property names, for example ["data", "audio", "pdf"]',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
operation: ['createAndSendMessage'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1709,6 +1773,7 @@ const contactIdProperty: INodeProperties = {
|
|||
show: {
|
||||
resource: ['contact'],
|
||||
operation: [
|
||||
'addLabels',
|
||||
'createNote',
|
||||
'createInbox',
|
||||
'delete',
|
||||
|
|
@ -1716,6 +1781,9 @@ const contactIdProperty: INodeProperties = {
|
|||
'get',
|
||||
'getContactableInboxes',
|
||||
'getConversations',
|
||||
'getLabels',
|
||||
'removeLabels',
|
||||
'setLabels',
|
||||
'update',
|
||||
],
|
||||
},
|
||||
|
|
@ -3108,8 +3176,34 @@ const messageCreateProperties: INodeProperties[] = [
|
|||
rows: 4,
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Content of the message',
|
||||
description: 'Content of the message. Optional when attachments are provided.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments Source',
|
||||
name: 'messageCreateAttachmentsSource',
|
||||
type: 'options',
|
||||
options: [{ name: 'Binary Properties', value: 'binaryProperties' }],
|
||||
default: 'binaryProperties',
|
||||
description: 'Where to read attachments from',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Attachment Binary Properties',
|
||||
name: 'messageCreateAttachmentBinaryProperties',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
description: 'JSON array with binary property names, for example ["data", "audio", "pdf"]',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
|
|
@ -4482,6 +4576,12 @@ export class Mega implements INodeType {
|
|||
description: 'Create a conversation',
|
||||
action: 'Create a conversation',
|
||||
},
|
||||
{
|
||||
name: 'Create and Send Message',
|
||||
value: 'createAndSendMessage',
|
||||
description: 'Create a conversation and send a message',
|
||||
action: 'Create a conversation and send a message',
|
||||
},
|
||||
{
|
||||
name: 'Filter',
|
||||
value: 'filter',
|
||||
|
|
@ -4913,6 +5013,7 @@ export class Mega implements INodeType {
|
|||
...conversationListProperties,
|
||||
conversationFilterProperty,
|
||||
...conversationCreateProperties,
|
||||
...conversationCreateAndSendProperties,
|
||||
...conversationUpdateProperties,
|
||||
...conversationToggleStatusProperties,
|
||||
conversationTogglePriorityProperty,
|
||||
|
|
@ -4929,6 +5030,270 @@ export class Mega implements INodeType {
|
|||
const returnData: INodeExecutionData[] = [];
|
||||
const credentials = await this.getCredentials('megaApi');
|
||||
const accountId = credentials.accountId as string;
|
||||
const runtimeGlobals = globalThis as typeof globalThis & {
|
||||
Blob?: new (parts?: Array<ArrayBuffer | ArrayBufferView | string>, options?: { type?: string }) => unknown;
|
||||
FormData?: new () => { append(name: string, value: unknown, fileName?: string): void };
|
||||
};
|
||||
const createRuntimeFormData = (): {
|
||||
append(name: string, value: unknown, fileName?: string): void;
|
||||
} => {
|
||||
if (typeof runtimeGlobals.FormData === 'undefined') {
|
||||
throw new NodeOperationError(this.getNode(), 'FormData is not available in this runtime');
|
||||
}
|
||||
|
||||
return new runtimeGlobals.FormData();
|
||||
};
|
||||
const createRuntimeBlob = (buffer: Uint8Array, mimeType: string): unknown => {
|
||||
if (typeof runtimeGlobals.Blob === 'undefined') {
|
||||
throw new NodeOperationError(this.getNode(), 'Blob is not available in this runtime');
|
||||
}
|
||||
|
||||
return new runtimeGlobals.Blob([buffer], { type: mimeType });
|
||||
};
|
||||
|
||||
const parseAttachmentPropertyNames = (
|
||||
itemIndex: number,
|
||||
parameterName: string,
|
||||
): string[] => {
|
||||
const rawValue = this.getNodeParameter(parameterName, itemIndex, '[]') as string | string[];
|
||||
let parsedValue: unknown;
|
||||
|
||||
try {
|
||||
parsedValue =
|
||||
typeof rawValue === 'string'
|
||||
? (JSON.parse(rawValue || '[]') as unknown)
|
||||
: (rawValue as unknown);
|
||||
} catch {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a valid JSON array of strings`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsedValue)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a JSON array of strings`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return parsedValue.map((value, index) => {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName}[${index}] must be a non-empty string`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
});
|
||||
};
|
||||
|
||||
const parseOptionalObject = (
|
||||
value: unknown,
|
||||
itemIndex: number,
|
||||
parameterName: string,
|
||||
): IDataObject => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let parsedValue = value;
|
||||
if (typeof parsedValue === 'string') {
|
||||
try {
|
||||
parsedValue = JSON.parse(parsedValue);
|
||||
} catch {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a valid JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return parsedValue as IDataObject;
|
||||
};
|
||||
|
||||
const appendFormValue = (
|
||||
formData: { append(name: string, value: unknown, fileName?: string): void },
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, String(value));
|
||||
};
|
||||
|
||||
const buildMultipartFormData = async (
|
||||
itemIndex: number,
|
||||
fields: Record<string, unknown>,
|
||||
attachmentPropertyNames: string[],
|
||||
): Promise<{ append(name: string, value: unknown, fileName?: string): void }> => {
|
||||
const formData = createRuntimeFormData();
|
||||
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
appendFormValue(formData, key, value);
|
||||
}
|
||||
|
||||
for (const propertyName of attachmentPropertyNames) {
|
||||
const binaryData = this.helpers.assertBinaryData(itemIndex, propertyName) as IBinaryData;
|
||||
const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, propertyName);
|
||||
const mimeType = binaryData.mimeType || 'application/octet-stream';
|
||||
const fileName = binaryData.fileName || propertyName;
|
||||
formData.append('attachments[]', createRuntimeBlob(buffer, mimeType), fileName);
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const createMessagePayload = (
|
||||
itemIndex: number,
|
||||
content: string,
|
||||
privateMessage: boolean,
|
||||
): IDataObject => {
|
||||
const body: IDataObject = {
|
||||
message_type: this.getNodeParameter('messageCreateType', itemIndex, 'outgoing') as string,
|
||||
private: privateMessage,
|
||||
content_type: this.getNodeParameter('messageCreateContentType', itemIndex, 'text') as string,
|
||||
};
|
||||
const trimmedContent = content.trim();
|
||||
const contentAttributes = this.getNodeParameter(
|
||||
'messageCreateContentAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
);
|
||||
const templateParams = this.getNodeParameter('messageCreateTemplateParams', itemIndex, {});
|
||||
const campaignId = this.getNodeParameter('messageCreateCampaignId', itemIndex, 0) as number;
|
||||
const parsedContentAttributes = parseOptionalObject(
|
||||
contentAttributes,
|
||||
itemIndex,
|
||||
'messageCreateContentAttributes',
|
||||
);
|
||||
const parsedTemplateParams = parseOptionalObject(
|
||||
templateParams,
|
||||
itemIndex,
|
||||
'messageCreateTemplateParams',
|
||||
);
|
||||
|
||||
if (trimmedContent) {
|
||||
body.content = trimmedContent;
|
||||
}
|
||||
if (Object.keys(parsedContentAttributes).length > 0) {
|
||||
body.content_attributes = parsedContentAttributes;
|
||||
}
|
||||
if (Object.keys(parsedTemplateParams).length > 0) {
|
||||
body.template_params = parsedTemplateParams;
|
||||
}
|
||||
if (campaignId > 0) {
|
||||
body.campaign_id = campaignId;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const sendConversationMessage = async (
|
||||
itemIndex: number,
|
||||
conversationId: number,
|
||||
content: string,
|
||||
privateMessage: boolean,
|
||||
attachmentPropertyNames: string[],
|
||||
messageOverrides?: Partial<IDataObject>,
|
||||
): Promise<IDataObject> => {
|
||||
const basePayload: IDataObject = {
|
||||
content_type: 'text',
|
||||
message_type: 'outgoing',
|
||||
private: privateMessage,
|
||||
...(messageOverrides ?? {}),
|
||||
};
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
if (trimmedContent) {
|
||||
basePayload.content = trimmedContent;
|
||||
}
|
||||
|
||||
if (attachmentPropertyNames.length === 0) {
|
||||
return (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
|
||||
basePayload,
|
||||
)) as IDataObject;
|
||||
}
|
||||
|
||||
const formData = await buildMultipartFormData(itemIndex, basePayload, attachmentPropertyNames);
|
||||
|
||||
return (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
|
||||
formData,
|
||||
)) as IDataObject;
|
||||
};
|
||||
|
||||
const buildConversationCreateBody = (itemIndex: number, messageContent = ''): IDataObject => {
|
||||
const body: IDataObject = {
|
||||
source_id: this.getNodeParameter('sourceId', itemIndex) as string,
|
||||
inbox_id: this.getNodeParameter('inboxId', itemIndex) as number,
|
||||
contact_id: this.getNodeParameter('contactId', itemIndex) as number,
|
||||
status: this.getNodeParameter('status', itemIndex) as string,
|
||||
};
|
||||
|
||||
const assigneeId = this.getNodeParameter('assigneeId', itemIndex) as number;
|
||||
const additionalAttributes = this.getNodeParameter(
|
||||
'additionalAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
);
|
||||
const customAttributes = this.getNodeParameter('customAttributes', itemIndex, {});
|
||||
const parsedAdditionalAttributes = parseOptionalObject(
|
||||
additionalAttributes,
|
||||
itemIndex,
|
||||
'additionalAttributes',
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'customAttributes',
|
||||
);
|
||||
|
||||
if (assigneeId > 0) {
|
||||
body.assignee_id = assigneeId;
|
||||
}
|
||||
|
||||
if (messageContent.trim()) {
|
||||
body.message = {
|
||||
content: messageContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(parsedAdditionalAttributes).length > 0) {
|
||||
body.additional_attributes = parsedAdditionalAttributes;
|
||||
}
|
||||
|
||||
if (Object.keys(parsedCustomAttributes).length > 0) {
|
||||
body.custom_attributes = parsedCustomAttributes;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
|
|
@ -5401,44 +5766,47 @@ export class Mega implements INodeType {
|
|||
)) as IDataObject;
|
||||
} else if (resource === 'message' && operation === 'create') {
|
||||
const conversationId = this.getNodeParameter('messageConversationId', itemIndex) as number;
|
||||
const body: IDataObject = {
|
||||
content: this.getNodeParameter('messageCreateContent', itemIndex) as string,
|
||||
message_type: this.getNodeParameter('messageCreateType', itemIndex, 'outgoing') as string,
|
||||
private: this.getNodeParameter('messageCreatePrivate', itemIndex, false) as boolean,
|
||||
content_type: this.getNodeParameter(
|
||||
'messageCreateContentType',
|
||||
const messageContent = this.getNodeParameter('messageCreateContent', itemIndex, '') as string;
|
||||
const privateMessage = this.getNodeParameter(
|
||||
'messageCreatePrivate',
|
||||
itemIndex,
|
||||
false,
|
||||
) as boolean;
|
||||
const attachmentPropertyNames = parseAttachmentPropertyNames(
|
||||
itemIndex,
|
||||
'messageCreateAttachmentBinaryProperties',
|
||||
);
|
||||
|
||||
if (!messageContent.trim() && attachmentPropertyNames.length === 0) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Content must be provided when no attachments are sent',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
const body = createMessagePayload(itemIndex, messageContent, privateMessage);
|
||||
|
||||
if (attachmentPropertyNames.length === 0) {
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
|
||||
body,
|
||||
)) as IDataObject;
|
||||
} else {
|
||||
const formData = await buildMultipartFormData(
|
||||
itemIndex,
|
||||
'text',
|
||||
) as string,
|
||||
};
|
||||
const contentAttributes = this.getNodeParameter(
|
||||
'messageCreateContentAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
const templateParams = this.getNodeParameter(
|
||||
'messageCreateTemplateParams',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
const campaignId = this.getNodeParameter('messageCreateCampaignId', itemIndex, 0) as number;
|
||||
|
||||
if (Object.keys(contentAttributes).length > 0) {
|
||||
body.content_attributes = contentAttributes;
|
||||
body as Record<string, unknown>,
|
||||
attachmentPropertyNames,
|
||||
);
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
|
||||
formData,
|
||||
)) as IDataObject;
|
||||
}
|
||||
if (Object.keys(templateParams).length > 0) {
|
||||
body.template_params = templateParams;
|
||||
}
|
||||
if (campaignId > 0) {
|
||||
body.campaign_id = campaignId;
|
||||
}
|
||||
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/messages`,
|
||||
body,
|
||||
)) as IDataObject;
|
||||
} else if (resource === 'message' && operation === 'delete') {
|
||||
const conversationId = this.getNodeParameter('messageConversationId', itemIndex) as number;
|
||||
const messageId = this.getNodeParameter('messageId', itemIndex) as number;
|
||||
|
|
@ -5537,7 +5905,12 @@ export class Mega implements INodeType {
|
|||
'scheduledMessageTemplateParams',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedTemplateParams = parseOptionalObject(
|
||||
templateParams,
|
||||
itemIndex,
|
||||
'scheduledMessageTemplateParams',
|
||||
);
|
||||
const recurrenceType = this.getNodeParameter(
|
||||
'scheduledMessageRecurrenceType',
|
||||
itemIndex,
|
||||
|
|
@ -5550,8 +5923,8 @@ export class Mega implements INodeType {
|
|||
if (title.trim()) {
|
||||
body.title = title;
|
||||
}
|
||||
if (Object.keys(templateParams).length > 0) {
|
||||
body.template_params = templateParams;
|
||||
if (Object.keys(parsedTemplateParams).length > 0) {
|
||||
body.template_params = parsedTemplateParams;
|
||||
}
|
||||
|
||||
if (recurrenceType !== 'none') {
|
||||
|
|
@ -5676,7 +6049,11 @@ export class Mega implements INodeType {
|
|||
body.scheduled_at = updateFields.scheduledAt;
|
||||
}
|
||||
if (updateFields.templateParams !== undefined) {
|
||||
body.template_params = updateFields.templateParams;
|
||||
body.template_params = parseOptionalObject(
|
||||
updateFields.templateParams,
|
||||
itemIndex,
|
||||
'scheduledMessageUpdateFields.values.templateParams',
|
||||
);
|
||||
}
|
||||
if (updateFields.title !== undefined && updateFields.title !== '') {
|
||||
body.title = updateFields.title;
|
||||
|
|
@ -5828,23 +6205,33 @@ export class Mega implements INodeType {
|
|||
'contactAdditionalAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const customAttributes = this.getNodeParameter(
|
||||
'contactCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedAdditionalAttributes = parseOptionalObject(
|
||||
additionalAttributes,
|
||||
itemIndex,
|
||||
'contactAdditionalAttributes',
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'contactCustomAttributes',
|
||||
);
|
||||
|
||||
if (name.trim()) body.name = name;
|
||||
if (email.trim()) body.email = email;
|
||||
if (phoneNumber.trim()) body.phone_number = phoneNumber;
|
||||
if (avatarUrl.trim()) body.avatar_url = avatarUrl;
|
||||
if (identifier.trim()) body.identifier = identifier;
|
||||
if (Object.keys(additionalAttributes).length > 0) {
|
||||
body.additional_attributes = additionalAttributes;
|
||||
if (Object.keys(parsedAdditionalAttributes).length > 0) {
|
||||
body.additional_attributes = parsedAdditionalAttributes;
|
||||
}
|
||||
if (Object.keys(customAttributes).length > 0) {
|
||||
body.custom_attributes = customAttributes;
|
||||
if (Object.keys(parsedCustomAttributes).length > 0) {
|
||||
body.custom_attributes = parsedCustomAttributes;
|
||||
}
|
||||
|
||||
response = (await megaApiRequest.call(
|
||||
|
|
@ -5870,7 +6257,11 @@ export class Mega implements INodeType {
|
|||
const body: IDataObject = {};
|
||||
|
||||
if (updateFields.additionalAttributes !== undefined) {
|
||||
body.additional_attributes = updateFields.additionalAttributes;
|
||||
body.additional_attributes = parseOptionalObject(
|
||||
updateFields.additionalAttributes,
|
||||
itemIndex,
|
||||
'contactUpdateFields.values.additionalAttributes',
|
||||
);
|
||||
}
|
||||
if (updateFields.avatarUrl !== undefined && updateFields.avatarUrl !== '') {
|
||||
body.avatar_url = updateFields.avatarUrl;
|
||||
|
|
@ -5879,7 +6270,11 @@ export class Mega implements INodeType {
|
|||
body.blocked = updateFields.blocked;
|
||||
}
|
||||
if (updateFields.customAttributes !== undefined) {
|
||||
body.custom_attributes = updateFields.customAttributes;
|
||||
body.custom_attributes = parseOptionalObject(
|
||||
updateFields.customAttributes,
|
||||
itemIndex,
|
||||
'contactUpdateFields.values.customAttributes',
|
||||
);
|
||||
}
|
||||
if (updateFields.email !== undefined && updateFields.email !== '') {
|
||||
body.email = updateFields.email;
|
||||
|
|
@ -6149,10 +6544,15 @@ export class Mega implements INodeType {
|
|||
'campaignTemplateParams',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedTemplateParams = parseOptionalObject(
|
||||
templateParams,
|
||||
itemIndex,
|
||||
'campaignTemplateParams',
|
||||
);
|
||||
|
||||
if (Object.keys(templateParams).length > 0) {
|
||||
body.template_params = templateParams;
|
||||
if (Object.keys(parsedTemplateParams).length > 0) {
|
||||
body.template_params = parsedTemplateParams;
|
||||
}
|
||||
|
||||
response = (await megaApiRequest.call(
|
||||
|
|
@ -6315,14 +6715,19 @@ export class Mega implements INodeType {
|
|||
'chatRoomMessageContentAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedContentAttributes = parseOptionalObject(
|
||||
contentAttributes,
|
||||
itemIndex,
|
||||
'chatRoomMessageContentAttributes',
|
||||
);
|
||||
|
||||
if (echoId.trim()) {
|
||||
chatRoomMessage.echo_id = echoId;
|
||||
}
|
||||
|
||||
if (Object.keys(contentAttributes).length > 0) {
|
||||
chatRoomMessage.content_attributes = contentAttributes;
|
||||
if (Object.keys(parsedContentAttributes).length > 0) {
|
||||
chatRoomMessage.content_attributes = parsedContentAttributes;
|
||||
}
|
||||
|
||||
response = (await megaApiRequest.call(
|
||||
|
|
@ -6806,43 +7211,8 @@ export class Mega implements INodeType {
|
|||
`/api/v1/accounts/${accountId}/conversations/${conversationId}`,
|
||||
)) as IDataObject;
|
||||
} else if (resource === 'conversation' && operation === 'create') {
|
||||
const body: IDataObject = {
|
||||
source_id: this.getNodeParameter('sourceId', itemIndex) as string,
|
||||
inbox_id: this.getNodeParameter('inboxId', itemIndex) as number,
|
||||
contact_id: this.getNodeParameter('contactId', itemIndex) as number,
|
||||
status: this.getNodeParameter('status', itemIndex) as string,
|
||||
};
|
||||
|
||||
const assigneeId = this.getNodeParameter('assigneeId', itemIndex) as number;
|
||||
const messageContent = this.getNodeParameter('messageContent', itemIndex) as string;
|
||||
const additionalAttributes = this.getNodeParameter(
|
||||
'additionalAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
const customAttributes = this.getNodeParameter(
|
||||
'customAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
|
||||
if (assigneeId > 0) {
|
||||
body.assignee_id = assigneeId;
|
||||
}
|
||||
|
||||
if (messageContent.trim()) {
|
||||
body.message = {
|
||||
content: messageContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(additionalAttributes).length > 0) {
|
||||
body.additional_attributes = additionalAttributes;
|
||||
}
|
||||
|
||||
if (Object.keys(customAttributes).length > 0) {
|
||||
body.custom_attributes = customAttributes;
|
||||
}
|
||||
const body = buildConversationCreateBody(itemIndex, messageContent);
|
||||
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
|
|
@ -6850,6 +7220,72 @@ export class Mega implements INodeType {
|
|||
`/api/v1/accounts/${accountId}/conversations`,
|
||||
body,
|
||||
)) as IDataObject;
|
||||
} else if (resource === 'conversation' && operation === 'createAndSendMessage') {
|
||||
const messageContent = this.getNodeParameter(
|
||||
'conversationCombinedMessageContent',
|
||||
itemIndex,
|
||||
'',
|
||||
) as string;
|
||||
const messageVisibility = this.getNodeParameter(
|
||||
'conversationCombinedMessageVisibility',
|
||||
itemIndex,
|
||||
'normal',
|
||||
) as string;
|
||||
const attachmentPropertyNames = parseAttachmentPropertyNames(
|
||||
itemIndex,
|
||||
'conversationCombinedAttachmentBinaryProperties',
|
||||
);
|
||||
|
||||
if (!messageContent.trim() && attachmentPropertyNames.length === 0) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Message Content must be provided when no attachments are sent',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
if (messageVisibility === 'normal' && attachmentPropertyNames.length === 0) {
|
||||
const body = buildConversationCreateBody(itemIndex, messageContent);
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations`,
|
||||
body,
|
||||
)) as IDataObject;
|
||||
} else {
|
||||
const conversationResponse = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations`,
|
||||
buildConversationCreateBody(itemIndex),
|
||||
)) as IDataObject;
|
||||
const conversationId = Number(conversationResponse.id ?? 0);
|
||||
|
||||
if (conversationId <= 0) {
|
||||
throw new Error(
|
||||
'Conversation was created but no conversation ID was returned for the private message step',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const messageResponse = await sendConversationMessage(
|
||||
itemIndex,
|
||||
conversationId,
|
||||
messageContent,
|
||||
messageVisibility === 'private',
|
||||
attachmentPropertyNames,
|
||||
);
|
||||
|
||||
response = {
|
||||
conversation: conversationResponse,
|
||||
message: messageResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Conversation ${conversationId} was created but sending the ${messageVisibility} message failed: ${(error as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (resource === 'conversation' && operation === 'update') {
|
||||
const conversationId = this.getNodeParameter('conversationId', itemIndex) as number;
|
||||
const body: IDataObject = {
|
||||
|
|
@ -6923,13 +7359,18 @@ export class Mega implements INodeType {
|
|||
'conversationCustomAttributesPayload',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'conversationCustomAttributesPayload',
|
||||
);
|
||||
response = (await megaApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/api/v1/accounts/${accountId}/conversations/${conversationId}/custom_attributes`,
|
||||
{
|
||||
custom_attributes: customAttributes,
|
||||
custom_attributes: parsedCustomAttributes,
|
||||
},
|
||||
)) as IDataObject;
|
||||
} else if (resource === 'conversation' && operation === 'getLabels') {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ export async function megaApiRequest(
|
|||
this: RequestContext,
|
||||
method: IHttpRequestMethods,
|
||||
route: string,
|
||||
body?: IDataObject,
|
||||
body?: IHttpRequestOptions['body'],
|
||||
qs?: IDataObject,
|
||||
requestOptions?: Partial<IHttpRequestOptions>,
|
||||
) {
|
||||
const credentials = await this.getCredentials('megaApi');
|
||||
const baseUrl = normalizeBaseUrl(credentials.baseUrl as string);
|
||||
|
|
@ -31,10 +32,19 @@ export async function megaApiRequest(
|
|||
const options: IHttpRequestOptions = {
|
||||
method,
|
||||
url: `${baseUrl}${route}`,
|
||||
body,
|
||||
qs,
|
||||
json: true,
|
||||
...(body !== undefined ? { body } : {}),
|
||||
...(qs !== undefined ? { qs } : {}),
|
||||
...requestOptions,
|
||||
};
|
||||
|
||||
if (options.json === undefined) {
|
||||
options.json = !(
|
||||
options.body &&
|
||||
typeof options.body === 'object' &&
|
||||
'append' in options.body &&
|
||||
options.body.constructor?.name === 'FormData'
|
||||
);
|
||||
}
|
||||
|
||||
return this.helpers.httpRequestWithAuthentication.call(this, 'megaApi', options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,6 +424,38 @@ export class MegaClient implements INodeType {
|
|||
const returnData: INodeExecutionData[] = [];
|
||||
const credentials = await this.getCredentials('megaClientApi');
|
||||
const inboxIdentifier = credentials.inboxIdentifier as string;
|
||||
const parseOptionalObject = (
|
||||
value: unknown,
|
||||
itemIndex: number,
|
||||
parameterName: string,
|
||||
): IDataObject => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let parsedValue = value;
|
||||
if (typeof parsedValue === 'string') {
|
||||
try {
|
||||
parsedValue = JSON.parse(parsedValue);
|
||||
} catch {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a valid JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return parsedValue as IDataObject;
|
||||
};
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
|
|
@ -442,14 +474,19 @@ export class MegaClient implements INodeType {
|
|||
'clientContactCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'clientContactCustomAttributes',
|
||||
);
|
||||
|
||||
if (identifier.trim()) body.identifier = identifier;
|
||||
if (identifierHash.trim()) body.identifier_hash = identifierHash;
|
||||
if (email.trim()) body.email = email;
|
||||
if (name.trim()) body.name = name;
|
||||
if (phoneNumber.trim()) body.phone_number = phoneNumber;
|
||||
if (Object.keys(customAttributes).length > 0) body.custom_attributes = customAttributes;
|
||||
if (Object.keys(parsedCustomAttributes).length > 0) body.custom_attributes = parsedCustomAttributes;
|
||||
|
||||
response = (await megaClientApiRequest.call(
|
||||
this,
|
||||
|
|
@ -476,14 +513,19 @@ export class MegaClient implements INodeType {
|
|||
'clientContactCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'clientContactCustomAttributes',
|
||||
);
|
||||
|
||||
if (identifier.trim()) body.identifier = identifier;
|
||||
if (identifierHash.trim()) body.identifier_hash = identifierHash;
|
||||
if (email.trim()) body.email = email;
|
||||
if (name.trim()) body.name = name;
|
||||
if (phoneNumber.trim()) body.phone_number = phoneNumber;
|
||||
if (Object.keys(customAttributes).length > 0) body.custom_attributes = customAttributes;
|
||||
if (Object.keys(parsedCustomAttributes).length > 0) body.custom_attributes = parsedCustomAttributes;
|
||||
|
||||
response = (await megaClientApiRequest.call(
|
||||
this,
|
||||
|
|
@ -504,12 +546,17 @@ export class MegaClient implements INodeType {
|
|||
'clientConversationCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject;
|
||||
);
|
||||
const parsedCustomAttributes = parseOptionalObject(
|
||||
customAttributes,
|
||||
itemIndex,
|
||||
'clientConversationCustomAttributes',
|
||||
);
|
||||
response = (await megaClientApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/public/api/v1/inboxes/${inboxIdentifier}/contacts/${contactIdentifier}/conversations`,
|
||||
{ custom_attributes: customAttributes },
|
||||
{ custom_attributes: parsedCustomAttributes },
|
||||
)) as IDataObject;
|
||||
} else if (resource === 'conversation' && operation === 'get') {
|
||||
const contactIdentifier = this.getNodeParameter('clientContactIdentifier', itemIndex) as string;
|
||||
|
|
|
|||
|
|
@ -791,6 +791,38 @@ export class MegaPlatform implements INodeType {
|
|||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const parseOptionalObject = (
|
||||
value: unknown,
|
||||
itemIndex: number,
|
||||
parameterName: string,
|
||||
): IDataObject => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let parsedValue = value;
|
||||
if (typeof parsedValue === 'string') {
|
||||
try {
|
||||
parsedValue = JSON.parse(parsedValue);
|
||||
} catch {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a valid JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`${parameterName} must be a JSON object`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return parsedValue as IDataObject;
|
||||
};
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
|
|
@ -799,6 +831,16 @@ export class MegaPlatform implements INodeType {
|
|||
let response: IDataObject;
|
||||
|
||||
if (resource === 'account' && operation === 'create') {
|
||||
const limits = parseOptionalObject(
|
||||
this.getNodeParameter('platformAccountLimits', itemIndex, {}),
|
||||
itemIndex,
|
||||
'platformAccountLimits',
|
||||
);
|
||||
const customAttributes = parseOptionalObject(
|
||||
this.getNodeParameter('platformAccountCustomAttributes', itemIndex, {}),
|
||||
itemIndex,
|
||||
'platformAccountCustomAttributes',
|
||||
);
|
||||
response = (await megaPlatformApiRequest.call(this, 'POST', '/platform/api/v1/accounts', {
|
||||
name: this.getNodeParameter('platformAccountName', itemIndex) as string,
|
||||
locale: this.getNodeParameter('platformAccountLocale', itemIndex, 'en') as string,
|
||||
|
|
@ -809,12 +851,8 @@ export class MegaPlatform implements INodeType {
|
|||
'',
|
||||
) as string,
|
||||
status: this.getNodeParameter('platformAccountStatus', itemIndex, 'active') as string,
|
||||
limits: this.getNodeParameter('platformAccountLimits', itemIndex, {}) as IDataObject,
|
||||
custom_attributes: this.getNodeParameter(
|
||||
'platformAccountCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject,
|
||||
limits,
|
||||
custom_attributes: customAttributes,
|
||||
})) as IDataObject;
|
||||
} else if (resource === 'account' && operation === 'get') {
|
||||
const accountId = this.getNodeParameter('platformAccountId', itemIndex) as number;
|
||||
|
|
@ -832,9 +870,21 @@ export class MegaPlatform implements INodeType {
|
|||
) as IDataObject;
|
||||
const body: IDataObject = {};
|
||||
|
||||
if (updateFields.customAttributes !== undefined) body.custom_attributes = updateFields.customAttributes;
|
||||
if (updateFields.customAttributes !== undefined) {
|
||||
body.custom_attributes = parseOptionalObject(
|
||||
updateFields.customAttributes,
|
||||
itemIndex,
|
||||
'platformAccountUpdateFields.values.customAttributes',
|
||||
);
|
||||
}
|
||||
if (updateFields.domain !== undefined) body.domain = updateFields.domain;
|
||||
if (updateFields.limits !== undefined) body.limits = updateFields.limits;
|
||||
if (updateFields.limits !== undefined) {
|
||||
body.limits = parseOptionalObject(
|
||||
updateFields.limits,
|
||||
itemIndex,
|
||||
'platformAccountUpdateFields.values.limits',
|
||||
);
|
||||
}
|
||||
if (updateFields.locale !== undefined) body.locale = updateFields.locale;
|
||||
if (updateFields.name !== undefined) body.name = updateFields.name;
|
||||
if (updateFields.status !== undefined) body.status = updateFields.status;
|
||||
|
|
@ -946,16 +996,17 @@ export class MegaPlatform implements INodeType {
|
|||
);
|
||||
response = { success: true, id: agentBotId };
|
||||
} else if (resource === 'user' && operation === 'create') {
|
||||
const customAttributes = parseOptionalObject(
|
||||
this.getNodeParameter('platformUserCustomAttributes', itemIndex, {}),
|
||||
itemIndex,
|
||||
'platformUserCustomAttributes',
|
||||
);
|
||||
response = (await megaPlatformApiRequest.call(this, 'POST', '/platform/api/v1/users', {
|
||||
name: this.getNodeParameter('platformUserName', itemIndex) as string,
|
||||
display_name: this.getNodeParameter('platformUserDisplayName', itemIndex, '') as string,
|
||||
email: this.getNodeParameter('platformUserEmail', itemIndex) as string,
|
||||
password: this.getNodeParameter('platformUserPassword', itemIndex) as string,
|
||||
custom_attributes: this.getNodeParameter(
|
||||
'platformUserCustomAttributes',
|
||||
itemIndex,
|
||||
{},
|
||||
) as IDataObject,
|
||||
custom_attributes: customAttributes,
|
||||
})) as IDataObject;
|
||||
} else if (resource === 'user' && operation === 'get') {
|
||||
const userId = this.getNodeParameter('platformUserId', itemIndex) as number;
|
||||
|
|
@ -969,7 +1020,13 @@ export class MegaPlatform implements INodeType {
|
|||
const updateFields = this.getNodeParameter('platformUserUpdateFields.values', itemIndex, {}) as IDataObject;
|
||||
const body: IDataObject = {};
|
||||
|
||||
if (updateFields.customAttributes !== undefined) body.custom_attributes = updateFields.customAttributes;
|
||||
if (updateFields.customAttributes !== undefined) {
|
||||
body.custom_attributes = parseOptionalObject(
|
||||
updateFields.customAttributes,
|
||||
itemIndex,
|
||||
'platformUserUpdateFields.values.customAttributes',
|
||||
);
|
||||
}
|
||||
if (updateFields.displayName !== undefined) body.display_name = updateFields.displayName;
|
||||
if (updateFields.email !== undefined) body.email = updateFields.email;
|
||||
if (updateFields.name !== undefined) body.name = updateFields.name;
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@jessefreitas/n8n-nodes-mega",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@jessefreitas/n8n-nodes-mega",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.12",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@n8n/node-cli": "0.23.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@jessefreitas/n8n-nodes-mega",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.12",
|
||||
"description": "Trabalhe com a API do Mega",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/jessefreitas/n8n_community_mega",
|
||||
|
|
|
|||
51
scripts/install_package.py
Normal file
51
scripts/install_package.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
|
||||
DEFAULT_VERSION = "0.4.7"
|
||||
|
||||
|
||||
def resolve_npm() -> str:
|
||||
if sys.platform == "win32":
|
||||
return shutil.which("npm.cmd") or shutil.which("npm") or "npm"
|
||||
return shutil.which("npm") or "npm"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Install the published Mega n8n community package using npm.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
default=DEFAULT_VERSION,
|
||||
help=f"Package version to install. Defaults to {DEFAULT_VERSION}.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
default=".",
|
||||
help="Directory where npm install should run. Defaults to the current directory.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
target = Path(args.target).resolve()
|
||||
if not target.exists():
|
||||
print(f"Target directory does not exist: {target}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
npm = resolve_npm()
|
||||
package_spec = f"{PACKAGE_NAME}@{args.version}"
|
||||
|
||||
result = subprocess.run(
|
||||
[npm, "install", package_spec],
|
||||
cwd=target,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
179
scripts/publish_and_update.py
Normal file
179
scripts/publish_and_update.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
|
||||
|
||||
|
||||
def resolve_npm() -> str:
|
||||
if sys.platform == "win32":
|
||||
return shutil.which("npm.cmd") or shutil.which("npm") or "npm"
|
||||
return shutil.which("npm") or "npm"
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Path, env: dict[str, str] | None = None) -> None:
|
||||
result = subprocess.run(command, cwd=cwd, env=env, check=False)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.returncode)
|
||||
|
||||
|
||||
def build_temp_npmrc(token: str) -> tuple[Path, Path]:
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="npm-publish-"))
|
||||
npmrc = temp_dir / ".npmrc"
|
||||
npmrc.write_text(
|
||||
f"//registry.npmjs.org/:_authToken={token}\nregistry=https://registry.npmjs.org/\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
return temp_dir, npmrc
|
||||
|
||||
|
||||
def read_package_version(repo_root: Path) -> str:
|
||||
package_json = repo_root / "package.json"
|
||||
content = package_json.read_text(encoding="utf-8")
|
||||
marker = '"version": "'
|
||||
start = content.find(marker)
|
||||
if start == -1:
|
||||
raise RuntimeError(f"Could not find version in {package_json}")
|
||||
start += len(marker)
|
||||
end = content.find('"', start)
|
||||
if end == -1:
|
||||
raise RuntimeError(f"Could not parse version in {package_json}")
|
||||
return content[start:end]
|
||||
|
||||
|
||||
def publish_package(repo_root: Path, token: str, skip_whoami: bool) -> str:
|
||||
npm = resolve_npm()
|
||||
version = read_package_version(repo_root)
|
||||
temp_dir, npmrc = build_temp_npmrc(token)
|
||||
env = os.environ.copy()
|
||||
env["NPM_CONFIG_USERCONFIG"] = str(npmrc)
|
||||
|
||||
try:
|
||||
if not skip_whoami:
|
||||
run([npm, "whoami"], cwd=repo_root, env=env)
|
||||
|
||||
run(
|
||||
[npm, "publish", "--access", "public", "--ignore-scripts"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
)
|
||||
|
||||
run([npm, "view", PACKAGE_NAME, "version"], cwd=repo_root, env=env)
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def update_directory(target: Path, version: str) -> None:
|
||||
if not target.exists():
|
||||
raise RuntimeError(f"Target directory does not exist: {target}")
|
||||
|
||||
npm = resolve_npm()
|
||||
run([npm, "install", f"{PACKAGE_NAME}@{version}"], cwd=target)
|
||||
|
||||
|
||||
def update_docker(service: str, version: str, compose_file: Path | None, project_dir: Path) -> None:
|
||||
docker = shutil.which("docker")
|
||||
if docker is None:
|
||||
raise RuntimeError("Docker is not available in PATH")
|
||||
|
||||
command = [docker, "compose"]
|
||||
if compose_file is not None:
|
||||
command.extend(["-f", str(compose_file)])
|
||||
command.extend(["exec", "-T", service, "npm", "install", f"{PACKAGE_NAME}@{version}"])
|
||||
run(command, cwd=project_dir)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Publish the Mega n8n package and update a target n8n installation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
default=os.environ.get("NPM_TOKEN", "").strip(),
|
||||
help="Granular npm token with publish permission. Defaults to NPM_TOKEN.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-publish",
|
||||
action="store_true",
|
||||
help="Skip npm publish and only run the update step.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
default="",
|
||||
help="Version to install on the target. Defaults to package.json version.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-whoami",
|
||||
action="store_true",
|
||||
help="Skip npm whoami before publish.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
default="",
|
||||
help="Directory where npm install should run for the n8n update step.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--docker-service",
|
||||
default="",
|
||||
help="Docker Compose service name to update with npm install inside the container.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--docker-compose-file",
|
||||
default="",
|
||||
help="Optional docker-compose file to use with --docker-service.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--docker-project-dir",
|
||||
default="",
|
||||
help="Working directory for docker compose. Defaults to the compose file directory or current directory.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
version = args.version.strip() or read_package_version(repo_root)
|
||||
|
||||
if not args.skip_publish:
|
||||
token = args.token
|
||||
if not token:
|
||||
token = getpass.getpass("NPM token: ").strip()
|
||||
|
||||
if not token:
|
||||
print("Missing npm token. Pass --token, set NPM_TOKEN, or enter it when prompted.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
version = publish_package(repo_root, token, args.skip_whoami)
|
||||
|
||||
if args.target.strip():
|
||||
update_directory(Path(args.target).resolve(), version)
|
||||
|
||||
if args.docker_service.strip():
|
||||
compose_file = Path(args.docker_compose_file).resolve() if args.docker_compose_file.strip() else None
|
||||
if args.docker_project_dir.strip():
|
||||
project_dir = Path(args.docker_project_dir).resolve()
|
||||
elif compose_file is not None:
|
||||
project_dir = compose_file.parent
|
||||
else:
|
||||
project_dir = Path.cwd()
|
||||
update_docker(args.docker_service.strip(), version, compose_file, project_dir)
|
||||
|
||||
if not args.target.strip() and not args.docker_service.strip():
|
||||
print("Publish completed. No update target was provided.")
|
||||
else:
|
||||
print(f"Completed publish/update flow for {PACKAGE_NAME}@{version}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
90
scripts/publish_npm.py
Normal file
90
scripts/publish_npm.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKAGE_NAME = "@jessefreitas/n8n-nodes-mega"
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Path, userconfig: Path | None = None) -> None:
|
||||
env = os.environ.copy()
|
||||
if userconfig is not None:
|
||||
env["NPM_CONFIG_USERCONFIG"] = str(userconfig)
|
||||
|
||||
executable = command[0]
|
||||
if os.name == "nt" and executable == "npm":
|
||||
executable = shutil.which("npm.cmd") or shutil.which("npm") or executable
|
||||
command = [executable, *command[1:]]
|
||||
|
||||
result = subprocess.run(command, cwd=cwd, env=env, check=False)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.returncode)
|
||||
|
||||
|
||||
def build_temp_npmrc(token: str) -> Path:
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="npm-publish-"))
|
||||
npmrc = temp_dir / ".npmrc"
|
||||
npmrc.write_text(
|
||||
f"//registry.npmjs.org/:_authToken={token}\nregistry=https://registry.npmjs.org/\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
return npmrc
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Publish @jessefreitas/n8n-nodes-mega using a temporary npm config.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
default=os.environ.get("NPM_TOKEN", ""),
|
||||
help="Granular npm token with publish permission. Defaults to NPM_TOKEN.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-whoami",
|
||||
action="store_true",
|
||||
help="Skip the npm whoami check before publishing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-view",
|
||||
action="store_true",
|
||||
help="Skip the final npm view version check after publishing.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
token = args.token.strip()
|
||||
if not token:
|
||||
token = getpass.getpass("NPM token: ").strip()
|
||||
|
||||
if not token:
|
||||
print("Missing npm token. Pass --token, set NPM_TOKEN, or enter it when prompted.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
npmrc = build_temp_npmrc(token)
|
||||
|
||||
try:
|
||||
if not args.skip_whoami:
|
||||
run(["npm", "whoami"], cwd=repo_root, userconfig=npmrc)
|
||||
|
||||
run(
|
||||
["npm", "publish", "--access", "public", "--ignore-scripts"],
|
||||
cwd=repo_root,
|
||||
userconfig=npmrc,
|
||||
)
|
||||
|
||||
if not args.skip_view:
|
||||
run(["npm", "view", PACKAGE_NAME, "version"], cwd=repo_root, userconfig=npmrc)
|
||||
finally:
|
||||
shutil.rmtree(npmrc.parent, ignore_errors=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in a new issue