@inite/n8n-nodes-instagram-private-api
Version:
n8n node for Instagram Private API with 2FA support
281 lines (280 loc) • 12.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstagramTrigger = void 0;
const instagram_private_api_1 = require("instagram-private-api");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
// Define error types locally since they're not exported from the package
class IgLoginRequiredError extends Error {
constructor(message) {
super(message);
this.name = 'IgLoginRequiredError';
}
}
class IgCheckpointError extends Error {
constructor(message) {
super(message);
this.name = 'IgCheckpointError';
}
}
class IgNotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'IgNotFoundError';
}
}
class InstagramTrigger {
constructor() {
this.description = {
displayName: 'Instagram Trigger',
name: 'instagramTrigger',
icon: 'file:instagram.svg',
group: ['trigger'],
version: 1,
// subtitle: '={{$parameter["event"]}}', // Keep commented out from previous steps
description: 'Triggers when Instagram events occur',
defaults: {
name: 'Instagram Trigger',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'instagramPrivateApi',
required: true,
},
],
// Restore properties definition
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
noDataExpression: true,
options: [
{
name: 'New Direct Message',
value: 'newDirectMessage',
description: 'Triggers when a new direct message is received',
},
],
default: 'newDirectMessage',
},
{
displayName: 'Polling Interval',
name: 'pollingInterval',
type: 'number',
default: 60,
description: 'How often to poll for new messages (in seconds)',
// Keep displayOptions commented out from previous steps
// displayOptions: {
// show: {
// event: ['newDirectMessage'],
// },
// },
},
],
};
// Remove the poll method entirely
// async poll(...) { ... }
}
// Refactor trigger method
async trigger() {
console.log('--- Trigger Method Start (Self-Polling) ---');
// Get parameters - ACCESS PARAMETERS HERE
const event = this.getNodeParameter('event', 0);
const pollingIntervalSeconds = this.getNodeParameter('pollingInterval', 0);
// Get credentials
const credentials = await this.getCredentials('instagramPrivateApi');
console.log(`Credentials obtained for user: ${credentials.username}`);
// --- Instagram Client & Session Setup (Copied from original poll method) ---
const ig = new instagram_private_api_1.IgApiClient();
ig.state.generateDevice(credentials.username);
const sessionDir = path.join(os.homedir(), '.n8n', 'instagram-sessions');
const sessionFilePath = path.join(sessionDir, `${credentials.username}.json`);
if (!fs.existsSync(sessionDir)) {
fs.mkdirSync(sessionDir, { recursive: true });
}
let capturedSessionData = null;
// Session loading function (to avoid duplicating in interval)
const loadAndVerifySession = async () => {
let sessionData;
let isAuthenticated = false;
if (credentials.sessionData) {
sessionData = credentials.sessionData;
}
if (fs.existsSync(sessionFilePath)) {
try {
const fileData = fs.readFileSync(sessionFilePath, 'utf8');
const parsedData = JSON.parse(fileData);
sessionData = parsedData.sessionData;
console.log('Session loaded from file');
}
catch (error) {
console.error('Failed to load session from file:', error);
}
}
if (sessionData) {
try {
await ig.state.deserialize(sessionData);
console.log('Session loaded successfully');
try {
await ig.account.currentUser();
console.log('Session is valid');
isAuthenticated = true;
}
catch (error) {
console.log('Session is invalid, re-authenticating...');
await ig.account.login(credentials.username, credentials.password);
console.log('Successfully re-authenticated with Instagram');
isAuthenticated = true;
}
}
catch (error) {
console.error('Failed to load session:', error);
}
}
if (!isAuthenticated) {
console.log('Authenticating with Instagram...');
try {
await ig.account.login(credentials.username, credentials.password);
console.log('Successfully authenticated with Instagram');
isAuthenticated = true;
}
catch (error) {
console.error('Failed to authenticate with Instagram:', error);
// Handle auth failure appropriately - maybe stop the interval?
throw new Error('Failed to authenticate with Instagram. Please check your credentials.');
}
}
return isAuthenticated;
};
// Initial session load/auth
await loadAndVerifySession();
// Session capture setup
ig.request.end$.subscribe(async () => {
try {
const serialized = await ig.state.serialize();
delete serialized.constants;
capturedSessionData = serialized;
try {
fs.writeFileSync(sessionFilePath, JSON.stringify({ sessionData: capturedSessionData }), 'utf8');
// console.log('Session data saved to file'); // Reduce logging noise
}
catch (error) {
console.error('Failed to save session to file:', error);
}
}
catch (error) {
console.error('Failed to serialize session:', error);
}
});
// --- Polling Logic (Moved from poll method) ---
let isRunning = false; // Prevent overlapping runs
const pollMessages = async () => {
if (isRunning) {
console.log('Polling already in progress, skipping this interval.');
return;
}
isRunning = true;
console.log('Polling for new messages...');
try {
// Ensure session is still valid before polling
if (!(await loadAndVerifySession())) {
console.error('Authentication failed, cannot poll messages.');
isRunning = false;
return; // Stop polling if auth fails
}
const messages = await ig.feed.directInbox().items();
const unreadMessages = messages.filter((message) => message.read_state > 0);
console.log(`Found ${unreadMessages.length} unread messages`);
if (unreadMessages.length > 0) {
const outputDataArray = [];
for (const message of unreadMessages) {
const lastItem = message.last_permanent_item;
if (!lastItem) {
console.log(`No last item found for thread ${message.thread_id}`);
continue;
}
const outputData = {
json: {
threadId: message.thread_id,
messageId: lastItem.item_id,
message: lastItem.text,
senderId: lastItem.user_id,
timestamp: lastItem.timestamp,
threadTitle: message.thread_title,
sessionData: capturedSessionData,
sessionFilePath,
},
};
outputDataArray.push(outputData);
// Mark as seen
try {
if (message.thread_id) {
await ig.direct.markItemSeen(message.thread_id, lastItem.item_id);
console.log(`Marked message ${lastItem.item_id} as seen in thread ${message.thread_id}`);
}
else {
console.log(`Cannot mark message as seen: thread_id is null or undefined`);
}
}
catch (markError) {
console.error(`Failed to mark message as seen:`, markError);
}
}
// Emit the data - IMPORTANT: Use this.emit() for self-managed triggers
if (outputDataArray.length > 0) {
console.log(`Emitting ${outputDataArray.length} messages`);
this.emit([outputDataArray]);
}
}
}
catch (error) {
console.error('Error during polling:', error);
// Consider stopping the interval on certain errors?
}
finally {
isRunning = false;
}
};
// Start the interval timer
const intervalId = setInterval(pollMessages, pollingIntervalSeconds * 1000);
console.log(`Polling interval set for ${pollingIntervalSeconds} seconds.`);
// For self-managed triggers like setInterval, return an empty object or specific response if needed
// The 'close' function is typically used when n8n manages the polling via the 'poll' method
// Returning a function to clear the interval IS useful if n8n calls it on deactivation
// Let's keep the close function for cleanup
return {
closeFunction: async () => {
console.log('--- Clearing Polling Interval --- ');
clearInterval(intervalId);
},
};
}
}
exports.InstagramTrigger = InstagramTrigger;