Last active
November 7, 2022 15:54
-
-
Save michaelnordmeyer/debca7f543da74c04eb2cf0f770c39fd to your computer and use it in GitHub Desktop.
Find Tweet IDs in Twitter's Data Export. Has to be placed in the folder where the tweet.js is located.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta title="Find Tweet IDs in Twitter's Data Export"> | |
<meta charset="UTF-8"> | |
<style> | |
body { | |
font-family: sans-serif; | |
line-height: 1.5; | |
} | |
.form, #output { | |
border: 1px solid lightgrey; | |
padding: 0.5em; | |
margin-bottom: 0.5em; | |
} | |
#output { | |
font-size: 0.9em; | |
} | |
.dim { | |
color: darkgrey; | |
} | |
.dim a { | |
color: lightblue; | |
} | |
@media (prefers-color-scheme: dark) { | |
body { | |
background-color: #111111; | |
color: #dddddd; | |
} | |
a { | |
color: #5a5aff; | |
} | |
} | |
</style> | |
<script> | |
function log(item){consle.log(`xx ${item}`);} | |
function init() { | |
let myPromise = new Promise(function(loadArchives, showLoadError) { | |
// "Producing Code" (May take some time) | |
loadArchives(); | |
showLoadError(); | |
}); | |
// "Consuming Code" (Must wait for a fulfilled Promise) | |
myPromise.then( | |
function(value) { /* code if successful */ }, | |
function(error) { /* code if some error */ } | |
); | |
let previousUsernames = []; | |
YTD.screen_name_change.part0.forEach(log); | |
console.log(YTD.screen_name_change.part0); | |
for (screenNameChange of YTD.screen_name_change.part0) { | |
previousUsernames.push(screenNameChange.screenNameChange.screenNameChange.changedFrom); | |
} | |
console.log(previousUsernames); | |
document.getElementById("numberOfTweets").innerHTML = YTD.tweet.part0.length; | |
document.getElementById("username").innerHTML = YTD.account.part0[0].account.username; | |
document.getElementById("previousUsernames").innerHTML = previousUsernames; | |
document.getElementById("showFullText").addEventListener("click", showFullTextMatches); | |
document.getElementById("showSource").addEventListener("click", showSourceMatches); | |
document.getElementById("showMediaByUser").addEventListener("click", showMediaByUserMatches); | |
document.getElementById("showMediaNotByUser").addEventListener("click", showMediaNotByUserMatches); | |
document.getElementById("showLinks").addEventListener("click", showLinkMatches); | |
document.getElementById("showApps").addEventListener("click", showUniqueApps); | |
document.getElementById("copyMatchedIds").addEventListener("click", copyMatches); | |
document.getElementById("copyMatchedIds").disabled = true; | |
} | |
function loadArchives() { | |
window.YTD = { account: { part0: [] }, tweet: { part0: [] }, screen_name_change: { part0: [] } }; | |
for (type of ["account", "screen_name_change", "tweet"]) { | |
let scriptTag = document.createElement("script"); | |
scriptTag.src = `${type.replace(/_/g, '-')}.js`; | |
document.head.appendChild(scriptTag); | |
} | |
} | |
function loadArchive(type) { | |
let scriptTag = document.createElement("script"); | |
scriptTag.src = `data/${type}.js`; | |
document.head.appendChild(scriptTag); | |
/* | |
if (typeof YTD.account.part0 == 'undefined' || typeof YTD.tweet.part0 == 'undefined') { | |
document.getElementById("header").innerHTML = `No tweets found. Twitter's '${type}.js' has to be in the same folder as this HTML file. All tweets will be loaded when the page is opened.`; | |
for (div of document.getElementsByTagName("div")) { | |
div.style.visibility = "hidden"; | |
} | |
return; | |
}*/ | |
} | |
var matches = new Map(); | |
async function showFullTextMatches() { | |
document.body.style.cursor = "wait"; | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
let query = document.getElementById("query").value; | |
function isMatch(tweet) { | |
return tweet.tweet.full_text.toLowerCase().includes(query); | |
} | |
YTD.tweet.part0.filter(isMatch).forEach(saveMatch); | |
document.getElementById("copyMatchedIds").disabled = false; | |
outputMatches(matches); | |
document.body.style.cursor = "default"; | |
} | |
function showSourceMatches() { | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
let query = document.getElementById("query").value; | |
function isMatch(tweet) { | |
return tweet.tweet.source.replace("Tweetbot for iΟS", "Tweetbot for iOS").toLowerCase().includes(query); | |
} | |
YTD.tweet.part0.filter(isMatch).forEach(saveMatch); | |
document.getElementById("copyMatchedIds").disabled = false; | |
outputMatches(matches); | |
} | |
function showMediaByUserMatches() { | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
let query = document.getElementById("query").value; | |
function isMatch(tweet) { | |
if (typeof tweet.tweet.entities.media != 'undefined') { | |
return tweet.tweet.entities.media[0].expanded_url.toLowerCase().includes(query); | |
} | |
} | |
YTD.tweet.part0.filter(isMatch).forEach(saveMatch); | |
document.getElementById("copyMatchedIds").disabled = false; | |
outputMatches(matches); | |
} | |
function showMediaNotByUserMatches() { | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
let query = document.getElementById("query").value; | |
function isMatch(tweet) { | |
if (typeof tweet.tweet.entities.media != 'undefined') { | |
return !tweet.tweet.entities.media[0].expanded_url.toLowerCase().includes(query); | |
} | |
} | |
YTD.tweet.part0.filter(isMatch).forEach(saveMatch); | |
document.getElementById("copyMatchedIds").disabled = false; | |
outputMatches(matches); | |
} | |
function showLinkMatches() { | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
document.getElementById("query").value = ""; | |
function isMatch(tweet) { | |
return /https?:\/\/[^t.co]/i.test(tweet.tweet.full_text.toLowerCase()); | |
} | |
YTD.tweet.part0.filter(isMatch).forEach(saveMatch); | |
document.getElementById("copyMatchedIds").disabled = false; | |
outputMatches(matches); | |
} | |
function showUniqueApps() { | |
matches.clear(); | |
document.getElementById("output").innerHTML = "Matching…"; | |
document.getElementById("query").value = ""; | |
document.getElementById("copyMatchedIds").disabled = true; | |
let uniqueApps = new Map(); | |
function saveMatch(tweet) { | |
let sanitizedSource = tweet.tweet.source.replace("Tweetbot for iΟS", "Tweetbot for iOS"); | |
let uniqueApp = uniqueApps.get(sanitizedSource); | |
if (typeof uniqueApp != 'undefined') { | |
uniqueApps.set(sanitizedSource, { source: sanitizedSource, count: uniqueApp.count + 1}); | |
} else { | |
uniqueApps.set(sanitizedSource, { source: sanitizedSource, count: 1}); | |
} | |
} | |
YTD.tweet.part0.forEach(saveMatch); | |
let matchesHtml = "<div>" + uniqueApps.size + " found</div><hr>"; | |
let sortedUniqueApps = []; | |
for (const uniqueApp of uniqueApps.values()) { | |
sortedUniqueApps.push(uniqueApp); | |
} | |
sortedUniqueApps.sort((a, b) => { return b.count - a.count; }); | |
for (const uniqueApp of sortedUniqueApps) { | |
matchesHtml = matchesHtml.concat(`<div>${uniqueApp.source} (${uniqueApp.count})</div>`); | |
} | |
document.getElementById("output").innerHTML = matchesHtml; | |
} | |
function saveMatch(tweet) { | |
matches.set(tweet.tweet.id, | |
{ link: `https://twitter.com/${YTD.account.part0[0].account.username}/status/${tweet.tweet.id}`, date: formatDate(new Date(tweet.tweet.created_at)), source: tweet.tweet.source.replace("Tweetbot for iΟS", "Tweetbot for iOS"), content: tweet.tweet.full_text } | |
); | |
} | |
function formatDate(date) { | |
return date.getFullYear() + "-" + | |
addLeadingZero(date.getMonth() + 1) + "-" + | |
addLeadingZero(date.getDate()) + " " + | |
addLeadingZero(date.getHours()) + ":" + | |
addLeadingZero(date.getMinutes()) + ":" + | |
addLeadingZero(date.getSeconds()); | |
} | |
function addLeadingZero(number) { | |
return number > 9 ? number : "0" + number; | |
} | |
function outputMatches(matches) { | |
const preparedMatches = prepareMatches(); | |
let matchesHtml = `<div>${preparedMatches.length} found</div><hr>`; | |
function createMatchesHtml(match) { | |
matchesHtml = matchesHtml.concat(match); | |
} | |
preparedMatches.forEach(createMatchesHtml); | |
document.getElementById("output").innerHTML = matchesHtml; | |
} | |
function prepareMatches() { | |
let preparedMatches = []; | |
for (const match of matches.entries()) { | |
preparedMatches.push(`<div data-time="${match[1].date}" data-id="${match[0]}"><span class="dim"><a href="${match[1].link}">${match[1].date}</a></span> ${match[1].content} <span class="dim">(posted by ${match[1].source})</span></div>`); | |
} | |
return preparedMatches.sort(); | |
} | |
function copyMatches() { | |
var tweetIds = ''; | |
for (const tweetId of matches.keys()) { | |
tweetIds = tweetIds.concat(tweetId + "\n"); | |
} | |
copyToClipboard(tweetIds, matches.size, YTD.tweet.part0.length); | |
} | |
function copyToClipboard(payload, numberOfMatches, numberOfTweets) { | |
navigator.clipboard.writeText(payload).then(() => { | |
alert(`Copied ${numberOfMatches} matched tweet IDs out of ${numberOfTweets} to clipboard`); | |
}, () => { | |
alert("Copying to clipboard not allowed"); | |
}); | |
} | |
</script> | |
</head> | |
<body onload="loadArchives(); init();"> | |
<h1>Find Tweet IDs in Twitter's Data Export</h1> | |
<h2>Infos</h2> | |
<ul> | |
<li>Twitter has used its own URL shortener <a href="https://en.wikipedia.org/wiki/Twitter#URL_shortener">since June 2011</a>. Since then all links posted to Twitter use a t.co wrapper. Although my first t.co link is from October 2010, which contradicts this. I have no idea why.</li> | |
<li>Twitter changed the tweet limit to 280 characters officially <a href="https://en.wikipedia.org/wiki/Twitter#Character_limits">in November 2017</a>, but inofficially in late September 2017.</li> | |
</ul> | |
<p>To mass-delete tweets, <a href="https://twitter.com/mnordmeyer">I</a> personally use <a href="https://tweetdelete.net/">TweetDelete</a>, where I paste the copied tweet IDs. You can easily use the official Twitter API for that, but I already payed for TweetDelete, so I didn't investigate it to use it myself.</p> | |
<p>All matching is case-insensitive.</p> | |
<p id="header">Found <span id="numberOfTweets">0</span> tweets for user <span id="username">not loaded</span> having previous usernames <span id="previousUsernames">not loaded</span> in export file. <button id="showApps">Show unique apps</button> | |
</p> | |
<div class="form"> | |
<input type="search" id="query" name="query" placeholder="Search" autocomplete="off" autocorrect="off" autocapitalize="off" autofocus> | |
<button id="showFullText">Show full text matches</button> | |
<button id="showSource">Show source matches</button> | |
<button id="showMediaByUser">Show media by user matches</button> | |
<button id="showMediaNotByUser">Show media not by user matches</button> | |
</div> | |
<div class="form"> | |
<button id="showLinks">Show links w/o t.co</button> | |
<button id="copyMatchedIds">Copy matched IDs to clipboard</button> | |
</div> | |
<div id="output">No results</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment