From 3cb8c544979b1fd4f6824ad12d2354d38191f72a Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Mon, 26 May 2025 11:42:57 +0200 Subject: [PATCH 01/11] Decrease scroll by to 1000 --- src/UI/Services/XScriptService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/Services/XScriptService.cs b/src/UI/Services/XScriptService.cs index 1162458..3013fa0 100644 --- a/src/UI/Services/XScriptService.cs +++ b/src/UI/Services/XScriptService.cs @@ -374,7 +374,7 @@ function confirmDelete(attempt = 0) {{ const confirmBtn = document.querySelector(""button[data-testid='confirmationSheetConfirm']""); if (confirmBtn && confirmBtn.offsetParent !== null) {{ confirmBtn.click(); - window.scrollBy(0, 3000); + window.scrollBy(0, 1000); }} else {{ confirmDelete(attempt + 1); }} @@ -415,7 +415,7 @@ async function unlike() {{ }} console.log('Unlike successful! Scrolling down...'); - window.scrollBy(0, 3000); + window.scrollBy(0, 1000); return true; }} @@ -468,7 +468,7 @@ function tryClickConfirm(attempt = 0) {{ const confirmBtn = document.querySelector('button[data-testid=""confirmationSheetConfirm""]'); if (confirmBtn && confirmBtn.offsetParent !== null) {{ confirmBtn.click(); - window.scrollBy(0, 3000); + window.scrollBy(0, 1000); }} else {{ tryClickConfirm(attempt + 1); }} From dfb75b7c336e1a735a7a7ba66fb3cc5d23372a08 Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Mon, 26 May 2025 11:58:48 +0200 Subject: [PATCH 02/11] Delete all messages until emtpy div is present --- src/UI/Services/XScriptService.cs | 58 +++++++------------------------ 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/UI/Services/XScriptService.cs b/src/UI/Services/XScriptService.cs index cd76f45..37d5d39 100644 --- a/src/UI/Services/XScriptService.cs +++ b/src/UI/Services/XScriptService.cs @@ -63,7 +63,7 @@ public async Task DeletePostsAsync() var postNumber = 1; var deletedItems = 0; var retryCount = 0; - while (await PostsExistAsync() || retryCount < 3) + while (!await IsEmptyStatePresentAsync() && retryCount < 3) { var countBefore = await GetPostsCountAsync(); @@ -149,7 +149,7 @@ public async Task DeleteLikesAsync() var deletedItems = 0; var retryCount = 0; - while (await LikesExistAsync() || retryCount < 3) + while (!await IsEmptyStatePresentAsync() && retryCount < 3) { var countBefore = await GetLikesCountAsync(); @@ -235,7 +235,7 @@ public async Task DeleteFollowingAsync() var postNumber = 1; var deletedItems = 0; var retryCount = 0; - while (await FollowingExistAsync() || retryCount < 3) + while (!await IsEmptyStatePresentAsync() && retryCount < 3) { var countBefore = await GetFollowingCountAsync(); @@ -273,53 +273,19 @@ public async Task DeleteFollowingAsync() return deletedItems; } - private async Task PostsExistAsync() + private async Task IsEmptyStatePresentAsync() { - const string js = "document.querySelector('div[data-testid=\"primaryColumn\"] section button[data-testid=\"caret\"]') !== null"; - var timeout = _userSettingsService.GetTimeoutSettings(); - var waitAfterDocumentLoad = timeout.WaitAfterDocumentLoad; - for (var i = 0; i < 5; i++) - { - await Task.Delay(waitAfterDocumentLoad); - if (await _webViewHostService.ExecuteScriptAsync(js) == "true") - { - return true; - } - } - return false; - } - - private async Task FollowingExistAsync() - { - const string js = "document.querySelector('button[data-testid$=\"unfollow\"]') !== null"; - var timeout = _userSettingsService.GetTimeoutSettings(); - var waitAfterDocumentLoad = timeout.WaitAfterDocumentLoad; - for (var i = 0; i < 5; i++) - { - await Task.Delay(waitAfterDocumentLoad); - if (await _webViewHostService.ExecuteScriptAsync(js) == "true") - { - return true; - } - } - return false; - } + var script = @" + (function() { + return document.querySelector('[data-testid=""emptyState""]') !== null; + })();"; - private async Task LikesExistAsync() - { - const string js = "document.querySelector('button[data-testid=\"unlike\"]') !== null"; - var timeout = _userSettingsService.GetTimeoutSettings(); - var waitAfterDocumentLoad = timeout.WaitAfterDocumentLoad; - await Task.Delay(500); - for (var i = 0; i < 5; i++) + var result = await _webViewHostService.ExecuteScriptAsync(script) == "true"; + if (result) { - await Task.Delay(waitAfterDocumentLoad); - if (await _webViewHostService.ExecuteScriptAsync(js) == "true") - { - return true; - } + _logger.LogInformation("Empty state present, nothing more to delete."); } - return false; + return result; } private async Task DeleteSinglePostAsync() From 89430948a82f76e9b8bceeb3181b8abf2530f905 Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:15:19 +0200 Subject: [PATCH 03/11] Read scripts from file --- src/Core/Contracts/Services/IFileService.cs | 1 + src/Core/Services/FileService.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/Core/Contracts/Services/IFileService.cs b/src/Core/Contracts/Services/IFileService.cs index 3835410..3cc3352 100644 --- a/src/Core/Contracts/Services/IFileService.cs +++ b/src/Core/Contracts/Services/IFileService.cs @@ -7,4 +7,5 @@ public interface IFileService void Save(string folderPath, string fileName, T content); void Delete(string folderPath, string fileName); + string ReadFile(string filePath); } diff --git a/src/Core/Services/FileService.cs b/src/Core/Services/FileService.cs index e970c5f..6c46cfa 100644 --- a/src/Core/Services/FileService.cs +++ b/src/Core/Services/FileService.cs @@ -17,6 +17,11 @@ public T Read(string folderPath, string fileName) return default; } + public string ReadFile(string filePath) + { + return File.ReadAllText(filePath); + } + public void Save(string folderPath, string fileName, T content) { if (!Directory.Exists(folderPath)) From 0b6414bca5c97c1138c289ad25feee4f9e5e028a Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:16:39 +0200 Subject: [PATCH 04/11] Use html for log page --- src/UI/Views/LogPage.xaml | 88 +++++++------------ src/UI/Views/LogPage.xaml.cs | 158 ++++++++++++++++++++++++----------- 2 files changed, 138 insertions(+), 108 deletions(-) diff --git a/src/UI/Views/LogPage.xaml b/src/UI/Views/LogPage.xaml index 2ff9cf6..23b78ef 100644 --- a/src/UI/Views/LogPage.xaml +++ b/src/UI/Views/LogPage.xaml @@ -6,6 +6,7 @@ xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:viewmodels="clr-namespace:CleanMyPosts.UI.ViewModels" + xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" Style="{DynamicResource MahApps.Styles.Page}"> @@ -15,67 +16,38 @@ - + + + + + - - - - + - + - - - + + + + + + + + + + - - - - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Date: Tue, 27 May 2025 18:24:20 +0200 Subject: [PATCH 07/11] Add tests --- src/Tests/Services/XScriptServiceTests.cs | 218 ++++++++++++++-------- 1 file changed, 142 insertions(+), 76 deletions(-) diff --git a/src/Tests/Services/XScriptServiceTests.cs b/src/Tests/Services/XScriptServiceTests.cs index 6036117..15b2e88 100644 --- a/src/Tests/Services/XScriptServiceTests.cs +++ b/src/Tests/Services/XScriptServiceTests.cs @@ -6,94 +6,160 @@ using Moq; using Xunit; -namespace CleanMyPosts.UI.Tests.Services +namespace CleanMyPosts.UI.Tests.Services; + +public class XScriptServiceTests { - public class XScriptServiceTests - { - private readonly Mock> _loggerMock = new(); - private readonly Mock _webViewHostServiceMock = new(); - private readonly Mock _userSettingsServiceMock = new(); - private readonly Mock _fileServiceMock = new(); - private readonly XScriptService _service; + private readonly Mock> _loggerMock = new(); + private readonly Mock _webViewHostServiceMock = new(); + private readonly Mock _userSettingsServiceMock = new(); + private readonly Mock _fileServiceMock = new(); + private readonly XScriptService _service; - public XScriptServiceTests() - { - _userSettingsServiceMock.Setup(x => x.GetTimeoutSettings()).Returns(new TimeoutSettings - { - WaitAfterDocumentLoad = 10, - WaitAfterDelete = 1, - WaitBetweenRetryDeleteAttempts = 0 - }); - - _webViewHostServiceMock.SetupProperty(x => x.Source); - _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync("document.readyState")) - .ReturnsAsync("\"complete\""); - - // Setup ExecuteScriptAsync for username retrieval (default) - _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("AppTabBar_Profile_Link")))) - .ReturnsAsync("\"testuser\""); - - // Setup dummy for Reload (do nothing) - _webViewHostServiceMock.Setup(x => x.Reload()); - - _fileServiceMock.Setup(x => x.Read(It.IsAny(), It.IsAny())) - .Returns("// dummy js script"); - - _service = new XScriptService( - _loggerMock.Object, - _webViewHostServiceMock.Object, - _userSettingsServiceMock.Object, - _fileServiceMock.Object - ); - - // Pre-set _userName to "testuser" to avoid fetching it from JS during most tests - typeof(XScriptService).GetField("_userName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(_service, "testuser"); - } - - [Fact] - public async Task ShowRepostsAsync_NavigatesToCorrectUrl() + public XScriptServiceTests() + { + _userSettingsServiceMock.Setup(x => x.GetTimeoutSettings()).Returns(new TimeoutSettings { - await _service.ShowRepostsAsync(); + WaitAfterDocumentLoad = 10, + WaitAfterDelete = 1, + WaitBetweenRetryDeleteAttempts = 0 + }); + + _webViewHostServiceMock.SetupProperty(x => x.Source); + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync("document.readyState")) + .ReturnsAsync("\"complete\""); + + // Setup ExecuteScriptAsync for username retrieval (default) + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("AppTabBar_Profile_Link")))) + .ReturnsAsync("\"testuser\""); + + // Setup dummy for Reload (do nothing) + _webViewHostServiceMock.Setup(x => x.Reload()); + + _fileServiceMock.Setup(x => x.Read(It.IsAny(), It.IsAny())) + .Returns("// dummy js script"); + + _service = new XScriptService( + _loggerMock.Object, + _webViewHostServiceMock.Object, + _userSettingsServiceMock.Object, + _fileServiceMock.Object + ); + + // Pre-set _userName to "testuser" to avoid fetching it from JS during most tests + typeof(XScriptService).GetField("_userName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(_service, "testuser"); + } - Assert.NotNull(_webViewHostServiceMock.Object.Source); - Assert.Contains("https://x.com/testuser", _webViewHostServiceMock.Object.Source.ToString()); - } + [Fact] + public async Task ShowRepostsAsync_NavigatesToCorrectUrl() + { + await _service.ShowRepostsAsync(); - [Fact] - public async Task ShowRepliesAsync_NavigatesToCorrectUrl() - { - await _service.ShowRepliesAsync(); + Assert.NotNull(_webViewHostServiceMock.Object.Source); + Assert.Contains("https://x.com/testuser", _webViewHostServiceMock.Object.Source.ToString()); + } - Assert.NotNull(_webViewHostServiceMock.Object.Source); - Assert.Contains("https://x.com/testuser/with_replies", _webViewHostServiceMock.Object.Source.ToString()); - } + [Fact] + public async Task ShowRepliesAsync_NavigatesToCorrectUrl() + { + await _service.ShowRepliesAsync(); - [Fact] - public async Task GetUserNameAsync_ReturnsCleanUserName() - { - var fakeJsResult = "\"\\\"testuser\\\"\""; // double quoted JSON string - _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("AppTabBar_Profile_Link")))) - .ReturnsAsync(fakeJsResult); + Assert.NotNull(_webViewHostServiceMock.Object.Source); + Assert.Contains("https://x.com/testuser/with_replies", _webViewHostServiceMock.Object.Source.ToString()); + } - var username = await _service.GetUserNameAsync(); + [Fact] + public async Task GetUserNameAsync_ReturnsCleanUserName() + { + var fakeJsResult = "\"\\\"testuser\\\"\""; // double quoted JSON string + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("AppTabBar_Profile_Link")))) + .ReturnsAsync(fakeJsResult); - Assert.Equal("testuser", username); - } + var username = await _service.GetUserNameAsync(); + + Assert.Equal("testuser", username); + } + + [Fact] + public async Task NavigateAsync_SetsSourceWhenUrlIsDifferent() + { + var url = new Uri("https://x.com/differentuser"); + _webViewHostServiceMock.Object.Source = new Uri("https://x.com/testuser"); + + var navigateMethod = _service.GetType() + .GetMethod("NavigateAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var result = await (Task)navigateMethod.Invoke(_service, new object[] { url }); + + Assert.Equal(url, _webViewHostServiceMock.Object.Source); + Assert.True(result); + } + + [Fact] + public async Task ShowLikesAsync_NavigatesToLikesUrl() + { + await _service.ShowLikesAsync(); + + var source = _webViewHostServiceMock.Object.Source?.ToString(); + Assert.NotNull(source); + Assert.Contains("https://x.com/testuser/likes", source); + } + + [Fact] + public async Task ShowPostsAsync_NavigatesToCorrectSearchUrl() + { + await _service.ShowPostsAsync(); + + var source = _webViewHostServiceMock.Object.Source?.ToString(); + Assert.NotNull(source); + Assert.Contains("https://x.com/search?q=from%3Atestuser", source); + } - [Fact] - public async Task NavigateAsync_SetsSourceWhenUrlIsDifferent() - { - var url = new Uri("https://x.com/differentuser"); - _webViewHostServiceMock.Object.Source = new Uri("https://x.com/testuser"); - var navigateMethod = _service.GetType() - .GetMethod("NavigateAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var result = await (Task)navigateMethod.Invoke(_service, new object[] { url }); + [Fact] + public async Task EnsureUserNameAsync_UpdatesUserNameIfChanged() + { + typeof(XScriptService).GetField("_userName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue(_service, "olduser"); + + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("AppTabBar_Profile_Link")))) + .ReturnsAsync("\"newuser\""); + + var method = _service.GetType().GetMethod("EnsureUserNameAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + await (Task)method.Invoke(_service, null); - Assert.Equal(url, _webViewHostServiceMock.Object.Source); - Assert.True(result); - } + var updatedUserName = typeof(XScriptService).GetField("_userName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(_service); + + Assert.Equal("newuser", updatedUserName); } + + + [Fact] + public async Task IsEmptyMessagePresentAsync_ReturnsTrueWhenEmptyStateExists() + { + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("emptyState")))) + .ReturnsAsync("true"); + + var method = _service.GetType().GetMethod("IsEmptyMessagePresentAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var result = await (Task)method.Invoke(_service, null); + + Assert.True(result); + } + + + [Fact] + public async Task IsAnArticlePresentAsync_ReturnsFalseWhenNoArticle() + { + _webViewHostServiceMock.Setup(x => x.ExecuteScriptAsync(It.Is(s => s.Contains("article")))) + .ReturnsAsync("false"); + + var method = _service.GetType().GetMethod("IsAnArticlePresentAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var result = await (Task)method.Invoke(_service, null); + + Assert.False(result); + } + } From b191cbf308ca70552978062ed8a764149da6fb0e Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:34:01 +0200 Subject: [PATCH 08/11] Update docu --- README.md | 28 ++++++++++++++++++++----- src/UI/Properties/Resources.Designer.cs | 2 +- src/UI/Properties/Resources.resx | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2731104..6d3ec65 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,14 @@ [![CI Tests](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml/badge.svg)](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml) [![Star this repo](https://img.shields.io/github/stars/thorstenalpers/CleanMyPosts.svg?style=social&label=Star&maxAge=60)](https://github.com/thorstenalpers/CleanMyPosts) -**CleanMyPosts** is a lightweight Windows desktop app that securely deletes all tweets, likes, and followings from your X (formerly Twitter) account in bulk using browser automation. +**CleanMyPosts** is a lightweight Windows desktop app that securely deletes all posts, reposts, replies, likes, and followings from your X (formerly Twitter) account in bulk using browser automation. ## 🚀 Features -- Bulk delete all tweets from your X (Twitter) account +- Bulk delete all posts +- Bulk delete all reposts +- Bulk delete all replies - Remove all likes with a single click - Unfollow all accounts in one go - Secure browser automation — no credentials stored @@ -48,15 +50,27 @@ Please keep in mind: Here’s a quick look at how CleanMyPosts works:
- Clean posts + Delete posts
- Clean Tweets GIF + Delete posts GIF +
+ +
+ Delete reposts +
+ Delete reposts GIF +
+ +
+ Delete replies +
+ Delete replies GIF
Clean likes
- Clean Likes GIF + Delete Likes GIF
@@ -76,6 +90,10 @@ Click on your profile to find your username, then use these links (replace USERN * **Delete posts:** https://x.com/search?q=from%3AUSERNAME → click ... on each post → Delete. +* **Delete reposts:** https://x.com/USERNAME → click ... on each repost icon → Delete. + +* **Delete replies:** https://x.com/USERNAME/with_replies → click ... on each post → Delete. + * **Unlike posts:** https://x.com/USERNAME/likes → click the heart to remove the like. * **Unfollow accounts:** https://x.com/USERNAME/following → click unfollow. diff --git a/src/UI/Properties/Resources.Designer.cs b/src/UI/Properties/Resources.Designer.cs index d769c67..90c6332 100644 --- a/src/UI/Properties/Resources.Designer.cs +++ b/src/UI/Properties/Resources.Designer.cs @@ -79,7 +79,7 @@ public static string LogPage { } /// - /// Looks up a localized string similar to CleanMyPosts is a lightweight Windows desktop app that securely deletes all tweets, likes, and followings from your X (formerly Twitter) account in bulk using browser automation.. + /// Looks up a localized string similar to CleanMyPosts is a lightweight Windows desktop app that securely deletes all posts, reposts, replies, likes, and followings from your X (formerly Twitter) account in bulk using browser automation.. /// public static string SettingsPageAboutText { get { diff --git a/src/UI/Properties/Resources.resx b/src/UI/Properties/Resources.resx index 3bbe567..51a6c81 100644 --- a/src/UI/Properties/Resources.resx +++ b/src/UI/Properties/Resources.resx @@ -124,7 +124,7 @@ Settings - CleanMyPosts is a lightweight Windows desktop app that securely deletes all tweets, likes, and followings from your X (formerly Twitter) account in bulk using browser automation. + CleanMyPosts is a lightweight Windows desktop app that securely deletes all posts, reposts, replies, likes, and followings from your X (formerly Twitter) account in bulk using browser automation. About From 26504b55584f85bad631e0190f1eeb9342d42720 Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:34:28 +0200 Subject: [PATCH 09/11] Rename gifs --- .../{clean-following.gif => delete-following.gif} | Bin assets/{clean-likes.gif => delete-likes.gif} | Bin assets/{clean-posts.gif => delete-posts.gif} | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename assets/{clean-following.gif => delete-following.gif} (100%) rename assets/{clean-likes.gif => delete-likes.gif} (100%) rename assets/{clean-posts.gif => delete-posts.gif} (100%) diff --git a/assets/clean-following.gif b/assets/delete-following.gif similarity index 100% rename from assets/clean-following.gif rename to assets/delete-following.gif diff --git a/assets/clean-likes.gif b/assets/delete-likes.gif similarity index 100% rename from assets/clean-likes.gif rename to assets/delete-likes.gif diff --git a/assets/clean-posts.gif b/assets/delete-posts.gif similarity index 100% rename from assets/clean-posts.gif rename to assets/delete-posts.gif From 69427d33ee4646f44d069b86e356e5711b7e3c74 Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:34:46 +0200 Subject: [PATCH 10/11] Make js more robust --- src/UI/Scripts/delete-all-following.js | 66 ++++---- src/UI/Scripts/delete-all-likes.js | 64 ++++---- src/UI/Scripts/delete-all-posts.js | 206 +++++++++++++------------ src/UI/Scripts/delete-all-replies.js | 160 ++++++++----------- src/UI/Scripts/delete-all-reposts.js | 77 +++++---- 5 files changed, 285 insertions(+), 288 deletions(-) diff --git a/src/UI/Scripts/delete-all-following.js b/src/UI/Scripts/delete-all-following.js index bb0a8a6..23d24e7 100644 --- a/src/UI/Scripts/delete-all-following.js +++ b/src/UI/Scripts/delete-all-following.js @@ -1,16 +1,18 @@ async function DeleteAllFollowing(waitBeforeTryClickDelete, waitBetweenTryClickDeleteAttempts, maxConfirmAttempts = 5) { - // Simple delay helper returning a Promise function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } - // Wait for unfollow button to appear, scrolling every interval until maxTotalTime + function isVisible(elem) { + return elem && elem.offsetParent !== null && !elem.disabled; + } + async function waitForUnfollowButton(selector, maxTotalTime = 5000, interval = 200) { const start = Date.now(); - while (true) { - if (document.querySelector(selector)) { - console.log("[waitForUnfollowButton] Found unfollow button"); + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + console.log("[waitForUnfollowButton] Found visible unfollow button"); return true; } @@ -20,89 +22,83 @@ async function DeleteAllFollowing(waitBeforeTryClickDelete, waitBetweenTryClickD return false; } - console.log("[waitForUnfollowButton] scroll down"); window.scrollBy(0, 500); + console.log("[waitForUnfollowButton] Scrolling..."); await delay(interval); } } - // Click unfollow button and handle confirmation retries with delays async function clickUnfollowButtonWithConfirm() { const btn = document.querySelector('button[data-testid$="-unfollow"]'); - if (!btn) { - console.log("[clickUnfollowButtonWithConfirm] No unfollow button found"); + if (!btn || !isVisible(btn)) { + console.log("[clickUnfollowButtonWithConfirm] No visible unfollow button found"); return false; } console.log("[clickUnfollowButtonWithConfirm] Clicking unfollow button"); btn.click(); - - // Wait before trying to click confirm await delay(waitBeforeTryClickDelete); - // Try clicking confirm button multiple times with increasing delays for (let attempt = 0; attempt < maxConfirmAttempts; attempt++) { const confirmBtn = document.querySelector('button[data-testid="confirmationSheetConfirm"]'); - if (confirmBtn && confirmBtn.offsetParent !== null) { - console.log(`[clickUnfollowButtonWithConfirm] Clicking confirm button on attempt #${attempt + 1}`); + if (confirmBtn && isVisible(confirmBtn)) { + console.log(`[clickUnfollowButtonWithConfirm] Clicking confirm on attempt #${attempt + 1}`); confirmBtn.click(); return true; - } else { - console.log(`[clickUnfollowButtonWithConfirm] Confirm button not visible on attempt #${attempt + 1}, retrying after delay...`); - await delay(waitBetweenTryClickDeleteAttempts * (attempt + 1)); } + + const delayTime = waitBetweenTryClickDeleteAttempts * (attempt + 1); + console.log(`[clickUnfollowButtonWithConfirm] Confirm button not ready (attempt ${attempt + 1}), retrying in ${delayTime}ms...`); + await delay(delayTime); } - console.log("[clickUnfollowButtonWithConfirm] Failed to find or click confirm button after retries"); + console.log("[clickUnfollowButtonWithConfirm] Failed to confirm unfollow after retries"); return false; } - // Try unfollow maxTries times before giving up on current item async function tryUnfollow(maxTries) { for (let attempt = 1; attempt <= maxTries; attempt++) { console.log(`[tryUnfollow] Attempt #${attempt}`); const success = await clickUnfollowButtonWithConfirm(); if (success) { - console.log(`[tryUnfollow] Success on attempt #${attempt}`); + console.log(`[tryUnfollow] Successfully unfollowed on attempt #${attempt}`); return true; } + if (attempt < maxTries) { - console.log(`[tryUnfollow] Failed attempt #${attempt}, retrying...`); + console.log(`[tryUnfollow] Will retry after delay...`); await delay(waitBetweenTryClickDeleteAttempts); } } - console.log(`[tryUnfollow] Failed after ${maxTries} attempts`); return false; } - // Main delete loop let unfollowCount = 1; window.followingDeletionDone = false; window.deletedFollowing = 0; - if (!document.querySelector('button[data-testid$="-unfollow"]')) { - console.log("[deleteAllFollowing] No unfollow buttons present on page. Aborting."); - window.followingDeletionDone = true; - return; - } + console.log("[DeleteAllFollowing] Starting unfollow loop..."); while (true) { const found = await waitForUnfollowButton('button[data-testid$="-unfollow"]', 5000, 200); if (!found) { - console.log("[deleteAllFollowing] No unfollow buttons found after timeout. Stopping."); - window.followingDeletionDone = true; + console.log("[DeleteAllFollowing] No unfollow buttons found after timeout."); break; } - const unfollowed = await tryUnfollow(5); // for example, max 5 tries per unfollow - if (unfollowed) { - console.log(`[deleteAllFollowing] Unfollowed #${unfollowCount}`); + const success = await tryUnfollow(5); + if (success) { + console.log(`[DeleteAllFollowing] Unfollowed #${unfollowCount}`); unfollowCount++; window.deletedFollowing++; } else { - console.log(`[deleteAllFollowing] Failed to unfollow #${unfollowCount}, stopping.`); - window.followingDeletionDone = true; + console.log(`[DeleteAllFollowing] Could not unfollow #${unfollowCount}, aborting.`); break; } + + await delay(waitBeforeTryClickDelete); } + + window.followingDeletionDone = true; + console.log(`[DeleteAllFollowing] Done. Total unfollowed: ${window.deletedFollowing}`); } diff --git a/src/UI/Scripts/delete-all-likes.js b/src/UI/Scripts/delete-all-likes.js index 46165ef..23b452a 100644 --- a/src/UI/Scripts/delete-all-likes.js +++ b/src/UI/Scripts/delete-all-likes.js @@ -1,16 +1,18 @@ async function DeleteAllLikes(waitTime) { - // Simple delay helper returning a Promise function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } - // Wait for unlike button to appear, scrolling every interval until maxTotalTime - async function waitForUnlikeButton(selector, maxTotalTime, interval) { - const start = Date.now(); + function isVisible(el) { + return el && el.offsetParent !== null; + } + async function waitForUnlikeButton(selector, maxTotalTime = 5000, interval = 200) { + const start = Date.now(); while (true) { - if (document.querySelector(selector)) { - console.log("[waitForUnlikeButton] Found unlike button"); + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { + console.log("[waitForUnlikeButton] Found visible unlike button"); return true; } @@ -20,30 +22,29 @@ async function DeleteAllLikes(waitTime) { return false; } - console.log("[waitForUnlikeButton] scroll down"); + console.log("[waitForUnlikeButton] Scrolling down..."); window.scrollBy(0, 500); await delay(interval); } } - // Click the unlike button and wait for waitTime ms, then check if button disappeared async function clickUnlikeButton() { const btn = document.querySelector('button[data-testid="unlike"]'); - if (!btn) { - console.log("[clickUnlikeButton] No unlike button found"); + if (!btn || !isVisible(btn)) { + console.log("[clickUnlikeButton] No visible unlike button found"); return false; } console.log("[clickUnlikeButton] Clicking unlike button"); btn.click(); - await delay(waitTime); - const stillThere = !!document.querySelector('button[data-testid="unlike"]'); - console.log(`[clickUnlikeButton] Unlike button present after wait? ${stillThere}`); - return !stillThere; + + const stillPresent = document.querySelector('button[data-testid="unlike"]'); + const success = !stillPresent; + console.log(`[clickUnlikeButton] Unlike button still present after delay? ${!!stillPresent}`); + return success; } - // Try clicking unlike button maxTries times before giving up async function tryUnlike(maxTries) { for (let attempt = 1; attempt <= maxTries; attempt++) { console.log(`[tryUnlike] Attempt #${attempt}`); @@ -52,43 +53,44 @@ async function DeleteAllLikes(waitTime) { console.log(`[tryUnlike] Success on attempt #${attempt}`); return true; } + if (attempt < maxTries) { - console.log(`[tryUnlike] Failed attempt #${attempt}, retrying...`); - await delay(1000); + const backoff = 500 + 500 * attempt; + console.log(`[tryUnlike] Failed attempt #${attempt}, retrying in ${backoff}ms...`); + await delay(backoff); } } + console.log(`[tryUnlike] Failed after ${maxTries} attempts`); return false; } - // Main delete loop let postNumber = 1; window.likesDeletionDone = false; window.deletedLikes = 0; - if (!document.querySelector('button[data-testid="unlike"]')) { - console.log("[deleteAll] No unlike buttons present on page. Aborting."); - window.likesDeletionDone = true; - return; - } + console.log("[DeleteAllLikes] Starting unlike loop..."); while (true) { const found = await waitForUnlikeButton('button[data-testid="unlike"]', 5000, 200); if (!found) { - console.log("[deleteAll] No unlike buttons found after timeout. Stopping."); - window.likesDeletionDone = true; + console.log("[DeleteAllLikes] No unlike buttons found. Ending."); break; } - const deleted = await tryUnlike(10); - if (deleted) { - console.log(`[deleteAll] Deleted like #${postNumber}`); - window.deletedLikes++; + const success = await tryUnlike(5); + if (success) { + console.log(`[DeleteAllLikes] Deleted like #${postNumber}`); postNumber++; + window.deletedLikes++; } else { - console.log(`[deleteAll] Failed to delete like #${postNumber}, stopping.`); - window.likesDeletionDone = true; + console.log(`[DeleteAllLikes] Failed to delete like #${postNumber}, stopping.`); break; } + + await delay(waitTime); } + + window.likesDeletionDone = true; + console.log(`[DeleteAllLikes] Completed. Total likes removed: ${window.deletedLikes}`); } diff --git a/src/UI/Scripts/delete-all-posts.js b/src/UI/Scripts/delete-all-posts.js index 78f08b1..784b115 100644 --- a/src/UI/Scripts/delete-all-posts.js +++ b/src/UI/Scripts/delete-all-posts.js @@ -1,38 +1,50 @@ -async function DeleteAllPosts(waitAfterDelete, waitBetweenDeleteAttempts) { +async function DeleteAllReplies(userName, waitAfterDelete, waitBetweenDeleteAttempts) { function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } - async function waitForDeleteButton(selector, maxWait, interval) { - const start = Date.now(); - while (true) { - if (document.querySelector(selector)) { - console.log("[waitForDeleteButton] Found caret button"); - return true; - } - const elapsed = Date.now() - start; - if (elapsed > maxWait) { - console.log(`[waitForDeleteButton] Timeout reached (${elapsed}ms)`); - return false; + function isVisible(elem) { + return elem && elem.offsetParent !== null && !elem.disabled; + } + + async function findVisibleCaretWithRetry(article, maxRetries = 7, delayMs = 300) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const caret = article.querySelector("button[data-testid='caret']"); + if (caret && isVisible(caret)) { + return caret; } - console.log("[waitForDeleteButton] Scrolling to look for caret button..."); - window.scrollBy(0, 500); - await delay(interval); + await delay(delayMs); } + return null; } - async function clickDeleteOnPost() { - console.log("[clickDeleteOnPost] Looking for caret button..."); - const caretButton = document.querySelector("div[data-testid='primaryColumn'] section button[data-testid='caret']"); - if (!caretButton) { - console.log("[clickDeleteOnPost] Caret button not found."); - return false; - } + async function clickCaretWithScrollRetry() { + const maxScrollAttempts = 6; + const scrollDelay = 1000; // ms + for (let scrollAttempt = 0; scrollAttempt < maxScrollAttempts; scrollAttempt++) { + const articles = Array.from(document.querySelectorAll("article[data-testid='tweet']")); + const target = articles.find(article => article.querySelector(`a[href*="/${userName}"]`)); + + if (target) { + const caret = await findVisibleCaretWithRetry(target, 7, 300); + if (!caret) { + console.log("[clickCaretWithScrollRetry] Caret found but not visible after retries."); + return false; + } + caret.click(); + return true; + } - console.log("[clickDeleteOnPost] Clicking caret button"); - caretButton.click(); - await delay(waitBetweenDeleteAttempts); + // Not found yet: scroll to load more replies and wait + console.log(`[clickCaretWithScrollRetry] Caret not found, scrolling attempt ${scrollAttempt + 1}...`); + window.scrollBy(0, 1500); + await delay(scrollDelay); + } + console.log("[clickCaretWithScrollRetry] Caret not found after scrolling attempts."); + return false; + } + async function tryClickDelete(attempt = 0) { const delays = [ waitBetweenDeleteAttempts, waitBetweenDeleteAttempts * 2, @@ -41,103 +53,101 @@ async function DeleteAllPosts(waitAfterDelete, waitBetweenDeleteAttempts) { waitBetweenDeleteAttempts * 5, ]; - async function tryClickDelete(attempt = 0) { - if (attempt >= delays.length) { - console.log("[tryClickDelete] Failed to find delete option."); - return false; - } + if (attempt >= delays.length) return false; - await delay(delays[attempt]); - - const menu = document.querySelector("[role='menu']"); - if (menu && menu.style.display !== "none") { - console.log(`[tryClickDelete] Looking for delete option in menu (attempt #${attempt + 1})`); - const items = document.querySelectorAll("[role='menuitem']"); - for (const item of items) { - const span = item.querySelector("span"); - if (!span) continue; - - const color = getComputedStyle(span).color; - const rgb = color.match(/\d+/g).map(Number); - const [r, g, b] = rgb; - - if (r > 180 && g < 100 && b < 100) { - console.log("[tryClickDelete] Found and clicked delete option"); - span.click(); - return true; - } - } + await delay(delays[attempt]); + + const menu = document.querySelector("[role='menu']"); + if (menu && menu.style.display !== "none") { + const items = document.querySelectorAll("[role='menuitem']"); + for (const item of items) { + const span = item.querySelector("span"); + if (!span) continue; - console.log(`[tryClickDelete] Delete option not found, retrying... (attempt #${attempt + 1})`); - return tryClickDelete(attempt + 1); - } else { - console.log(`[tryClickDelete] Menu not visible, retrying... (attempt #${attempt + 1})`); - return tryClickDelete(attempt + 1); + const color = getComputedStyle(span).color; + const rgb = color.match(/\d+/g).map(Number); + const [r, g, b] = rgb; + + if (r > 180 && g < 100 && b < 100) { + span.click(); + return true; + } } + return tryClickDelete(attempt + 1); + } else { + return tryClickDelete(attempt + 1); } + } - async function confirmDelete(attempt = 0) { - if (attempt >= delays.length) { - console.log("[confirmDelete] Failed to confirm deletion."); - return false; - } + async function confirmDelete(attempt = 0) { + const delays = [ + waitBetweenDeleteAttempts, + waitBetweenDeleteAttempts * 2, + waitBetweenDeleteAttempts * 3, + waitBetweenDeleteAttempts * 4, + waitBetweenDeleteAttempts * 5, + ]; - await delay(delays[attempt]); - const confirmBtn = document.querySelector("button[data-testid='confirmationSheetConfirm']"); + if (attempt >= delays.length) return false; - if (confirmBtn && confirmBtn.offsetParent !== null) { - console.log(`[confirmDelete] Clicking confirm delete (attempt #${attempt + 1})`); - confirmBtn.click(); - return true; - } else { - console.log(`[confirmDelete] Confirm button not visible, retrying... (attempt #${attempt + 1})`); - return confirmDelete(attempt + 1); - } - } + await delay(delays[attempt]); - const clickedDelete = await tryClickDelete(); - if (!clickedDelete) { - console.log("[clickDeleteOnPost] Could not click delete from menu."); - return false; + const confirmBtn = document.querySelector("button[data-testid='confirmationSheetConfirm']"); + if (confirmBtn && confirmBtn.offsetParent !== null) { + confirmBtn.click(); + return true; + } else { + return confirmDelete(attempt + 1); } + } - const confirmed = await confirmDelete(); - if (!confirmed) { - console.log("[clickDeleteOnPost] Could not confirm deletion."); - return false; - } + async function waitForReplyCaret(maxWait, interval) { + const start = Date.now(); + while (true) { + const caret = document.querySelector("article[data-testid='tweet'] button[data-testid='caret']"); + if (caret) return true; - console.log("[clickDeleteOnPost] Post deleted successfully."); - return true; - } + if ((Date.now() - start) > maxWait) return false; - window.postsDeletionDone = false; - window.deletedPosts = 0; + window.scrollBy(0, 500); + await delay(interval); + } + } - let postNumber = 1; - console.log("[DeleteAllPosts] Starting post deletion loop..."); + window.repliesDeletionDone = false; + window.deletedReplies = 0; while (true) { - const found = await waitForDeleteButton("div[data-testid='primaryColumn'] section button[data-testid='caret']", 5000, 200); + const found = await waitForReplyCaret(5000, 200); if (!found) { - console.log("[DeleteAllPosts] No more posts found to delete. Stopping."); + console.log("[DeleteAllReplies] No more replies found."); break; } - console.log(`[DeleteAllPosts] Attempting to delete post #${postNumber}`); - const success = await clickDeleteOnPost(); - if (!success) { - console.log(`[DeleteAllPosts] Failed to delete post #${postNumber}. Stopping.`); + const caretClicked = await clickCaretWithScrollRetry(); + if (!caretClicked) { + console.log("[DeleteAllReplies] Failed to find and click caret."); + break; + } + + await delay(waitBetweenDeleteAttempts); + + const clickedDelete = await tryClickDelete(); + if (!clickedDelete) { + console.log("[DeleteAllReplies] Failed to click delete option."); break; } - window.deletedPosts++; - console.log(`[DeleteAllPosts] Deleted post #${postNumber}`); - postNumber++; + const confirmed = await confirmDelete(); + if (!confirmed) { + console.log("[DeleteAllReplies] Failed to confirm delete."); + break; + } + window.deletedReplies++; await delay(waitAfterDelete); } - window.postsDeletionDone = true; - console.log(`[DeleteAllPosts] Deletion complete. Total posts deleted: ${window.deletedPosts}`); + window.repliesDeletionDone = true; + console.log(`[DeleteAllReplies] Completed. Total replies deleted: ${window.deletedReplies}`); } diff --git a/src/UI/Scripts/delete-all-replies.js b/src/UI/Scripts/delete-all-replies.js index 2e6f5c2..f4ad91b 100644 --- a/src/UI/Scripts/delete-all-replies.js +++ b/src/UI/Scripts/delete-all-replies.js @@ -3,139 +3,115 @@ async function DeleteAllReplies(userName, waitAfterDelete, waitBetweenDeleteAtte return new Promise(resolve => setTimeout(resolve, ms)); } - async function waitForReplyCaret(maxWait, interval) { + function isVisible(el) { + return el && el.offsetParent !== null; + } + + async function waitForReplyCaret(maxWait = 5000, interval = 200) { const start = Date.now(); - while (true) { + while (Date.now() - start < maxWait) { const caret = document.querySelector("article[data-testid='tweet'] button[data-testid='caret']"); - if (caret) return true; - - if ((Date.now() - start) > maxWait) return false; + if (caret && isVisible(caret)) return true; window.scrollBy(0, 500); await delay(interval); } + return false; } - // Retry helper to find caret button inside a given article element async function findCaretWithRetry(article, maxRetries = 5, delayMs = 300) { - for (let attempt = 0; attempt < maxRetries; attempt++) { + for (let i = 0; i < maxRetries; i++) { const caret = article.querySelector("button[data-testid='caret']"); - if (caret) return caret; + if (caret && isVisible(caret)) return caret; await delay(delayMs); } return null; } - async function clickDeleteOnReply() { - const maxAttempts = 6; - const scrollDelay = 1000; // 1 second between scrolls - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const articles = Array.from(document.querySelectorAll("article[data-testid='tweet']")); - const target = articles.find(article => { - // Check if article contains a reply link with username - const isReply = article.querySelector('a[href*="/' + userName + '"]'); - return isReply; - }); - - if (target) { - // Found a reply article - find caret with retry - const caret = await findCaretWithRetry(target, 7, 300); - if (!caret) { - console.log("[clickDeleteOnReply] Caret not found after retries."); - return false; - } - - caret.click(); - - await delay(waitBetweenDeleteAttempts); - - const delays = [ - waitBetweenDeleteAttempts, - waitBetweenDeleteAttempts * 2, - waitBetweenDeleteAttempts * 3, - waitBetweenDeleteAttempts * 4, - waitBetweenDeleteAttempts * 5, - ]; - - async function tryClickDelete(attempt = 0) { - if (attempt >= delays.length) return false; - - await delay(delays[attempt]); - - const menu = document.querySelector("[role='menu']"); - if (menu && menu.style.display !== "none") { - const items = document.querySelectorAll("[role='menuitem']"); - for (const item of items) { - const span = item.querySelector("span"); - if (!span) continue; - - const color = getComputedStyle(span).color; - const rgb = color.match(/\d+/g).map(Number); - const [r, g, b] = rgb; - - if (r > 180 && g < 100 && b < 100) { - span.click(); - return true; - } - } - return tryClickDelete(attempt + 1); - } else { - return tryClickDelete(attempt + 1); - } + async function tryClickDeleteMenuItem(attempts, baseDelay) { + for (let i = 0; i < attempts; i++) { + await delay(baseDelay * (i + 1)); + const menuItems = document.querySelectorAll("[role='menuitem']"); + for (const item of menuItems) { + const span = item.querySelector("span"); + if (!span) continue; + + const color = getComputedStyle(span).color; + const [r, g, b] = color.match(/\d+/g).map(Number); + if (r > 180 && g < 100 && b < 100) { + span.click(); + return true; } + } + } + return false; + } - async function confirmDelete(attempt = 0) { - if (attempt >= delays.length) return false; + async function tryConfirmDelete(attempts, baseDelay) { + for (let i = 0; i < attempts; i++) { + await delay(baseDelay * (i + 1)); + const confirmBtn = document.querySelector("button[data-testid='confirmationSheetConfirm']"); + if (confirmBtn && isVisible(confirmBtn)) { + confirmBtn.click(); + return true; + } + } + return false; + } - await delay(delays[attempt]); + async function clickDeleteOnReply() { + const articles = Array.from(document.querySelectorAll("article[data-testid='tweet']")); + const replyArticle = articles.find(article => article.querySelector(`a[href*="/${userName}"]`)); + if (!replyArticle) { + console.log("[clickDeleteOnReply] No matching reply article found."); + return false; + } - const confirmBtn = document.querySelector("button[data-testid='confirmationSheetConfirm']"); - if (confirmBtn && confirmBtn.offsetParent !== null) { - confirmBtn.click(); - return true; - } else { - return confirmDelete(attempt + 1); - } - } + const caret = await findCaretWithRetry(replyArticle, 6, 300); + if (!caret) { + console.log("[clickDeleteOnReply] Caret not found in reply article."); + return false; + } - const clickedDelete = await tryClickDelete(); - if (!clickedDelete) return false; + caret.click(); + await delay(waitBetweenDeleteAttempts); - const confirmed = await confirmDelete(); - return confirmed; - } + const deleteClicked = await tryClickDeleteMenuItem(5, waitBetweenDeleteAttempts); + if (!deleteClicked) { + console.log("[clickDeleteOnReply] Failed to click delete menu item."); + return false; + } - // No reply found yet - scroll to load more replies and wait - console.log(`[clickDeleteOnReply] No reply found, scrolling attempt ${attempt + 1}...`); - window.scrollBy(0, 1500); - await delay(scrollDelay); + const confirmed = await tryConfirmDelete(5, waitBetweenDeleteAttempts); + if (!confirmed) { + console.log("[clickDeleteOnReply] Failed to confirm deletion."); + return false; } - console.log("[clickDeleteOnReply] No reply found after scrolling attempts."); - return false; + return true; } - window.repliesDeletionDone = false; window.deletedReplies = 0; while (true) { const found = await waitForReplyCaret(5000, 200); if (!found) { - console.log("[DeleteAllReplies] No more replies found."); + console.log("[DeleteAllReplies] No more visible replies."); break; } - const success = await clickDeleteOnReply(); - if (!success) { - console.log("[DeleteAllReplies] Failed to delete reply."); + const deleted = await clickDeleteOnReply(); + if (!deleted) { + console.log("[DeleteAllReplies] Failed to delete a reply. Stopping."); break; } window.deletedReplies++; + console.log(`[DeleteAllReplies] Deleted reply #${window.deletedReplies}`); await delay(waitAfterDelete); } window.repliesDeletionDone = true; + console.log(`[DeleteAllReplies] Finished. Total deleted replies: ${window.deletedReplies}`); } diff --git a/src/UI/Scripts/delete-all-reposts.js b/src/UI/Scripts/delete-all-reposts.js index 03dee72..009676c 100644 --- a/src/UI/Scripts/delete-all-reposts.js +++ b/src/UI/Scripts/delete-all-reposts.js @@ -3,10 +3,15 @@ async function DeleteAllRepost(waitTime) { return new Promise(resolve => setTimeout(resolve, ms)); } + function isVisible(elem) { + return elem && elem.offsetParent !== null && !elem.disabled; + } + async function waitForUnretweetButton(selector, maxTotalTime, interval) { const start = Date.now(); while (true) { - if (document.querySelector(selector)) { + const btn = document.querySelector(selector); + if (btn && isVisible(btn)) { console.log("[waitForUnretweetButton] Found unretweet button"); return true; } @@ -17,73 +22,81 @@ async function DeleteAllRepost(waitTime) { return false; } - console.log("[waitForUnretweetButton] scroll down"); + console.log("[waitForUnretweetButton] Scrolling to find button..."); window.scrollBy(0, 500); await delay(interval); } } - async function clickUnretweetButton() { - const btn = document.querySelector('button[data-testid="unretweet"]'); - if (!btn) { - console.log("[clickUnretweetButton] No unretweet button found"); - return false; - } - - console.log("[clickUnretweetButton] Clicking unretweet button"); - btn.click(); - await delay(500); + async function clickUnretweetButtonWithRetry(maxTries = 5) { + for (let attempt = 1; attempt <= maxTries; attempt++) { + const btn = document.querySelector('button[data-testid="unretweet"]'); + if (btn && isVisible(btn)) { + console.log(`[clickUnretweetButton] Clicking unretweet (attempt ${attempt})`); + btn.click(); + await delay(500); - for (let i = 0; i < 5; i++) { - await delay(waitTime); - const menuItem = document.querySelector('div[role="menuitem"][data-testid="unretweetConfirm"]'); - if (menuItem) { - console.log("[clickUnretweetButton] Confirming unretweet"); - menuItem.click(); - await delay(waitTime); - return true; + const confirmResult = await confirmUnretweet(waitTime); + if (confirmResult) return true; + } else { + console.log(`[clickUnretweetButton] Button not found or not visible (attempt ${attempt})`); } + + await delay(500); } - console.log("[clickUnretweetButton] Failed to confirm unretweet"); + console.log("[clickUnretweetButtonWithRetry] Failed to unretweet after retries."); return false; } - async function tryUnretweet(maxTries) { - for (let attempt = 1; attempt <= maxTries; attempt++) { - console.log(`[tryUnretweet] Attempt #${attempt}`); - const success = await clickUnretweetButton(); - if (success) { - console.log(`[tryUnretweet] Success on attempt #${attempt}`); + async function confirmUnretweet(waitTime, maxRetries = 5) { + const delays = Array.from({ length: maxRetries }, (_, i) => waitTime * (i + 1)); + for (let i = 0; i < delays.length; i++) { + await delay(delays[i]); + + const menuItem = document.querySelector('div[role="menuitem"][data-testid="unretweetConfirm"]'); + if (menuItem && isVisible(menuItem)) { + console.log(`[confirmUnretweet] Confirming unretweet (attempt ${i + 1})`); + menuItem.click(); + await delay(waitTime); return true; + } else { + console.log(`[confirmUnretweet] Confirm button not ready (attempt ${i + 1})`); } - await delay(1000); } + + console.log("[confirmUnretweet] Failed to confirm after retries."); return false; } + // MAIN LOOP let postNumber = 1; let deletedCount = 0; window.repostsDeletionDone = false; + console.log("[DeleteAllRepost] Starting repost deletion loop..."); + while (true) { const found = await waitForUnretweetButton('button[data-testid="unretweet"]', 5000, 200); if (!found) { - console.log("[deleteAllRepost] No unretweet buttons found. Ending."); + console.log("[DeleteAllRepost] No more unretweet buttons found."); break; } - const deleted = await tryUnretweet(5); + const deleted = await clickUnretweetButtonWithRetry(5); if (deleted) { - console.log(`[deleteAllRepost] Deleted repost #${postNumber}`); + console.log(`[DeleteAllRepost] Deleted repost #${postNumber}`); postNumber++; deletedCount++; } else { - console.log(`[deleteAllRepost] Failed to delete repost #${postNumber}, stopping.`); + console.log(`[DeleteAllRepost] Failed to delete repost #${postNumber}, stopping.`); break; } + + await delay(waitTime); } window.deletedReposts = deletedCount; window.repostsDeletionDone = true; + console.log(`[DeleteAllRepost] Done. Total reposts deleted: ${deletedCount}`); } From 2c16c20fde8bfaa92647a5b9da3d480a21749be1 Mon Sep 17 00:00:00 2001 From: thorstenalpers Date: Tue, 27 May 2025 18:39:31 +0200 Subject: [PATCH 11/11] New release --- release-notes/v2.0.0.md | 5 +++++ src/UI/UI.csproj | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 release-notes/v2.0.0.md diff --git a/release-notes/v2.0.0.md b/release-notes/v2.0.0.md new file mode 100644 index 0000000..8d5c91d --- /dev/null +++ b/release-notes/v2.0.0.md @@ -0,0 +1,5 @@ +### What's Changed + +- **Bulk Delete Reposts**: Quickly remove multiple reposts in one action to maintain a clean and streamlined profile. +- **Bulk Delete Replies**: Effortlessly delete multiple replies at once, helping you manage and organize your interactions more efficiently. +- **Improved Log View**: The log display now uses HTML formatting, offering clearer and more structured output for better readability. \ No newline at end of file diff --git a/src/UI/UI.csproj b/src/UI/UI.csproj index 520873e..31027e3 100644 --- a/src/UI/UI.csproj +++ b/src/UI/UI.csproj @@ -10,7 +10,7 @@ Assets\logo.ico CleanMyPosts net9.0-windows - 1.0.3 + 2.0.0 false latest True