Skip to content

Instantly share code, notes, and snippets.

@michaelnordmeyer
Last active November 7, 2022 15:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaelnordmeyer/debca7f543da74c04eb2cf0f770c39fd to your computer and use it in GitHub Desktop.
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.
<!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