@tuanltntu/n8n-nodes-bitrix24
Version:
Comprehensive n8n community node for Bitrix24 API integration with CRM, Tasks, Chat, Telephony, and more
812 lines • 32.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.bitrix24ApiKeyRequest = exports.getEntityFields = exports.returnFullBitrix24Response = exports.testAuth = exports.validateBinaryDataExists = exports.bitrix24DownloadFile = exports.bitrix24ApiRequestAllItems = exports.makeStandardBitrix24Call = exports.bitrix24Request = void 0;
/**
* Simplified Bitrix24 API request function that handles all authentication types
*/
async function bitrix24Request(endpoint, body = {}, qs = {}, options = {}) {
const authType = (options.authType || "oauth2").toLowerCase();
try {
// Use the appropriate auth method based on authType
if (authType === "webhook") {
return await bitrix24WebhookCall.call(this, endpoint, body, qs, options);
}
else if (authType === "apikey") {
return await bitrix24ApiKeyCall.call(this, endpoint, body, qs, options);
}
else {
// Default to OAuth2
// Get credentials
const credentials = await this.getCredentials("bitrix24OAuth");
const portalUrl = credentials.portalUrl.replace(/\/$/, "");
// Remove leading slash if it exists
const cleanEndpoint = endpoint.startsWith("/")
? endpoint.substring(1)
: endpoint;
// Form request URL
const url = `${portalUrl}/rest/${cleanEndpoint}`;
// Prepare request options
const requestOptions = {
method: "POST",
body,
qs,
url,
json: true,
};
try {
// Make the request using OAuth2 helper
const response = await this.helpers.requestOAuth2.call(this, "bitrix24OAuth", requestOptions);
// Trả về response trực tiếp không qua xử lý
return response;
}
catch (error) {
// Check if we should fall back to webhook
if (error.statusCode === 401 && options.fallbackToWebhook === true) {
return await bitrix24WebhookCall.call(this, endpoint, body, qs, options);
}
// Return error response instead of throwing
return handleBitrix24Error(error, authType);
}
}
}
catch (error) {
// Return error response instead of throwing
return handleBitrix24Error(error, authType);
}
}
exports.bitrix24Request = bitrix24Request;
/**
* Make an API request to Bitrix24 using webhook
*/
async function bitrix24WebhookCall(endpoint, body = {}, qs = {}, options = {}) {
// Get webhook credentials
const credentials = await this.getCredentials("bitrix24Webhook");
const webhookUrl = credentials.webhookUrl;
// Ensure we have a valid webhook URL
if (!webhookUrl) {
throw new Error("No webhook URL provided in credentials");
}
let url;
// Check if an access_token was provided in options
if (options.accessToken && typeof options.accessToken === "string") {
// Extract portal base URL from webhook URL
// Webhook URL format: https://domain.bitrix24.com/rest/USER_ID/WEBHOOK_CODE/
// We need: https://domain.bitrix24.com/rest/endpoint?auth=accessToken
const webhookUrlParts = webhookUrl.match(/^(https?:\/\/[^\/]+)/);
if (!webhookUrlParts) {
throw new Error("Invalid webhook URL format");
}
const portalBaseUrl = webhookUrlParts[1];
// Remove leading slash from endpoint if present
const cleanEndpoint = endpoint.startsWith("/")
? endpoint.substring(1)
: endpoint;
// Build URL with portal base + /rest/ + endpoint
url = `${portalBaseUrl}/rest/${cleanEndpoint}`;
// Add auth parameter to query string
if (!qs.auth) {
qs.auth = options.accessToken;
}
}
else {
// Use webhook URL as-is (built-in authentication)
// Remove trailing slash if present
const baseUrl = webhookUrl.replace(/\/$/, "");
// Remove leading slash from endpoint if present
const cleanEndpoint = endpoint.startsWith("/")
? endpoint.substring(1)
: endpoint;
// Form the complete URL for webhook call
url = `${baseUrl}/${cleanEndpoint}`;
}
// Make HTTP request
const requestOptions = {
method: "POST",
url,
body,
qs,
json: true,
};
try {
const response = await this.helpers.request(requestOptions);
// Trả về response trực tiếp không qua xử lý
return response;
}
catch (error) {
// Xử lý đặc biệt cho lỗi có cấu trúc "400 - {...}"
if (error.response &&
error.response.body &&
typeof error.response.body === "string") {
const errorBody = error.response.body;
const statusMatch = errorBody.match(/^(\d+)\s*-\s*(\{.*\})$/);
if (statusMatch) {
try {
const status = parseInt(statusMatch[1], 10);
const jsonPart = statusMatch[2];
const parsedError = JSON.parse(jsonPart);
if (parsedError.error && parsedError.error_description) {
return {
error: parsedError.error,
error_description: parsedError.error_description,
status: status,
original_error: parsedError,
auth_type: "webhook",
};
}
}
catch (parseError) {
// Nếu không parse được JSON, sử dụng handleBitrix24Error
}
}
}
// Trả về lỗi đã định dạng
return handleBitrix24Error(error, "webhook");
}
}
/**
* Make an API request to Bitrix24 using OAuth2 or API Key
*/
async function bitrix24ApiKeyCall(endpoint, body = {}, qs = {}, options = {}) {
// Get API key credentials
const credentials = await this.getCredentials("bitrix24Api");
const portalUrl = credentials.portalUrl.replace(/\/$/, "");
// Use access token from options or from credentials
let accessToken = options.accessToken;
if (!accessToken) {
accessToken = credentials.accessToken;
}
// Remove leading slash if it exists
const cleanEndpoint = endpoint.startsWith("/")
? endpoint.substring(1)
: endpoint;
// Form request URL
const url = `${portalUrl}/rest/${cleanEndpoint}`;
// Add auth parameter to query string
if (!qs.auth) {
qs.auth = accessToken;
}
// Prepare request options
const requestOptions = {
method: "POST",
body,
qs,
url,
json: true,
};
try {
const response = await this.helpers.request(requestOptions);
// Check if response contains error fields from Bitrix24
if (response === null || response === void 0 ? void 0 : response.error) {
// Format the error properly
return {
error: response.error,
error_description: response.error_description || "Unknown Bitrix24 error",
status: response.status || 400,
original_error: response,
auth_type: "apikey",
};
}
return handleBitrix24Response(response);
}
catch (error) {
// Check if we need to refresh the token
if (error.statusCode === 401 &&
error.error &&
error.error.error === "expired_token") {
// Token expired, need to refresh
try {
const refreshedToken = await refreshAccessToken.call(this, credentials);
if (refreshedToken && refreshedToken.accessToken) {
// Update query string with new token
qs.auth = refreshedToken.accessToken;
requestOptions.qs = qs;
// Retry request with new token
try {
const retryResponse = await this.helpers.request(requestOptions);
// Check if retry response contains error fields from Bitrix24
if (retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.error) {
// Format the error properly
return {
error: retryResponse.error,
error_description: retryResponse.error_description || "Unknown Bitrix24 error",
status: retryResponse.status || 400,
original_error: retryResponse,
auth_type: "apikey",
};
}
return handleBitrix24Response(retryResponse);
}
catch (retryError) {
// Return error response instead of throwing
return handleBitrix24Error(retryError, "apikey");
}
}
else {
throw new Error(`Failed to refresh token: No token returned`);
}
}
catch (refreshError) {
// Return error response instead of throwing
return handleBitrix24Error({
message: `Token refresh failed: ${refreshError.message}`,
statusCode: 401,
}, "apikey");
}
}
// Return error response instead of throwing
return handleBitrix24Error(error, "apikey");
}
}
/**
* Handle standard Bitrix24 response
*/
function handleBitrix24Response(response) {
if (typeof response !== "object") {
return { result: response };
}
// Check if response contains Bitrix24 error fields
if (response === null || response === void 0 ? void 0 : response.error) {
// Return in standardized error format
return {
error: response.error,
error_description: response.error_description || "Unknown Bitrix24 error",
status: response.status || 400,
original_error: response,
};
}
return response;
}
/**
* Handle error from Bitrix24 response
*/
function handleBitrix24Error(error, authType = "unknown") {
var _a, _b;
let errorMessage = "Unknown Bitrix24 API error";
let errorCode = "UNKNOWN_ERROR";
let statusCode = error.statusCode || 500;
let errorBody = {};
// First check if error already has bitrix24 error format
if (error.error && error.error_description) {
errorCode = error.error;
errorMessage = error.error_description;
errorBody = error;
}
// Check if response body exists and try to parse it
else if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.body) {
try {
// Handle special case for string response like "400 - {...}"
if (typeof error.response.body === "string") {
const statusMatch = error.response.body.match(/^(\d+)\s*-\s*(.*)/);
if (statusMatch) {
// Extract status code and remaining text
statusCode = parseInt(statusMatch[1], 10);
const remainingText = statusMatch[2].trim();
// Try to parse remaining text as JSON if it looks like JSON
if (remainingText.startsWith("{") && remainingText.endsWith("}")) {
try {
const parsedJson = JSON.parse(remainingText);
if (parsedJson.error) {
errorCode = parsedJson.error;
}
if (parsedJson.error_description) {
errorMessage = parsedJson.error_description;
}
errorBody = parsedJson;
return {
error: errorCode,
error_description: errorMessage,
status: statusCode,
original_error: errorBody,
auth_type: authType,
};
}
catch (e) {
// Failed to parse as JSON, use as text
errorMessage = remainingText;
errorBody = { raw_text: remainingText };
}
}
else {
// Not JSON format, use as text
errorMessage = remainingText;
errorBody = { raw_text: remainingText };
}
}
else {
// Not status code format, try to parse whole body as JSON
try {
errorBody = JSON.parse(error.response.body);
if (errorBody.error) {
errorCode = errorBody.error;
}
if (errorBody.error_description) {
errorMessage = errorBody.error_description;
}
}
catch (e) {
// Not JSON, use as plain text
errorMessage = error.response.body;
errorBody = { raw_text: error.response.body };
}
}
}
else {
// Body is already an object
errorBody = error.response.body;
if (errorBody.error) {
errorCode = errorBody.error;
}
if (errorBody.error_description) {
errorMessage = errorBody.error_description;
}
}
}
catch (e) {
// Ignore JSON parse errors
}
}
else if (error.statusCode === 401) {
errorMessage = `Authentication failed (${authType})`;
errorCode = "AUTH_FAILED";
}
else if (error.statusCode === 404) {
errorMessage = `Endpoint not found: ${((_b = error.options) === null || _b === void 0 ? void 0 : _b.url) || "unknown"}`;
errorCode = "ENDPOINT_NOT_FOUND";
}
else if (error.message) {
errorMessage = error.message;
}
// Return error response instead of throwing
return {
error: errorCode,
error_description: errorMessage,
status: statusCode,
original_error: errorBody,
auth_type: authType,
};
}
/**
* Simplified function to make a standard Bitrix24 call
*/
async function makeStandardBitrix24Call(endpoint, body = {}, qs = {}, itemIndex = 0, getAllItems = false, callOptions = {}) {
// Get authentication type from node parameters or from callOptions
let authType = callOptions.authType;
if (!authType) {
authType = this.getNodeParameter("authentication", itemIndex, "oAuth2");
}
// Check if we should fall back to webhook if OAuth2 fails
const fallbackToWebhook = authType === "oAuth2"
? this.getNodeParameter("fallbackToWebhook", itemIndex, false)
: false;
// Get additional options if any
let options = {};
try {
options = this.getNodeParameter("options", itemIndex, {});
}
catch (e) {
// Options parameter is optional
}
// Merge with call options
options = { ...options, ...callOptions };
// Add auth-related options
options.authType = authType.toLowerCase();
options.fallbackToWebhook = fallbackToWebhook;
// Check for access_token in options and make sure it's a string
if (options.access_token) {
options.accessToken = String(options.access_token).trim();
// Log that we're using access_token from options
console.log(`makeStandardBitrix24Call: Using access_token from options`);
}
// Check for accessToken from callOptions
if (options.accessToken) {
options.accessToken = String(options.accessToken).trim();
console.log(`makeStandardBitrix24Call: Using accessToken from callOptions`);
}
// Make the API call
return await bitrix24Request.call(this, endpoint, body, qs, options);
}
exports.makeStandardBitrix24Call = makeStandardBitrix24Call;
/**
* Make an API request to load all items from paginated Bitrix24 API
*/
async function bitrix24ApiRequestAllItems(propertyName, endpoint, body = {}, query = {}, uri) {
var _a;
const returnData = [];
let responseData;
// Đảm bảo query là một đối tượng và start là một thuộc tính
if (typeof query !== "object") {
query = {};
}
// Đặt giá trị start ban đầu
query.start = typeof query.start === "undefined" ? 0 : query.start;
do {
try {
// Use the unified request method
responseData = await bitrix24Request.call(this, endpoint, body, query, uri);
// If the response has result but the property name doesn't exist, return full response
if (!responseData ||
!responseData.result ||
responseData.result[propertyName] === undefined) {
return responseData;
}
// If the property is not an array
if (!Array.isArray(responseData.result[propertyName])) {
if (responseData.result[propertyName]) {
returnData.push(responseData.result[propertyName]);
}
break;
}
// Add the items to our return data
returnData.push.apply(returnData, responseData.result[propertyName]);
// Increment the start position for the next page
// Đảm bảo chuyển start thành số trước khi tăng
query.start = parseInt(query.start, 10) + 50; // Default Bitrix24 page size
}
catch (error) {
break;
}
} while (((_a = responseData.result[propertyName]) === null || _a === void 0 ? void 0 : _a.length) > 0);
// Return the full collected data
return {
result: returnData,
};
}
exports.bitrix24ApiRequestAllItems = bitrix24ApiRequestAllItems;
/**
* Download a file from a URL
*/
async function bitrix24DownloadFile(downloadUrl) {
try {
const options = {
method: "GET",
url: downloadUrl,
encoding: null,
};
return await this.helpers.httpRequest(options);
}
catch (error) {
throw new Error(`Failed to download file: ${error.message}`);
}
}
exports.bitrix24DownloadFile = bitrix24DownloadFile;
/**
* Validate that the binary data field exists in the input
*/
function validateBinaryDataExists(helpers, item, binaryPropertyName) {
if (item.binary === undefined) {
throw new Error("No binary data exists on item!");
}
if (item.binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" exists on item!`);
}
}
exports.validateBinaryDataExists = validateBinaryDataExists;
/**
* Test if credentials are valid
*/
async function testAuth(credential) {
const { helpers } = this;
// Test the credentials based on which type is being used
if (credential.hasOwnProperty("webhookUrl")) {
// Test webhook-based API
const webhookUrl = credential.webhookUrl;
try {
const options = {
method: "POST",
url: `${webhookUrl}user.current`,
json: true,
};
await helpers.request(options);
return true;
}
catch (error) {
return false;
}
}
else if (credential.hasOwnProperty("accessToken") &&
credential.hasOwnProperty("portalUrl")) {
// Test API Key-based API
try {
const portalUrl = credential.portalUrl;
const accessToken = credential.accessToken;
// Make sure portal URL doesn't end with /
const baseUrl = portalUrl.replace(/\/$/, "");
const options = {
method: "POST",
url: `${baseUrl}/rest/user.current`,
qs: {
auth: accessToken,
},
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
json: true,
};
await helpers.request(options);
return true;
}
catch (error) {
return false;
}
}
else {
// Test OAuth2-based API
try {
const portalUrl = credential.portalUrl;
// Make sure portal URL doesn't end with /
const baseUrl = portalUrl.replace(/\/$/, "");
const options = {
method: "POST",
url: `${baseUrl}/rest/user.current`,
json: true,
};
await helpers.request(options, {
oauth2: true,
credentialsType: "bitrix24OAuth",
});
return true;
}
catch (error) {
return false;
}
}
}
exports.testAuth = testAuth;
/**
* Process and return full Bitrix24 API response
* Unified handler for all response types
*/
function returnFullBitrix24Response(responseData, helpers, itemIndex) {
// Return the full response with all data exactly as received
// without modifying its structure
return helpers.constructExecutionMetaData(helpers.returnJsonArray(responseData), { itemData: { item: itemIndex } });
}
exports.returnFullBitrix24Response = returnFullBitrix24Response;
/**
* Load resource options for use in dropdown selectors
*/
function getEntityFields(entityType) {
return new Promise(async (resolve, reject) => {
try {
// Get entity fields
const endpoint = `crm.${entityType}.fields`;
// Use the unified request method
const response = await bitrix24Request.call(this, endpoint);
if (!response || !response.result) {
return resolve([]);
}
// Process response to format n8n expects
const options = [];
for (const key in response.result) {
const field = response.result[key];
// Custom fields (UF_*) handling
if (key.startsWith("UF_")) {
// Try to find the best label for custom fields
let fieldName = "";
// Check various possible label properties in order of preference
if (field.formLabel) {
fieldName = field.formLabel;
}
else if (field.listLabel) {
fieldName = field.listLabel;
}
else if (field.editFormLabel) {
fieldName = field.editFormLabel;
}
else if (field.title) {
fieldName = field.title;
}
else if (field.NAME) {
fieldName = field.NAME;
}
else if (field.name) {
fieldName = field.name;
}
else if (field.FIELD_NAME) {
fieldName = field.FIELD_NAME;
}
else {
// If no label found, use the field ID but mark it as custom
fieldName = `Custom Field: ${key}`;
}
options.push({
name: fieldName,
value: key,
description: `Custom field (${key})`,
});
}
else {
// Standard fields
options.push({
name: field.title ||
field.formLabel ||
field.listLabel ||
field.NAME ||
field.name ||
key,
value: key,
});
}
}
return resolve(options);
}
catch (error) {
reject(error);
}
});
}
exports.getEntityFields = getEntityFields;
/**
* Refresh the access token using the refresh token
*/
async function refreshAccessToken(credentials) {
const portalUrl = credentials.portalUrl;
const clientId = credentials.clientId;
const clientSecret = credentials.clientSecret;
const refreshToken = credentials.refreshToken;
if (!refreshToken) {
throw new Error("No refresh token available");
}
if (!clientId || !clientSecret) {
throw new Error("Client ID and Client Secret are required for token refresh");
}
const options = {
method: "GET",
url: `https://oauth.bitrix.info/oauth/token/`,
qs: {
grant_type: "refresh_token",
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
},
json: true,
};
try {
const response = await this.helpers.httpRequest(options);
// Return the new tokens
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresIn: response.expires_in,
};
}
catch (error) {
throw new Error(`Failed to refresh access token: ${error.message}`);
}
}
/**
* Make an API request to Bitrix24 using API Key
*/
async function bitrix24ApiKeyRequest(method, endpoint, body = {}, query = {}, itemIndex = 0) {
try {
const credentials = await this.getCredentials("bitrix24Api");
// Ensure itemIndex is a valid number
const actualItemIndex = typeof itemIndex === "number" && !isNaN(itemIndex) ? itemIndex : 0;
// Check if a different access token is set on the item level
let accessToken = "";
try {
// Only try to get an item-specific token if we have a valid item index
if (typeof this.getNodeParameter === "function") {
try {
const options = this.getNodeParameter("options", actualItemIndex, {});
accessToken = options.accessToken || "";
}
catch (error) {
// Do nothing, no access token set on item level
}
}
}
catch (error) {
// Error getting item-specific access token
}
if (accessToken === "") {
accessToken = credentials.accessToken;
}
// Ensure auth parameter is properly set
query.auth = accessToken;
const options = {
headers: {
"Content-Type": "application/json",
},
method,
body,
qs: query,
url: `${credentials.portalUrl}${credentials.portalUrl.endsWith("/") ? "" : "/"}rest/${endpoint}`,
json: true,
};
try {
const response = await this.helpers.httpRequest(options);
return response;
}
catch (error) {
// Handle errors specifically
if (error.statusCode === 401 &&
error.error &&
error.error.error === "expired_token") {
// Access token expired, attempt to refresh
try {
const refreshToken = credentials.refreshToken;
const refreshOptions = {
headers: {
"Content-Type": "application/json",
},
method: "GET",
qs: {
client_id: credentials.clientId,
grant_type: "refresh_token",
client_secret: credentials.clientSecret,
refresh_token: refreshToken,
},
url: `${credentials.portalUrl}${credentials.portalUrl.endsWith("/") ? "" : "/"}oauth/token/`,
json: true,
};
const refreshResponse = await this.helpers.httpRequest(refreshOptions);
if (refreshResponse.access_token) {
// Token refreshed successfully
try {
// Try to update credentials if possible, but continue even if it fails
// @ts-ignore - updateCredentials may exist on execute functions
if (typeof this.updateCredentials === "function") {
// @ts-ignore
await this.updateCredentials({
...credentials,
accessToken: refreshResponse.access_token,
refreshToken: refreshResponse.refresh_token || credentials.refreshToken,
}, "bitrix24Api");
}
}
catch (credentialUpdateError) {
// Continue even if updating credentials fails
}
// Update the query with new token and retry the request
query.auth = refreshResponse.access_token;
options.qs = query;
try {
// Retry the original request with new token
const retryResponse = await this.helpers.httpRequest(options);
return retryResponse;
}
catch (retryError) {
// If retry also fails, but for a different reason (not auth)
// Only continue with other auth methods if it's still an auth error
if (retryError.statusCode === 401) {
throw new Error(`Token refresh succeeded but authentication still failed: ${retryError.message}`);
}
else {
// For non-auth errors, throw the specific error to show the user
if (retryError.error && retryError.error.error_description) {
throw new Error(`${retryError.error.error_description} (Error code: ${retryError.error.error})`);
}
else {
throw retryError;
}
}
}
}
else {
throw new Error(`Token refresh failed: ${JSON.stringify(refreshResponse)}`);
}
}
catch (refreshError) {
throw new Error(`Token refresh failed: ${refreshError.message}`);
}
}
// Original error is not an expired token, or refresh failed
// Just pass through the original error
if (error.error && error.error.error_description) {
throw new Error(`${error.error.error_description} (Error code: ${error.error.error})`);
}
else {
throw error;
}
}
}
catch (originalError) {
// Don't try other authentication methods, just throw the original error
// User explicitly selected API key authentication
throw new Error(`Bitrix24 API Key authentication failed: ${originalError.message}`);
}
}
exports.bitrix24ApiKeyRequest = bitrix24ApiKeyRequest;
//# sourceMappingURL=GenericFunctions.js.map