X Post Scraper & Saver

Save X (Twitter) posts content for later viewing

Size

15.0 KB

Version

1.0.1

Created

Oct 21, 2025

Updated

2 days ago

1// ==UserScript==
2// @name		X Post Scraper & Saver
3// @description		Save X (Twitter) posts content for later viewing
4// @version		1.0.1
5// @match		https://*.x.com/*
6// @icon		https://abs.twimg.com/favicons/twitter.3.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('X Post Scraper & Saver initialized');
12
13    // Debounce function to prevent excessive calls
14    function debounce(func, wait) {
15        let timeout;
16        return function executedFunction(...args) {
17            const later = () => {
18                clearTimeout(timeout);
19                func(...args);
20            };
21            clearTimeout(timeout);
22            timeout = setTimeout(later, wait);
23        };
24    }
25
26    // Extract post data from a tweet article element
27    function extractPostData(article) {
28        try {
29            const postData = {
30                id: article.getAttribute('aria-labelledby') || Date.now().toString(),
31                timestamp: new Date().toISOString(),
32                author: '',
33                username: '',
34                content: '',
35                images: [],
36                links: [],
37                stats: {
38                    replies: 0,
39                    reposts: 0,
40                    likes: 0,
41                    views: 0
42                }
43            };
44
45            // Extract author info
46            const authorLink = article.querySelector('a[role="link"][href^="/"]');
47            if (authorLink) {
48                const authorName = article.querySelector('div[data-testid="User-Name"] span');
49                if (authorName) {
50                    postData.author = authorName.textContent.trim();
51                }
52                postData.username = authorLink.getAttribute('href');
53            }
54
55            // Extract tweet text content
56            const tweetText = article.querySelector('div[data-testid="tweetText"]');
57            if (tweetText) {
58                postData.content = tweetText.textContent.trim();
59            }
60
61            // Extract images
62            const images = article.querySelectorAll('div[data-testid="tweetPhoto"] img');
63            images.forEach(img => {
64                if (img.src) {
65                    postData.images.push(img.src);
66                }
67            });
68
69            // Extract links
70            const links = article.querySelectorAll('div[data-testid="tweetText"] a[href]');
71            links.forEach(link => {
72                postData.links.push({
73                    text: link.textContent,
74                    url: link.href
75                });
76            });
77
78            // Extract engagement stats
79            const statsGroup = article.querySelector('div[role="group"]');
80            if (statsGroup) {
81                const ariaLabel = statsGroup.getAttribute('aria-label');
82                if (ariaLabel) {
83                    const repliesMatch = ariaLabel.match(/(\d+)\s+repl/i);
84                    const repostsMatch = ariaLabel.match(/(\d+)\s+repost/i);
85                    const likesMatch = ariaLabel.match(/(\d+)\s+like/i);
86                    const viewsMatch = ariaLabel.match(/(\d+)\s+view/i);
87
88                    if (repliesMatch) postData.stats.replies = parseInt(repliesMatch[1]);
89                    if (repostsMatch) postData.stats.reposts = parseInt(repostsMatch[1]);
90                    if (likesMatch) postData.stats.likes = parseInt(likesMatch[1]);
91                    if (viewsMatch) postData.stats.views = parseInt(viewsMatch[1]);
92                }
93            }
94
95            return postData;
96        } catch (error) {
97            console.error('Error extracting post data:', error);
98            return null;
99        }
100    }
101
102    // Save post to storage
103    async function savePost(postData) {
104        try {
105            const savedPosts = await GM.getValue('saved_posts', []);
106            
107            // Check if post already exists
108            const existingIndex = savedPosts.findIndex(p => p.id === postData.id);
109            if (existingIndex !== -1) {
110                console.log('Post already saved, updating...');
111                savedPosts[existingIndex] = postData;
112            } else {
113                savedPosts.unshift(postData); // Add to beginning
114            }
115
116            await GM.setValue('saved_posts', savedPosts);
117            console.log('Post saved successfully:', postData);
118            return true;
119        } catch (error) {
120            console.error('Error saving post:', error);
121            return false;
122        }
123    }
124
125    // Create save button for a tweet
126    function createSaveButton(article) {
127        // Check if button already exists
128        if (article.querySelector('.rm-save-post-btn')) {
129            return;
130        }
131
132        const saveButton = document.createElement('button');
133        saveButton.className = 'rm-save-post-btn';
134        saveButton.innerHTML = '💾 Save';
135        saveButton.style.cssText = `
136            position: absolute;
137            top: 8px;
138            right: 8px;
139            background: #1d9bf0;
140            color: white;
141            border: none;
142            border-radius: 20px;
143            padding: 6px 16px;
144            font-size: 13px;
145            font-weight: 600;
146            cursor: pointer;
147            z-index: 10;
148            transition: background 0.2s;
149            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
150        `;
151
152        saveButton.addEventListener('mouseenter', () => {
153            saveButton.style.background = '#1a8cd8';
154        });
155
156        saveButton.addEventListener('mouseleave', () => {
157            saveButton.style.background = '#1d9bf0';
158        });
159
160        saveButton.addEventListener('click', async (e) => {
161            e.preventDefault();
162            e.stopPropagation();
163
164            saveButton.disabled = true;
165            saveButton.innerHTML = '⏳ Saving...';
166
167            const postData = extractPostData(article);
168            if (postData) {
169                const success = await savePost(postData);
170                if (success) {
171                    saveButton.innerHTML = '✅ Saved!';
172                    saveButton.style.background = '#00ba7c';
173                    setTimeout(() => {
174                        saveButton.innerHTML = '💾 Save';
175                        saveButton.style.background = '#1d9bf0';
176                        saveButton.disabled = false;
177                    }, 2000);
178                } else {
179                    saveButton.innerHTML = '❌ Error';
180                    saveButton.style.background = '#f4212e';
181                    setTimeout(() => {
182                        saveButton.innerHTML = '💾 Save';
183                        saveButton.style.background = '#1d9bf0';
184                        saveButton.disabled = false;
185                    }, 2000);
186                }
187            }
188        });
189
190        // Make article position relative for absolute positioning
191        article.style.position = 'relative';
192        article.appendChild(saveButton);
193    }
194
195    // Create view saved posts button
196    function createViewSavedButton() {
197        if (document.querySelector('.rm-view-saved-btn')) {
198            return;
199        }
200
201        const viewButton = document.createElement('button');
202        viewButton.className = 'rm-view-saved-btn';
203        viewButton.innerHTML = '📚 View Saved Posts';
204        viewButton.style.cssText = `
205            position: fixed;
206            bottom: 20px;
207            right: 20px;
208            background: #1d9bf0;
209            color: white;
210            border: none;
211            border-radius: 30px;
212            padding: 12px 24px;
213            font-size: 15px;
214            font-weight: 700;
215            cursor: pointer;
216            z-index: 9999;
217            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
218            transition: all 0.2s;
219            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
220        `;
221
222        viewButton.addEventListener('mouseenter', () => {
223            viewButton.style.transform = 'scale(1.05)';
224            viewButton.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.4)';
225        });
226
227        viewButton.addEventListener('mouseleave', () => {
228            viewButton.style.transform = 'scale(1)';
229            viewButton.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
230        });
231
232        viewButton.addEventListener('click', () => {
233            showSavedPostsModal();
234        });
235
236        document.body.appendChild(viewButton);
237    }
238
239    // Show saved posts in a modal
240    async function showSavedPostsModal() {
241        const savedPosts = await GM.getValue('saved_posts', []);
242
243        const modal = document.createElement('div');
244        modal.className = 'rm-saved-posts-modal';
245        modal.style.cssText = `
246            position: fixed;
247            top: 0;
248            left: 0;
249            width: 100%;
250            height: 100%;
251            background: rgba(0, 0, 0, 0.7);
252            z-index: 10000;
253            display: flex;
254            align-items: center;
255            justify-content: center;
256            padding: 20px;
257        `;
258
259        const modalContent = document.createElement('div');
260        modalContent.style.cssText = `
261            background: #15202b;
262            color: #ffffff;
263            border-radius: 16px;
264            max-width: 800px;
265            width: 100%;
266            max-height: 90vh;
267            overflow-y: auto;
268            padding: 24px;
269            position: relative;
270        `;
271
272        const closeButton = document.createElement('button');
273        closeButton.innerHTML = '✕';
274        closeButton.style.cssText = `
275            position: absolute;
276            top: 16px;
277            right: 16px;
278            background: transparent;
279            color: #8b98a5;
280            border: none;
281            font-size: 24px;
282            cursor: pointer;
283            padding: 8px;
284            line-height: 1;
285        `;
286        closeButton.addEventListener('click', () => modal.remove());
287
288        const title = document.createElement('h2');
289        title.textContent = `Saved Posts (${savedPosts.length})`;
290        title.style.cssText = `
291            margin: 0 0 20px 0;
292            font-size: 24px;
293            font-weight: 700;
294        `;
295
296        const postsContainer = document.createElement('div');
297
298        if (savedPosts.length === 0) {
299            postsContainer.innerHTML = '<p style="text-align: center; color: #8b98a5; padding: 40px;">No saved posts yet. Click "Save" on any post to save it!</p>';
300        } else {
301            savedPosts.forEach((post, index) => {
302                const postElement = document.createElement('div');
303                postElement.style.cssText = `
304                    border: 1px solid #38444d;
305                    border-radius: 12px;
306                    padding: 16px;
307                    margin-bottom: 16px;
308                    background: #192734;
309                `;
310
311                const postHeader = document.createElement('div');
312                postHeader.style.cssText = `
313                    display: flex;
314                    justify-content: space-between;
315                    align-items: start;
316                    margin-bottom: 12px;
317                `;
318
319                const authorInfo = document.createElement('div');
320                authorInfo.innerHTML = `
321                    <div style="font-weight: 700; font-size: 15px;">${post.author || 'Unknown'}</div>
322                    <div style="color: #8b98a5; font-size: 13px;">${post.username || ''}</div>
323                    <div style="color: #8b98a5; font-size: 12px; margin-top: 4px;">${new Date(post.timestamp).toLocaleString()}</div>
324                `;
325
326                const deleteButton = document.createElement('button');
327                deleteButton.innerHTML = '🗑️';
328                deleteButton.style.cssText = `
329                    background: transparent;
330                    border: none;
331                    color: #f4212e;
332                    cursor: pointer;
333                    font-size: 18px;
334                    padding: 4px 8px;
335                `;
336                deleteButton.addEventListener('click', async () => {
337                    savedPosts.splice(index, 1);
338                    await GM.setValue('saved_posts', savedPosts);
339                    modal.remove();
340                    showSavedPostsModal();
341                });
342
343                postHeader.appendChild(authorInfo);
344                postHeader.appendChild(deleteButton);
345
346                const postContent = document.createElement('div');
347                postContent.textContent = post.content;
348                postContent.style.cssText = `
349                    font-size: 15px;
350                    line-height: 1.5;
351                    margin-bottom: 12px;
352                    white-space: pre-wrap;
353                `;
354
355                const postStats = document.createElement('div');
356                postStats.style.cssText = `
357                    display: flex;
358                    gap: 16px;
359                    color: #8b98a5;
360                    font-size: 13px;
361                `;
362                postStats.innerHTML = `
363                    <span>💬 ${post.stats.replies}</span>
364                    <span>🔄 ${post.stats.reposts}</span>
365                    <span>❤️ ${post.stats.likes}</span>
366                    <span>👁️ ${post.stats.views}</span>
367                `;
368
369                postElement.appendChild(postHeader);
370                postElement.appendChild(postContent);
371                if (post.images && post.images.length > 0) {
372                    const imagesDiv = document.createElement('div');
373                    imagesDiv.style.cssText = 'margin: 12px 0;';
374                    imagesDiv.innerHTML = `<div style="color: #8b98a5; font-size: 13px;">📷 ${post.images.length} image(s) attached</div>`;
375                    postElement.appendChild(imagesDiv);
376                }
377                postElement.appendChild(postStats);
378
379                postsContainer.appendChild(postElement);
380            });
381        }
382
383        modalContent.appendChild(closeButton);
384        modalContent.appendChild(title);
385        modalContent.appendChild(postsContainer);
386        modal.appendChild(modalContent);
387
388        modal.addEventListener('click', (e) => {
389            if (e.target === modal) {
390                modal.remove();
391            }
392        });
393
394        document.body.appendChild(modal);
395    }
396
397    // Process all tweets on the page
398    function processTweets() {
399        const articles = document.querySelectorAll('article[data-testid="tweet"]');
400        articles.forEach(article => {
401            createSaveButton(article);
402        });
403    }
404
405    // Initialize the extension
406    function init() {
407        console.log('Starting X Post Scraper...');
408
409        // Create view saved posts button
410        createViewSavedButton();
411
412        // Process existing tweets
413        processTweets();
414
415        // Watch for new tweets being added
416        const observer = new MutationObserver(debounce(() => {
417            processTweets();
418        }, 500));
419
420        observer.observe(document.body, {
421            childList: true,
422            subtree: true
423        });
424
425        console.log('X Post Scraper ready!');
426    }
427
428    // Wait for page to be ready
429    if (document.readyState === 'loading') {
430        document.addEventListener('DOMContentLoaded', init);
431    } else {
432        init();
433    }
434})();
X Post Scraper & Saver | Robomonkey