1- import puppeteer from "puppeteer " ;
2- import { prompt } from "./utils/terminal " ;
1+ import { program } from "commander " ;
2+ import fs from "fs " ;
33import path from "path" ;
4+ import puppeteer from "puppeteer" ;
45import { loadData } from "./load-data" ;
6+ import { prompt } from "./utils/terminal" ;
57
6- const jsonFileInput = path . resolve ( process . cwd ( ) , process . argv [ 2 ] ) ;
7-
8- //only required to write a log
9- const fs = require ( 'fs' ) ;
10- const log_name = Date . now ( ) + '_log.txt' ;
11-
12- //debug functions
13- var log = false ;
14- var extended_error = true ;
15- var skipTo = 0 ;
16-
17- //basic delays and timeout
18- var delay_amount = 5000 ;
19- var timeout_amount = 5000 ;
20-
21- //index of args
22- var i = 0 ;
23- for ( const arg of process . argv )
24- {
25- //i had some errors so this is here to just catch and print them
26- try {
27- switch ( arg )
28- {
29- case "-debug" : { log = true ; break ; }
30- case "-log" : { extended_error = true ; break ; }
31- case "-exlog" :
32- {
33- log = true ;
34- extended_error = true ;
35- break ;
36- }
37- case "-nolog" : { extended_error = false ; log = false ; break ; }
38- case "-skip" : { skipTo = Number ( process . argv [ i + 1 ] ) ; break ; }
39- case "-delay" : {
40- var arg_delay = Number ( process . argv [ i + 1 ] ) ;
41- if ( arg_delay < 5000 )
42- console . log ( "Lower then 5000ms delay could cause rate limiting!" ) ;
43- delay_amount = arg_delay ;
44- break ;
45- }
46- case "-timeout" : {
47- var arg_timeout = Number ( process . argv [ i + 1 ] ) ;
48- if ( arg_timeout < 5000 )
49- console . log ( "Lower then 5000ms could cause false positives when checking for page elements!" ) ;
50- timeout_amount = arg_timeout ;
51- break ;
52- }
53- }
54- }
55- catch ( error )
56- {
57- console . log ( error ) ;
58- }
59- i ++ ;
60- }
8+ program
9+ . argument ( "<file>" , "the tweets file to load" )
10+ . option ( "-d, --debug" , "writes debug information to log file" )
11+ . option ( "-l, --log" , "writes log information to log file" , false )
12+ . option ( "-e, --exlog" , "writes all information from log and debug to log file" , true )
13+ . option ( "-n, --nolog" , "forces app to run without making a log file, could be helpful if removing large amounts of tweets/retweets/likes" )
14+ . option (
15+ "-s, --skip <number>" ,
16+ "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" ,
17+ "0"
18+ )
19+ . option (
20+ "-t, --timeout <number>" ,
21+ "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" ,
22+ "5000"
23+ )
24+ . option ( "-w, --wait <number>" , "the delay used between actions, try not to use below 5000ms as this could cause rate limiting" , "5000" ) ;
6125
26+ program . parse ( ) ;
27+ const options = program . opts ( ) ;
28+ const log = options . log ;
29+ const extended_error = options . exlog ;
30+ const skipTo = parseInt ( options . skip ) ;
31+ const timeout_amount = parseInt ( options . timeout ) ;
32+ const delay_amount = parseInt ( options . wait ) ;
6233
63-
34+ const jsonFileInput = path . resolve ( process . cwd ( ) , options . file ) ;
35+ const log_name = Date . now ( ) + "_log.txt" ;
6436
37+ ( async ( ) => {
38+ var tweets ;
39+ var isLikes ;
6540
41+ //checks for .js files
42+ try {
43+ tweets = await loadData ( jsonFileInput ) ;
44+ console . log ( `Found ${ tweets . length } tweets` ) ;
45+ isLikes = ! ( typeof tweets [ 0 ] . tweetId === "undefined" ) ;
46+ } catch ( e ) {
47+ console . log ( "No tweet.js, twitter-circle-tweet.js or like.js, Exiting Program" ) ;
48+ process . exit ( 0 ) ;
49+ }
6650
67- ( async ( ) => {
68-
69- var tweets ;
70- var isLikes ;
71-
72- //checks for .js files
73- try {
74- tweets = await loadData ( jsonFileInput ) ;
75- console . log ( `Found ${ tweets . length } tweets` ) ;
76- isLikes = ! ( typeof tweets [ 0 ] . tweetId === 'undefined' ) ;
77- } catch ( e ) {
78- console . log ( "No tweet.js, twitter-circle-tweet.js or like.js, Exiting Program" ) ;
79- process . exit ( 0 ) ;
80- }
81-
82- //browser instance
83- const browser = await puppeteer . launch ( { headless : false } ) ;
84- const page = await browser . newPage ( ) ;
85- await page . goto ( "https://twitter.com/" ) ;
86-
87- //create new log file
88- if ( log || extended_error )
89- fs . writeFileSync ( log_name , 'Process Started' ) ;
90-
91-
92- //wait for interaction
93- await prompt ( "Login and press enter to continue..." ) ;
94-
95- //check if user is logged in by clicking on the profile button
96- try {
97- await page . click ( 'a[data-testid="AppTabBar_Profile_Link"' ) ;
98- page . waitForNavigation ( { timeout : timeout_amount } )
99- } catch ( error ) {
100- //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
101- if ( extended_error )
102- fs . appendFileSync ( log_name , '\n' + error ) ;
103-
104- //print to screen and exit log
105- console . log ( "Not logged in, Exiting Program" ) ;
106- console . log ( error ) ;
107-
108- //close browser and the process
109- await browser . close ( ) ;
110- process . exit ( 0 ) ;
111- }
112-
113- //only require to see where you are in the list
114- var tweet_index = 0 ;
115-
116- for ( const tweet of tweets ) {
117- tweet_index ++ ;
118- if ( tweet_index < skipTo )
119- continue ;
120- if ( isLikes ) {
121- await page . goto ( `https://twitter.com/x/status/${ tweet . tweetId } ` ) ;
122- try {
123- //check for options menu, if it times out we log the error and continue to next instance
124- const options = await page . waitForSelector ( 'article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]' , { visible : true , timeout : timeout_amount } ) ;
125- await delay ( delay_amount ) ;
126-
127- try {
128- //check if its a liked tweet if it is un-like it
129- await page . click ( 'div[data-testid="unlike"]' ) ;
130- await delay ( delay_amount * 2 ) ;
131-
132- //log it
133- console . log ( "unliked, " + tweet_index ) ;
134- if ( log )
135- fs . appendFileSync ( log_name , '\n' + 'un-retweeted: #' + tweet_index + ' ID: ' + tweet . tweetId ) ;
136- }
137- catch ( error )
138- {
139- //log error and continue on
140- console . log ( 'Error: probably already unliked' ) ;
141- if ( log )
142- fs . appendFileSync ( log_name , '\n' + 'Errored: #' + tweet_index + ' ID: ' + tweet . tweetId ) ;
143- if ( extended_error )
144- fs . appendFileSync ( log_name , '\n' + error ) ;
145- console . log ( error ) ;
146- }
147- }
148- catch ( error )
149- {
150- // log error and continue on
151- console . log ( 'Error: tweet unavalible' ) ;
152- if ( log )
153- fs . appendFileSync ( log_name , '\n' + 'Errored: #' + tweet_index + ' ID: ' + tweet . tweetId ) ;
154- if ( extended_error )
155- fs . appendFileSync ( log_name , '\n' + error ) ;
156- console . log ( error ) ;
157- }
158- }
159- else {
160- await page . goto ( `https://twitter.com/x/status/${ tweet . id } ` ) ;
161- try {
162- //check for options menu, if it times out we log the error and continue to next instance
163- const options = await page . waitForSelector ( 'article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]' , { visible : true , timeout : timeout_amount } ) ;
164- await delay ( delay_amount ) ;
165- try {
166- //check if its a retweet if it is un-retweet it
167- await page . click ( 'div[data-testid="unretweet"]' ) ;
168- await delay ( delay_amount ) ;
169-
170- //confirm un-retweet
171- await page . click ( 'div[data-testid="unretweetConfirm"]' ) ;
172- await delay ( delay_amount ) ;
173-
174- //log it
175- console . log ( "Unretweeted, " + tweet_index ) ;
176- if ( log )
177- fs . appendFileSync ( log_name , '\n' + 'un-retweeted: #' + tweet_index + ' ID: ' + tweet . id ) ;
178- await delay ( delay_amount ) ;
179-
180- } catch ( e ) //if its not a retweet continue to tweet delete
181- {
182- // click on the first found selector
183- await options ?. click ( ) ;
184- await delay ( delay_amount ) ;
185-
186- // select delete
187- await page . click ( 'div[data-testid="Dropdown"] > div[role="menuitem"]' ) ;
188- await delay ( delay_amount ) ;
189-
190- // confirm delete
191- await page . click ( 'div[data-testid="confirmationSheetConfirm"]' ) ;
192- await delay ( delay_amount ) ;
193-
194- //log it
195- console . log ( "Deleted, " + tweet_index ) ;
196- if ( log )
197- fs . appendFileSync ( log_name , '\n' + 'Deleted: #' + tweet_index + ' ID: ' + tweet . id ) ;
198- }
199- } catch ( error )
200- {
201- // log error and continue on
202- console . log ( 'Error: probably already deleted' ) ;
203- if ( log )
204- fs . appendFileSync ( log_name , '\n' + 'Errored: #' + tweet_index + ' ID: ' + tweet . id ) ;
205- if ( extended_error )
206- fs . appendFileSync ( log_name , '\n' + error ) ;
207- console . log ( error ) ;
208- }
209- }
210- }
211- // close browser
212- await browser . close ( ) ;
51+ //browser instance
52+ const browser = await puppeteer . launch ( { headless : false } ) ;
53+ const page = await browser . newPage ( ) ;
54+ await page . goto ( "https://twitter.com/" ) ;
55+
56+ //create new log file
57+ if ( log || extended_error ) fs . writeFileSync ( log_name , "Process Started" ) ;
58+
59+ //wait for interaction
60+ await prompt ( "Login and press enter to continue..." ) ;
61+
62+ //check if user is logged in by clicking on the profile button
63+ try {
64+ await page . click ( 'a[data-testid="AppTabBar_Profile_Link"' ) ;
65+ page . waitForNavigation ( { timeout : timeout_amount } ) ;
66+ } catch ( error ) {
67+ //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
68+ if ( extended_error ) fs . appendFileSync ( log_name , "\n" + error ) ;
69+
70+ //print to screen and exit log
71+ console . log ( "Not logged in, Exiting Program" ) ;
72+ console . log ( error ) ;
73+
74+ //close browser and the process
75+ await browser . close ( ) ;
76+ process . exit ( 0 ) ;
77+ }
78+
79+ //only require to see where you are in the list
80+ var tweet_index = 0 ;
81+
82+ for ( const tweet of tweets ) {
83+ tweet_index ++ ;
84+ if ( tweet_index < skipTo ) continue ;
85+ if ( isLikes ) {
86+ await page . goto ( `https://twitter.com/x/status/${ tweet . tweetId } ` ) ;
87+ try {
88+ //check for options menu, if it times out we log the error and continue to next instance
89+ const options = await page . waitForSelector ( 'article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]' , {
90+ visible : true ,
91+ timeout : timeout_amount ,
92+ } ) ;
93+ await delay ( delay_amount ) ;
94+
95+ try {
96+ //check if its a liked tweet if it is un-like it
97+ await page . click ( 'div[data-testid="unlike"]' ) ;
98+ await delay ( delay_amount * 2 ) ;
99+
100+ //log it
101+ console . log ( "unliked, " + tweet_index ) ;
102+ if ( log ) fs . appendFileSync ( log_name , "\n" + "un-retweeted: #" + tweet_index + " ID: " + tweet . tweetId ) ;
103+ } catch ( error ) {
104+ //log error and continue on
105+ console . log ( "Error: probably already unliked" ) ;
106+ if ( log ) fs . appendFileSync ( log_name , "\n" + "Errored: #" + tweet_index + " ID: " + tweet . tweetId ) ;
107+ if ( extended_error ) fs . appendFileSync ( log_name , "\n" + error ) ;
108+ console . log ( error ) ;
109+ }
110+ } catch ( error ) {
111+ // log error and continue on
112+ console . log ( "Error: tweet unavalible" ) ;
113+ if ( log ) fs . appendFileSync ( log_name , "\n" + "Errored: #" + tweet_index + " ID: " + tweet . tweetId ) ;
114+ if ( extended_error ) fs . appendFileSync ( log_name , "\n" + error ) ;
115+ console . log ( error ) ;
116+ }
117+ } else {
118+ await page . goto ( `https://twitter.com/x/status/${ tweet . id } ` ) ;
119+ try {
120+ //check for options menu, if it times out we log the error and continue to next instance
121+ const options = await page . waitForSelector ( 'article[data-testid="tweet"][tabindex="-1"] div[aria-label=More]' , {
122+ visible : true ,
123+ timeout : timeout_amount ,
124+ } ) ;
125+ await delay ( delay_amount ) ;
126+ try {
127+ //check if its a retweet if it is un-retweet it
128+ await page . click ( 'div[data-testid="unretweet"]' ) ;
129+ await delay ( delay_amount ) ;
130+
131+ //confirm un-retweet
132+ await page . click ( 'div[data-testid="unretweetConfirm"]' ) ;
133+ await delay ( delay_amount ) ;
134+
135+ //log it
136+ console . log ( "Unretweeted, " + tweet_index ) ;
137+ if ( log ) fs . appendFileSync ( log_name , "\n" + "un-retweeted: #" + tweet_index + " ID: " + tweet . id ) ;
138+ await delay ( delay_amount ) ;
139+ } catch (
140+ e //if its not a retweet continue to tweet delete
141+ ) {
142+ // click on the first found selector
143+ await options ?. click ( ) ;
144+ await delay ( delay_amount ) ;
145+
146+ // select delete
147+ await page . click ( 'div[data-testid="Dropdown"] > div[role="menuitem"]' ) ;
148+ await delay ( delay_amount ) ;
149+
150+ // confirm delete
151+ await page . click ( 'div[data-testid="confirmationSheetConfirm"]' ) ;
152+ await delay ( delay_amount ) ;
153+
154+ //log it
155+ console . log ( "Deleted, " + tweet_index ) ;
156+ if ( log ) fs . appendFileSync ( log_name , "\n" + "Deleted: #" + tweet_index + " ID: " + tweet . id ) ;
157+ }
158+ } catch ( error ) {
159+ // log error and continue on
160+ console . log ( "Error: probably already deleted" ) ;
161+ if ( log ) fs . appendFileSync ( log_name , "\n" + "Errored: #" + tweet_index + " ID: " + tweet . id ) ;
162+ if ( extended_error ) fs . appendFileSync ( log_name , "\n" + error ) ;
163+ console . log ( error ) ;
164+ }
165+ }
166+ }
167+ // close browser
168+ await browser . close ( ) ;
213169} ) ( ) ;
214170
215171// delay function to help avoid any rate limiting or slow connection issues
216172function delay ( ms : number ) {
217- //only here just to do a quick skip
218- if ( ms == 0 )
219- return ;
220- return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
221- }
173+ //only here just to do a quick skip
174+ if ( ms == 0 ) return ;
175+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
176+ }
0 commit comments