/ clis / twitter / notifications.js
notifications.js
  1  import { CommandExecutionError } from '@jackwener/opencli/errors';
  2  import { cli, Strategy } from '@jackwener/opencli/registry';
  3  cli({
  4      site: 'twitter',
  5      name: 'notifications',
  6      description: 'Get Twitter/X notifications',
  7      domain: 'x.com',
  8      strategy: Strategy.INTERCEPT,
  9      browser: true,
 10      args: [
 11          { name: 'limit', type: 'int', default: 20 },
 12      ],
 13      columns: ['id', 'action', 'author', 'text', 'url'],
 14      func: async (page, kwargs) => {
 15          // 1. Navigate to home first (we need a loaded Twitter page for SPA navigation)
 16          await page.goto('https://x.com/home');
 17          await page.wait(3);
 18          // 2. Install interceptor BEFORE SPA navigation
 19          await page.installInterceptor('NotificationsTimeline');
 20          // 3. SPA navigate to notifications via history API
 21          await page.evaluate(`() => {
 22          window.history.pushState({}, '', '/notifications');
 23          window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
 24      }`);
 25          await page.waitForCapture(5);
 26          // Verify SPA navigation succeeded
 27          const currentUrl = await page.evaluate('() => window.location.pathname');
 28          if (currentUrl !== '/notifications') {
 29              throw new CommandExecutionError('SPA navigation to notifications failed. Twitter may have changed its routing.');
 30          }
 31          // 4. Scroll to trigger pagination
 32          await page.autoScroll({ times: 2, delayMs: 2000 });
 33          // 5. Retrieve data
 34          const requests = await page.getInterceptedRequests();
 35          if (!requests || requests.length === 0)
 36              return [];
 37          let results = [];
 38          const seen = new Set();
 39          for (const req of requests) {
 40              try {
 41                  // GraphQL response: { data: { viewer: ... } } (one level of .data)
 42                  let instructions = [];
 43                  if (req.data?.viewer?.timeline_response?.timeline?.instructions) {
 44                      instructions = req.data.viewer.timeline_response.timeline.instructions;
 45                  }
 46                  else if (req.data?.viewer_v2?.user_results?.result?.notification_timeline?.timeline?.instructions) {
 47                      instructions = req.data.viewer_v2.user_results.result.notification_timeline.timeline.instructions;
 48                  }
 49                  else if (req.data?.timeline?.instructions) {
 50                      instructions = req.data.timeline.instructions;
 51                  }
 52                  let addEntries = instructions.find((i) => i.type === 'TimelineAddEntries');
 53                  if (!addEntries) {
 54                      addEntries = instructions.find((i) => i.entries && Array.isArray(i.entries));
 55                  }
 56                  if (!addEntries)
 57                      continue;
 58                  for (const entry of addEntries.entries) {
 59                      if (!entry.entryId.startsWith('notification-')) {
 60                          if (entry.content?.items) {
 61                              for (const subItem of entry.content.items) {
 62                                  processNotificationItem(subItem.item?.itemContent, subItem.entryId);
 63                              }
 64                          }
 65                          continue;
 66                      }
 67                      processNotificationItem(entry.content?.itemContent, entry.entryId);
 68                  }
 69                  function processNotificationItem(itemContent, entryId) {
 70                      if (!itemContent)
 71                          return;
 72                      let item = itemContent?.notification_results?.result || itemContent?.tweet_results?.result || itemContent;
 73                      let actionText = 'Notification';
 74                      let author = 'unknown';
 75                      let text = '';
 76                      let urlStr = '';
 77                      if (item.__typename === 'TimelineNotification') {
 78                          text = item.rich_message?.text || item.message?.text || '';
 79                          const fromUser = item.template?.from_users?.[0]?.user_results?.result;
 80                          // Twitter moved screen_name from legacy to core
 81                          author = fromUser?.core?.screen_name || fromUser?.legacy?.screen_name || 'unknown';
 82                          urlStr = item.notification_url?.url || '';
 83                          actionText = item.notification_icon || 'Activity';
 84                          const targetTweet = item.template?.target_objects?.[0]?.tweet_results?.result;
 85                          if (targetTweet) {
 86                              const targetText = targetTweet.note_tweet?.note_tweet_results?.result?.text || targetTweet.legacy?.full_text || '';
 87                              text += text && targetText ? ' | ' + targetText : targetText;
 88                              if (!urlStr) {
 89                                  urlStr = `https://x.com/i/status/${targetTweet.rest_id}`;
 90                              }
 91                          }
 92                      }
 93                      else if (item.__typename === 'TweetNotification') {
 94                          const tweet = item.tweet_result?.result;
 95                          const tweetUser = tweet?.core?.user_results?.result;
 96                          author = tweetUser?.core?.screen_name || tweetUser?.legacy?.screen_name || 'unknown';
 97                          text = tweet?.note_tweet?.note_tweet_results?.result?.text || tweet?.legacy?.full_text || item.message?.text || '';
 98                          actionText = 'Mention/Reply';
 99                          urlStr = `https://x.com/i/status/${tweet?.rest_id}`;
100                      }
101                      else if (item.__typename === 'Tweet') {
102                          const tweetUser = item.core?.user_results?.result;
103                          author = tweetUser?.core?.screen_name || tweetUser?.legacy?.screen_name || 'unknown';
104                          text = item.note_tweet?.note_tweet_results?.result?.text || item.legacy?.full_text || '';
105                          actionText = 'Mention';
106                          urlStr = `https://x.com/i/status/${item.rest_id}`;
107                      }
108                      const id = item.id || item.rest_id || entryId;
109                      if (seen.has(id))
110                          return;
111                      seen.add(id);
112                      results.push({
113                          id,
114                          action: actionText,
115                          author: author,
116                          text: text,
117                          url: urlStr || `https://x.com/notifications`
118                      });
119                  }
120              }
121              catch (e) {
122                  // ignore parsing errors for individual payloads
123              }
124          }
125          return results.slice(0, kwargs.limit);
126      }
127  });