/ clis / instagram / note.js
note.js
  1  import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
  2  import { cli, Strategy } from '@jackwener/opencli/registry';
  3  const INSTAGRAM_INBOX_URL = 'https://www.instagram.com/direct/inbox/';
  4  const INSTAGRAM_NOTE_DOC_ID = '25155183657506484';
  5  const INSTAGRAM_NOTE_MUTATION_NAME = 'usePolarisCreateInboxTrayItemSubmitMutation';
  6  const INSTAGRAM_NOTE_ROOT_FIELD = 'xdt_create_inbox_tray_item';
  7  function requirePage(page) {
  8      if (!page)
  9          throw new CommandExecutionError('Browser session required for instagram note');
 10      return page;
 11  }
 12  function validateInstagramNoteArgs(kwargs) {
 13      if (kwargs.content === undefined) {
 14          throw new ArgumentError('Argument "content" is required.', 'Provide a note text, for example: opencli instagram note "hello"');
 15      }
 16  }
 17  function normalizeInstagramNoteContent(kwargs) {
 18      const content = String(kwargs.content ?? '').trim();
 19      if (!content) {
 20          throw new ArgumentError('Instagram note content cannot be empty.', 'Provide a non-empty note text, for example: opencli instagram note "hello"');
 21      }
 22      if (Array.from(content).length > 60) {
 23          throw new ArgumentError('Instagram note content must be 60 characters or fewer.', 'Shorten the note text and try again.');
 24      }
 25      return content;
 26  }
 27  function buildNoteSuccessResult(noteId) {
 28      return [{
 29              status: '✅ Posted',
 30              detail: 'Instagram note published successfully',
 31              noteId,
 32          }];
 33  }
 34  function buildPublishInstagramNoteJs(content) {
 35      return `
 36      (async () => {
 37        const input = ${JSON.stringify({ content })};
 38        const html = document.documentElement?.outerHTML || '';
 39        const scripts = Array.from(document.scripts || [])
 40          .map((script) => script.textContent || '')
 41          .join('\\n');
 42        const source = html + '\\n' + scripts;
 43        const pick = (patterns) => {
 44          for (const pattern of patterns) {
 45            const match = source.match(pattern);
 46            if (!match) continue;
 47            for (let index = 1; index < match.length; index += 1) {
 48              if (match[index]) return match[index];
 49            }
 50            return match[0] || '';
 51          }
 52          return '';
 53        };
 54        const readCookie = (name) => {
 55          const prefix = name + '=';
 56          const part = document.cookie
 57            .split('; ')
 58            .find((cookie) => cookie.startsWith(prefix));
 59          return part ? decodeURIComponent(part.slice(prefix.length)) : '';
 60        };
 61        const actorId = pick([
 62          /"actorID":"(\\d+)"/,
 63          /"actor_id":"(\\d+)"/,
 64          /"viewerId":"(\\d+)"/,
 65        ]);
 66        const fbDtsg = pick([
 67          /(NAF[a-zA-Z0-9:_-]{20,})/,
 68          /(NAf[a-zA-Z0-9:_-]{20,})/,
 69        ]);
 70        const lsd = pick([
 71          /"LSD",\\[\\],\\{"token":"([^"]+)"\\}/,
 72          /"lsd":"([^"]+)"/,
 73        ]);
 74        const appId = pick([
 75          /"X-IG-App-ID":"(\\d+)"/,
 76          /"instagramWebAppId":"(\\d+)"/,
 77          /"appId":"(\\d+)"/,
 78        ]);
 79        const asbdId = pick([
 80          /"X-ASBD-ID":"(\\d+)"/,
 81          /"asbd_id":"(\\d+)"/,
 82        ]);
 83        const spinR = pick([/"__spin_r":(\\d+)/]);
 84        const spinB = pick([/"__spin_b":"([^"]+)"/]);
 85        const spinT = pick([/"__spin_t":(\\d+)/]);
 86        const csrfToken = readCookie('csrftoken') || pick([
 87          /"csrf_token":"([^"]+)"/,
 88          /"csrfToken":"([^"]+)"/,
 89        ]);
 90        const jazoest = fbDtsg
 91          ? '2' + Array.from(fbDtsg).reduce((total, char) => total + char.charCodeAt(0), 0)
 92          : '';
 93  
 94        if (!actorId || !fbDtsg || !lsd || !appId || !csrfToken || !spinR || !spinB || !spinT || !jazoest) {
 95          return {
 96            ok: false,
 97            stage: 'config',
 98            text: JSON.stringify({
 99              actorId: Boolean(actorId),
100              fbDtsg: Boolean(fbDtsg),
101              lsd: Boolean(lsd),
102              appId: Boolean(appId),
103              csrfToken: Boolean(csrfToken),
104              spinR: Boolean(spinR),
105              spinB: Boolean(spinB),
106              spinT: Boolean(spinT),
107              jazoest: Boolean(jazoest),
108            }),
109          };
110        }
111  
112        const variables = {
113          input: {
114            actor_id: actorId,
115            client_mutation_id: '1',
116            additional_params: {
117              note_create_params: {
118                note_style: 0,
119                text: input.content,
120              },
121            },
122            audience: 0,
123            inbox_tray_item_type: 'note',
124          },
125        };
126  
127        const body = new URLSearchParams();
128        body.set('av', actorId);
129        body.set('__user', '0');
130        body.set('__a', '1');
131        body.set('__req', '1');
132        body.set('__hs', '');
133        body.set('dpr', String(window.devicePixelRatio || 1));
134        body.set('__ccg', 'UNKNOWN');
135        body.set('__rev', spinR);
136        body.set('__s', '');
137        body.set('__hsi', '');
138        body.set('__dyn', '');
139        body.set('__csr', '');
140        body.set('__comet_req', '7');
141        body.set('fb_dtsg', fbDtsg);
142        body.set('jazoest', jazoest);
143        body.set('lsd', lsd);
144        body.set('__spin_r', spinR);
145        body.set('__spin_b', spinB);
146        body.set('__spin_t', spinT);
147        body.set('fb_api_caller_class', 'RelayModern');
148        body.set('fb_api_req_friendly_name', ${JSON.stringify(INSTAGRAM_NOTE_MUTATION_NAME)});
149        body.set('variables', JSON.stringify(variables));
150        body.set('server_timestamps', 'true');
151        body.set('doc_id', ${JSON.stringify(INSTAGRAM_NOTE_DOC_ID)});
152  
153        const headers = {
154          Accept: '*/*',
155          'Content-Type': 'application/x-www-form-urlencoded',
156          'X-ASBD-ID': asbdId || undefined,
157          'X-CSRFToken': csrfToken,
158          'X-FB-Friendly-Name': ${JSON.stringify(INSTAGRAM_NOTE_MUTATION_NAME)},
159          'X-FB-LSD': lsd,
160          'X-IG-App-ID': appId,
161          'X-Root-Field-Name': ${JSON.stringify(INSTAGRAM_NOTE_ROOT_FIELD)},
162        };
163  
164        const response = await fetch('/graphql/query', {
165          method: 'POST',
166          credentials: 'include',
167          headers,
168          body: body.toString(),
169        });
170        const text = await response.text();
171        const normalizedText = text.replace(/^for \\(;;\\);?/, '').trim();
172        let data = null;
173        try {
174          data = JSON.parse(normalizedText);
175        } catch {}
176  
177        const rootField = ${JSON.stringify(INSTAGRAM_NOTE_ROOT_FIELD)};
178        const note = data?.data?.[rootField]?.inbox_tray_item;
179        const noteId = String(note?.inbox_tray_item_id || note?.id || '');
180        if (response.ok && noteId) {
181          return {
182            ok: true,
183            stage: 'publish',
184            noteId,
185            text: String(note?.note_dict?.text || input.content || ''),
186          };
187        }
188  
189        return {
190          ok: false,
191          stage: 'publish',
192          status: response.status,
193          text: normalizedText || text,
194        };
195      })()
196    `;
197  }
198  cli({
199      site: 'instagram',
200      name: 'note',
201      description: 'Publish a text Instagram note',
202      domain: 'www.instagram.com',
203      strategy: Strategy.UI,
204      browser: true,
205      timeoutSeconds: 120,
206      args: [
207          { name: 'content', positional: true, required: true, help: 'Note text (max 60 characters)' },
208      ],
209      columns: ['status', 'detail', 'noteId'],
210      validateArgs: validateInstagramNoteArgs,
211      func: async (page, kwargs) => {
212          const browserPage = requirePage(page);
213          const content = normalizeInstagramNoteContent(kwargs);
214          await browserPage.goto(INSTAGRAM_INBOX_URL);
215          await browserPage.wait({ time: 2 });
216          const result = await browserPage.evaluate(buildPublishInstagramNoteJs(content));
217          if (!result?.ok) {
218              throw new CommandExecutionError(`Instagram note publish failed at ${String(result?.stage || 'unknown')}: ${String(result?.text || 'unknown error')}`);
219          }
220          return buildNoteSuccessResult(String(result.noteId || ''));
221      },
222  });