Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic error handling and inclusion of retweet removal with logs #5

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ node_modules/
.vscode/

dist/
data/
*_log.txt
test.sh
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
# Delete Your Old Tweets
# Delete Your Old Tweets, Retweets And Likes

1. this application will require [NodeJS](https://nodejs.org/en/download/current) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/) to run
1. [Download your Twitter archive](https://twitter.com/settings/download_your_data), which contains all your tweets.
1. Locate the JSON contains your tweets, which is `data/tweet.js` (or `data/twitter-circle-tweet.js`) in the archive.
2. Locate the JSON contains your tweets, which is `data/tweet.js` (`data/like.js` or `data/twitter-circle-tweet.js`) in the archive.

3. Choose one of the following

## To Delete tweets
1. Run this project with `yarn && yarn start data/tweet.js`.
1. It will wait you for login
1. After login, it will delete all your tweets.
2. It will wait you for login
3. After login, it will delete all your tweets.

## To Delete Twitter circle tweets
1. Run this project with `yarn && yarn start data/twitter-circle-tweet.js`.
2. It will wait you for login
3. After login, it will delete all your circle tweets.


## To Unlike previously Liked tweets
1. Run this project with `yarn && yarn start data/like.js`.
2. It will wait you for login
3. After login, it will unlike your likes.


## Commandline Arguments

| argumensts | |
| ------------- |:-------------:|
| -d, --debug | writes debug information to log file |
| -l, --log | writes log information to log file |
| -e, --exlog | writes all information from log and debug to log file |
| -n, --nolog | forces app to run without making a log file, could be helpful if removing large amounts of tweets/retweets/likes |
| -s, --skip <number> | skips up to the index given, good if you had to close the app or it crashed and don't have time to rerun the entire file |
| -w, --wait <number> | the delay used between actions, try not to use below 5000ms as this could cause rate limiting |
| -t, --timeout <number> | the timeout amount used after tweet is loaded (helpful on low bandwidth connections), try not to use below 5000ms as this could cause rate limiting |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^11.1.0",
"puppeteer": "^20.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
Expand Down
205 changes: 193 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,209 @@
import puppeteer from "puppeteer";
import { prompt } from "./utils/terminal";
import { program } from "commander";
import fs from "fs";
import path from "path";
import puppeteer from "puppeteer";
import { loadData } from "./load-data";
import { prompt } from "./utils/terminal";


var fileValue;


const jsonFileInput = path.resolve(process.cwd(), process.argv[2]);
program
.arguments("<file>")
//this is here because for some reason it doesn't parse the file properly?
.action(function(file) {
fileValue = file;
})

.option("-d, --debug",
"writes debug information to log file"
)

Comment on lines +15 to +21
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The better option in my opinion is to chain all the .option first, and the .action will be the last, with all the rest of the code inside it, so you don't need to decalre a global var fileValue.

.option("-l, --log",
"writes log information to log file",
false
)

.option("-e, --exlog",
"writes all information from log and debug to log file",
true
)

.option("-n, --nolog",
"forces app to run without making a log file, could be helpful if removing large amounts of tweets/retweets/likes"
)

.option(
"-s, --skip <number>",
"skips up to the index given, good if you had to close the app or it crashed and don't have time to rerun the entire file",
"0"
)

.option(
"-t, --timeout <number>",
"the timeout amount used after tweet is loaded (helpful on low bandwidth connections), try not to use below 5000ms as this could cause rate limiting",
"5000"
)

.option("-w, --wait <number>",
"the delay used between actions, try not to use below 5000ms as this could cause rate limiting",
"5000");

program.parse();

const options = program.opts();
const log = options.log;
const extended_error = options.exlog;
const skipTo = parseInt(options.skip);
const timeout_amount = parseInt(options.timeout);
const delay_amount = parseInt(options.wait);
const jsonFileInput = path.resolve(process.cwd(), fileValue /*options.file*/);//again not sure if broken or just something wrong with my install
const log_name = Date.now() + "_log.txt";

(async () => {
const tweets = await loadData(jsonFileInput);
console.log(`Found ${tweets.length} tweets`);


//create new log file
if (log || extended_error){
fs.writeFileSync(log_name, "Process Started");
fs.appendFileSync(log_name, "\n" + process.argv);
}


var tweets;
var isLikes;

//checks for .js files
try {
tweets = await loadData(jsonFileInput);
console.log(`Found ${tweets.length} tweets`);
isLikes = !(typeof tweets[0].tweetId === "undefined");
} catch (e) {
console.log("No tweet.js, twitter-circle-tweet.js or like.js, Exiting Program");
process.exit(0);
}

//browser instance
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://twitter.com/");

//wait for interaction
await prompt("Login and press enter to continue...");

for (const tweet of tweets) {
await page.goto(`https://twitter.com/baruchiro/status/${tweet.id}`);
const options = await page.waitForSelector('article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]', { visible: true });
// click on the first found selector
await options?.click();
await page.click('div[data-testid="Dropdown"] > div[role="menuitem"]');
await page.click('div[data-testid="confirmationSheetConfirm"]');
//check if user is logged in by clicking on the profile button
try {
await page.click('a[data-testid="AppTabBar_Profile_Link"');
page.waitForNavigation({ timeout: timeout_amount });
} catch (error) {
//might not always be a not logged in issue but in the case it's not log the error to see if we can fix it
if (extended_error) fs.appendFileSync(log_name, "\n" + error);

//print to screen and exit log
console.log("Not logged in, Exiting Program");
console.log(error);

//close browser and the process
await browser.close();
process.exit(0);
}

//only require to see where you are in the list
var tweet_index = 0;

for (const tweet of tweets) {
tweet_index++;
if (tweet_index < skipTo) continue;
if (isLikes) {
await page.goto(`https://twitter.com/x/status/${tweet.tweetId}`);
try {
//check for options menu, if it times out we log the error and continue to next instance
const options = await page.waitForSelector('article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]', {
visible: true,
timeout: timeout_amount,
});
await delay(delay_amount);
Comment on lines +122 to +125
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The options seems to be unused.


try {
//check if its a liked tweet if it is un-like it
await page.click('div[data-testid="unlike"]');
await delay(delay_amount * 2);

//log it
console.log("unliked, " + tweet_index);
if (log) fs.appendFileSync(log_name, "\n" + "un-retweeted: #" + tweet_index + " ID: " + tweet.tweetId);
} catch (error) {
//log error and continue on
console.log("Error: probably already unliked");
if (log) fs.appendFileSync(log_name, "\n" + "Errored: #" + tweet_index + " ID: " + tweet.tweetId);
if (extended_error) fs.appendFileSync(log_name, "\n" + error);
console.log(error);
}
} catch (error) {
// log error and continue on
console.log("Error: tweet unavalible");
if (log) fs.appendFileSync(log_name, "\n" + "Errored: #" + tweet_index + " ID: " + tweet.tweetId);
if (extended_error) fs.appendFileSync(log_name, "\n" + error);
console.log(error);
}
} else {
await page.goto(`https://twitter.com/x/status/${tweet.id}`);
try {
//check for options menu, if it times out we log the error and continue to next instance
const options = await page.waitForSelector('article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]', {
visible: true,
timeout: timeout_amount,
});
await delay(delay_amount);
try {
//check if its a retweet if it is un-retweet it
await page.click('div[data-testid="unretweet"]');
await delay(delay_amount);

//confirm un-retweet
await page.click('div[data-testid="unretweetConfirm"]');
await delay(delay_amount);

//log it
console.log("Unretweeted, " + tweet_index);
if (log) fs.appendFileSync(log_name, "\n" + "un-retweeted: #" + tweet_index + " ID: " + tweet.id);
await delay(delay_amount);
} catch (
e //if its not a retweet continue to tweet delete
) {
// click on the first found selector
await options?.click();
await delay(delay_amount);

// select delete
await page.click('div[data-testid="Dropdown"] > div[role="menuitem"]');
await delay(delay_amount);

// confirm delete
await page.click('div[data-testid="confirmationSheetConfirm"]');
await delay(delay_amount);

//log it
console.log("Deleted, " + tweet_index);
if (log) fs.appendFileSync(log_name, "\n" + "Deleted: #" + tweet_index + " ID: " + tweet.id);
}
} catch (error) {
// log error and continue on
console.log("Error: probably already deleted");
if (log) fs.appendFileSync(log_name, "\n" + "Errored: #" + tweet_index + " ID: " + tweet.id);
if (extended_error) fs.appendFileSync(log_name, "\n" + error);
console.log(error);
}
}
}
// close browser
await browser.close();
})();

// delay function to help avoid any rate limiting or slow connection issues
function delay(ms: number) {
//only here just to do a quick skip
if (ms == 0) return;
return new Promise((resolve) => setTimeout(resolve, ms));
}
31 changes: 31 additions & 0 deletions src/load-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare global {
YTD: {
twitter_circle_tweet: TweetsCollection;
tweets: TweetsCollection;
like: LikesCollection;
};
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand All @@ -16,11 +17,21 @@ type TweetsCollection = {
}[];
};

type LikesCollection = {
[key: string]: {
like: {
tweetId: string;
expandedUrl: string;
};
}[];
};

// @ts-expect-error - We don't care about the window object
global.window = {
YTD: {
twitter_circle_tweet: {},
tweets: {},
like: {},
},
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
} as Window;

Expand All @@ -35,6 +46,10 @@ export const loadData = async (file: string) => {
console.log("Found tweets");
return extractTweets(global.window.YTD.tweets);
}
if (Object.keys(global.window.YTD.like).length) {
console.log("Found likes");
return extractLikes(global.window.YTD.like);
}
console.log("No tweets found");
throw new Error("No tweets found");
};
Expand All @@ -47,3 +62,19 @@ const extractTweets = (tweets: TweetsCollection) => {
})),
);
};
const extractLikes = (like: LikesCollection) => {
return Object.values(like).flatMap((arr) =>
arr.map((obj) => ({
tweetId: obj.like.tweetId,
expandedUrl: obj.like.expandedUrl,
})),
);
};
const extractLikes = (like: LikesCollection) => {
return Object.values(like).flatMap((arr) =>
arr.map((obj) => ({
tweetId: obj.like.tweetId,
expandedUrl: obj.like.expandedUrl,
})),
);
};
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ colorette@^2.0.20:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==

commander@11.1.0:
commander@11.1.0, commander@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
Expand Down
Loading