X (Twitter) tweet linklerini CSV olarak dışa aktarır. (x_likes_manager formatında)
// ==UserScript==
// @name X Tweet CSV Generator
// @namespace https://x.com/
// @version 1.0.0
// @description X (Twitter) tweet linklerini CSV olarak dışa aktarır. (x_likes_manager formatında)
// @author XLikesManager
// @match https://x.com/*
// @match https://twitter.com/*
// @icon https://abs.twimg.com/favicons/twitter.3.ico
// @license MIT
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const BEARER = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
const state = {
queryId: null,
csrfToken: null,
tweets: []
};
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const sleep = ms => new Promise(r => setTimeout(r, ms));
function getCsrf() {
const m = document.cookie.match(/ct0=([^;]+)/);
return m ? m[1] : null;
}
async function discoverQueryId() {
try {
const links = [...document.querySelectorAll('link[href*="main."], script[src*="main."]')];
let mainUrl = null;
for (const l of links) {
const url = l.href || l.src;
if (url && /main\.[a-f0-9]+\.[a-z]+\.js/i.test(url)) { mainUrl = url; break; }
}
if (!mainUrl) {
const scripts = document.querySelectorAll('script[src]');
for (const s of scripts) {
if (/\/main\.[a-f0-9]+/i.test(s.src)) { mainUrl = s.src; break; }
}
}
if (!mainUrl) return 'V3vfsYzNEyD9tsf4xoFRgw';
const js = await (await fetch(mainUrl)).text();
const m = js.match(/queryId:"([^"]+)",operationName:"TweetResultByRestId"/);
return m ? m[1] : 'V3vfsYzNEyD9tsf4xoFRgw';
} catch (e) {
return 'V3vfsYzNEyD9tsf4xoFRgw';
}
}
async function apiFetch(url, opts = {}) {
const headers = {
'authorization': `Bearer ${BEARER}`,
'x-csrf-token': getCsrf() || '',
'x-twitter-auth-type': 'OAuth2Session',
'x-twitter-active-user': 'yes',
'content-type': 'application/json',
...(opts.headers || {})
};
const resp = await fetch(url, { ...opts, headers, credentials: 'include' });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
}
async function fetchTweetDetail(tweetId) {
const qid = state.queryId;
const variables = { tweetId, withCommunity: false, includePromotedContent: false, withVoice: false };
const features = {
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
creator_subscriptions_tweet_preview_api_enabled: true,
freedom_of_speech_not_reach_fetch_enabled: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true
};
const url = `https://x.com/i/api/graphql/${qid}/TweetResultByRestId?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify(features))}`;
const data = await apiFetch(url);
const result = data?.data?.tweetResult?.result;
if (!result) return null;
let tweet = result.tweet || result;
if (tweet.__typename === 'TweetTombstone') return null;
const legacy = tweet.legacy || {};
const userResults = tweet.core?.user_results?.result || {};
const userLegacy = userResults.legacy || {};
const mediaEntities = legacy.extended_entities?.media || legacy.entities?.media || [];
const mediaUrls = mediaEntities.map(m => {
if (m.video_info?.variants) {
const mp4s = m.video_info.variants.filter(v => v.content_type === 'video/mp4');
if (mp4s.length) return mp4s.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0].url.split('?')[0];
}
return m.media_url_https || m.media_url;
}).filter(Boolean);
return {
id: tweetId,
text: legacy.full_text || '',
username: userLegacy.screen_name || '',
displayName: userLegacy.name || '',
date: legacy.created_at || '',
likes: legacy.favorite_count || 0,
retweets: legacy.retweet_count || 0,
replies: legacy.reply_count || 0,
mediaUrls,
url: `https://x.com/${userLegacy.screen_name || 'i'}/status/${tweetId}`
};
}
function generateCSV(tweets) {
const BOM = '\uFEFF';
const headers = ['Sıra No', 'Tweet ID', 'Tweet Metni', 'Tweet Yazarı', 'Yazar Görünen Adı',
'Tweet Tarihi', 'Beğeni Sayısı', 'RT Sayısı', 'Yanıt Sayısı', 'Medya URL\'leri', 'Tweet URL\'si'];
const escapeCSV = (val) => {
const s = String(val ?? '').replace(/"/g, '""');
return s.includes(';') || s.includes('"') || s.includes('\n') || s.includes('\r') ? `"${s}"` : s;
};
const rows = tweets.map((t, i) => [
i + 1, t.id, escapeCSV(t.text), t.username ? `@${t.username}` : '', escapeCSV(t.displayName),
t.date ? new Date(t.date).toISOString() : '', t.likes, t.retweets, t.replies,
t.mediaUrls.join(', '), t.url
].join(';'));
return BOM + headers.join(';') + '\n' + rows.join('\n');
}
function injectStyles() {
const css = document.createElement('style');
css.textContent = `
#xcsv-fab {
position: fixed; bottom: 80px; right: 20px; width: 48px; height: 48px;
background: #1d9bf0; border-radius: 50%; display: flex; align-items: center; justify-content: center;
cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; border: none; transition: transform 0.2s;
}
#xcsv-fab:hover { transform: scale(1.1); }
#xcsv-fab svg { width: 24px; height: 24px; fill: white; }
#xcsv-panel-wrapper {
position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none;
align-items: center; justify-content: center; z-index: 10000;
}
#xcsv-panel {
width: 500px; background: #16181c; color: #e7e9ea; padding: 24px; border-radius: 16px;
border: 1px solid #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#xcsv-panel textarea {
width: 100%; height: 200px; background: #000; color: #fff; border: 1px solid #333;
border-radius: 12px; padding: 12px; font-family: monospace; resize: none; margin: 12px 0;
}
#xcsv-panel textarea:focus { outline: 1px solid #1d9bf0; }
#xcsv-panel button.primary {
width: 100%; padding: 12px; background: #1d9bf0; color: #fff; border: none;
border-radius: 999px; font-weight: bold; cursor: pointer; font-size: 15px;
}
#xcsv-panel button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
#xcsv-panel .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
#xcsv-panel h3 { margin: 0; font-size: 18px; }
#xcsv-panel .close { background: none; border: none; color: #71767b; cursor: pointer; font-size: 20px; }
`;
document.head.appendChild(css);
}
function createUI() {
injectStyles();
// FAB
const fab = document.createElement('button');
fab.id = 'xcsv-fab';
fab.title = 'X Tweet to CSV Generator';
fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg>`;
document.body.appendChild(fab);
// Wrapper
const wrapper = document.createElement('div');
wrapper.id = 'xcsv-panel-wrapper';
wrapper.innerHTML = `
<div id="xcsv-panel">
<div class="header">
<h3>X Tweet to CSV Generator</h3>
<button class="close">✕</button>
</div>
<p style="font-size: 13px; color: #71767b; line-height: 1.4;">Linkleri alt alta girin. Sadece tweet linkleri işlenecektir.</p>
<textarea id="xcsv-links" placeholder="https://x.com/user/status/123456789..."></textarea>
<div id="xcsv-status" style="font-size: 13px; margin-bottom: 12px; color: #1d9bf0; min-height: 18px;"></div>
<button id="xcsv-generate" class="primary">CSV Oluştur ve İndir</button>
</div>
`;
document.body.appendChild(wrapper);
fab.onclick = () => { wrapper.style.display = 'flex'; };
wrapper.querySelector('.close').onclick = () => { wrapper.style.display = 'none'; };
wrapper.onclick = (e) => { if (e.target === wrapper) wrapper.style.display = 'none'; };
const btn = $('#xcsv-generate');
const status = $('#xcsv-status');
const textarea = $('#xcsv-links');
btn.onclick = async () => {
const lines = textarea.value.split('\n').map(l => l.trim()).filter(Boolean);
const ids = lines.map(l => { const m = l.match(/status\/(\d+)/); return m ? m[1] : null; }).filter(Boolean);
if (ids.length === 0) return alert('Lütfen geçerli tweet linkleri girin.');
btn.disabled = true;
textarea.disabled = true;
state.queryId = await discoverQueryId();
state.tweets = [];
for (let i = 0; i < ids.length; i++) {
status.textContent = `İşleniyor: ${i + 1} / ${ids.length}`;
try {
const tweet = await fetchTweetDetail(ids[i]);
if (tweet) state.tweets.push(tweet);
} catch (e) {}
await sleep(800);
}
if (state.tweets.length > 0) {
const csv = generateCSV(state.tweets);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `x_links_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
status.textContent = `Tamamlandı! ${state.tweets.length} tweet indirildi.`;
} else {
status.textContent = 'Veri çekilemedi. Bağlantınızı kontrol edin.';
}
btn.disabled = false;
textarea.disabled = false;
};
}
// Initialize with delay for X.com
setTimeout(() => {
if (!location.hostname.includes('x.com') && !location.hostname.includes('twitter.com')) return;
createUI();
}, 2000);
})();