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

Save and delete bookmarks in X #346

Merged
merged 25 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aeacf4d
Start adding archiveBookmarks setting
micahflee Dec 15, 2024
41dbba2
Add deleteBookmarks setting
micahflee Dec 15, 2024
ce9cd51
Start implementing indexBookmarks, and add a bookmark testdata file
micahflee Dec 16, 2024
a82a868
Refactor runJobIndexBookmarks to work for bookmarks instead of likes,…
micahflee Dec 16, 2024
9cfa6af
Make indexParseTweetsResponseData work with bookmark data too, and wr…
micahflee Dec 16, 2024
1280c28
Use indexParseTweets for tweets, likes, and bookmarks
micahflee Dec 17, 2024
da16b1c
Delete all the indexLikesFinished() and similar functions, because al…
micahflee Dec 17, 2024
a7cb2fb
Add save bookmarks to review page
micahflee Dec 17, 2024
69402c1
Fix loading bookmarks URL, make status and progress work with bookmar…
micahflee Dec 17, 2024
62a3117
Start adding bookmarks to archive
micahflee Dec 17, 2024
6fbacec
Prevent archive.js from accidentally getting packaged into x-archive.zip
micahflee Dec 17, 2024
5f0a073
Change title of X archive, and hide tweets nav item if there are no t…
micahflee Dec 17, 2024
3611781
Start adding code to view model to delete bookmarks
micahflee Dec 17, 2024
0cbe673
Merge apiDeleteTweet and apiDeleteLike into graphqlDelete, and use th…
micahflee Dec 17, 2024
87e9812
Add support for deleting bookmarks
micahflee Dec 17, 2024
9371a32
Allow deleting just bookmarks
micahflee Dec 17, 2024
06e0b17
Stop logging every cookie change
micahflee Dec 17, 2024
3694d67
Actually run the deleteBookmarks job
micahflee Dec 17, 2024
464fc4c
When indexing tweets, allow them to be a tweet, retweet, like, and bo…
micahflee Dec 17, 2024
98c0158
Add new deletedAt columns to tweet table, and make the migration gues…
micahflee Dec 17, 2024
2206741
Switch from tweet.deletedAt to different delete vars, and update dele…
micahflee Dec 18, 2024
92e60aa
Test the migration
micahflee Dec 18, 2024
50a8bc5
Add bookmarks stats to wizard sidebar
micahflee Dec 18, 2024
56aa839
Move logic for posting XProgressInfo to the server into util_x xPostP…
micahflee Dec 18, 2024
eb19183
Send bookmarks data to server with XProgress
micahflee Dec 18, 2024
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
4 changes: 3 additions & 1 deletion archive-static-sites/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
cd "$(dirname "$0")"

# Build the archive static site
echo "👉 Building X archive static site..."
echo ">> Building X archive static site..."
rm -f x-archive/public/assets/archive.js
cd x-archive
rm -r dist || true
npm install
Expand All @@ -13,5 +14,6 @@ npm run build
# Zip it up
cd dist
mkdir -p ../../../build/
rm -f ../../../build/x-archive.zip
zip -r ../../../build/x-archive.zip .
cd ../..
16 changes: 12 additions & 4 deletions archive-static-sites/x-archive/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ provide('archiveData', archiveData);
onMounted(() => {
// Make sure the archive data is there
archiveDataFound.value = 'archiveData' in window;

// Set the document title
if (archiveDataFound.value) {
document.title = `@${archiveData.value.username}'s Archive`;
}
})
</script>

Expand All @@ -31,18 +36,21 @@ onMounted(() => {
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<li v-if="archiveData.tweets.length > 0" class="nav-item">
<router-link to="/" class="nav-link">Tweets</router-link>
</li>
<li class="nav-item">
<li v-if="archiveData.retweets.length > 0" class="nav-item">
<router-link to="/retweets" class="nav-link">Retweets</router-link>
</li>
<li class="nav-item">
<li v-if="archiveData.likes.length > 0" class="nav-item">
<router-link to="/likes" class="nav-link">Likes</router-link>
</li>
<li class="nav-item">
<li v-if="archiveData.conversations.length > 0" class="nav-item">
<router-link to="/messages" class="nav-link">Messages</router-link>
</li>
<li v-if="archiveData.bookmarks.length > 0" class="nav-item">
<router-link to="/bookmarks" class="nav-link">Bookmarks</router-link>
</li>
</ul>
</div>
</div>
Expand Down
13 changes: 11 additions & 2 deletions archive-static-sites/x-archive/src/components/TweetComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,17 @@ defineProps<{
</div>
</div>
<div class="meta d-flex gap-2">
<span v-if="tweet.deletedAt" class="date text-muted ms-2">
deleted {{ formattedDate(tweet.deletedAt) }}
<span v-if="tweet.deletedTweetAt" class="date text-muted ms-2">
tweet deleted {{ formattedDate(tweet.deletedTweetAt) }}
</span>
<span v-if="tweet.deletedRetweetAt" class="date text-muted ms-2">
retweet deleted {{ formattedDate(tweet.deletedRetweetAt) }}
</span>
<span v-if="tweet.deletedLikeAt" class="date text-muted ms-2">
like deleted {{ formattedDate(tweet.deletedLikeAt) }}
</span>
<span v-if="tweet.deletedBookmarkAt" class="date text-muted ms-2">
bookmark deleted {{ formattedDate(tweet.deletedBookmarkAt) }}
</span>
<span v-if="tweet.archivedAt" class="date text-muted ms-2">
archived {{ formattedDate(tweet.archivedAt) }}
Expand Down
8 changes: 7 additions & 1 deletion archive-static-sites/x-archive/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import TweetsView from "../views/TweetsView.vue";
import RetweetsView from "../views/RetweetsView.vue";
import LikesView from "../views/LikesView.vue";
import MessagesView from "../views/MessagesView.vue";
import BookmarksView from "../views/BookmarksView.vue";

const routes: Array<RouteRecordRaw> = [
{
Expand All @@ -24,7 +25,12 @@ const routes: Array<RouteRecordRaw> = [
path: "/messages",
name: "messages",
component: MessagesView,
}
},
{
path: "/bookmarks",
name: "bookmarks",
component: BookmarksView,
},
];

const router = createRouter({
Expand Down
6 changes: 5 additions & 1 deletion archive-static-sites/x-archive/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export type Tweet = {
text: string;
path: string;
archivedAt: string | null;
deletedAt: string | null;
deletedTweetAt: string | null;
deletedRetweetAt: string | null;
deletedLikeAt: string | null;
deletedBookmarkAt: string | null;
};

export type User = {
Expand Down Expand Up @@ -46,6 +49,7 @@ export type XArchive = {
tweets: Tweet[];
retweets: Tweet[];
likes: Tweet[];
bookmarks: Tweet[];
users: Record<string, User>;
conversations: Conversation[];
messages: Message[];
Expand Down
52 changes: 52 additions & 0 deletions archive-static-sites/x-archive/src/views/BookmarksView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { Ref, inject, ref, computed } from 'vue'
import { XArchive } from '../types'
import TweetComponent from '../components/TweetComponent.vue'

const archiveData = inject('archiveData') as Ref<XArchive>;

const filterText = ref('');

const filteredBookmarks = computed(() => {
return archiveData.value.bookmarks.filter(tweet => {
const lowerCaseFilterText = filterText.value.toLowerCase();
const tweetText = tweet.text ? tweet.text.toLowerCase() : '';
const tweetUsername = tweet.username ? tweet.username.toLowerCase() : '';
return tweetText.includes(lowerCaseFilterText) || tweetUsername.includes(lowerCaseFilterText);
});
});
</script>

<template>
<div class="tweets-container">
<div class="filter-container">
<p><input type="text" v-model="filterText" class="form-control" placeholder="Filter your bookmarks"></p>
<p class="text-center text-muted small">Showing {{ filteredBookmarks.length.toLocaleString() }} bookmarks
</p>
</div>

<div class="tweets-list">
<TweetComponent v-for="tweet in filteredBookmarks" :key="tweet.tweetID" :tweet="tweet" />
</div>
</div>
</template>

<style scoped>
.tweets-container {
display: flex;
flex-direction: column;
height: calc(100vh - 150px);
max-width: 700px;
margin: 0 auto;
}

.filter-container {
flex-shrink: 0;
}

.tweets-list {
flex-grow: 1;
overflow-y: auto;
padding: 0 20px;
}
</style>
69 changes: 68 additions & 1 deletion src/account_x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ class MockMITMController implements IMITMController {
},
];
}
if (testdata == "indexBookmarks") {
this.responseData = [
{
host: 'x.com',
url: '/i/api/graphql/Ds7FCVYEIivOKHsGcE84xQ/Bookmarks?variables=%7B%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%7D&features=%7B%22graphql_timeline_v2_bookmark_timeline%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Afalse%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D',
status: 200,
headers: {},
body: fs.readFileSync(path.join(__dirname, '..', 'testdata', 'XAPIBookmarks.json'), 'utf8'),
processed: false
}
];
}
}
setAutomationErrorReportTestdata(filename: string) {
const testData = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'testdata', 'automation-errors', filename), 'utf8'));
Expand Down Expand Up @@ -480,7 +492,7 @@ test("XAccountController.indexParsedTweets() should add all the test tweets", as
const progress: XProgress = await controller.indexParseTweets()
expect(progress.likesIndexed).toBe(2);
expect(progress.retweetsIndexed).toBe(3);
expect(progress.tweetsIndexed).toBe(23);
expect(progress.tweetsIndexed).toBe(24);
expect(progress.unknownIndexed).toBe(16);

const rows: XTweetRow[] = database.exec(controller.db, "SELECT * FROM tweet", [], "all") as XTweetRow[];
Expand Down Expand Up @@ -703,6 +715,19 @@ test('XAccountController.indexParseAllJSON() should return user stats', async ()
expect(account.likesCount).toBe(177);
})

test("XAccountController.indexParsedTweets() should index bookmarks", async () => {
mitmController.setTestdata("indexBookmarks");
if (controller.account) {
controller.account.username = 'nexamind91325';
}

const progress: XProgress = await controller.indexParseTweets()
expect(progress.bookmarksIndexed).toBe(14);

const rows: XTweetRow[] = database.exec(controller.db, "SELECT * FROM tweet WHERE isBookmarked=1", [], "all") as XTweetRow[];
expect(rows.length).toBe(14);
})

// Testing the X migrations

test("test migration: 20241016_add_config", async () => {
Expand All @@ -721,3 +746,45 @@ test("test migration: 20241016_add_config", async () => {
const rows = database.exec(controller.db, "SELECT * FROM config", [], "all") as Record<string, string>[];
expect(rows.length).toBe(0);
})

test("test migration: 20241127_add_deletedAt_fields", async () => {
// Close the X account database
controller.cleanup();

// Replace it with test data
const accountDataPath = getAccountDataPath("X", "test");
fs.mkdirSync(accountDataPath, { recursive: true });
fs.copyFileSync(path.join(__dirname, '..', 'testdata', 'migrations-x', '20241127_add_deletedAt_fields.sqlite3'), path.join(accountDataPath, 'data.sqlite3'));

// Before the migration, there is only deletedAt fields
// Run the migrations
controller.initDB()

// The tweets should all have deletedTweetAt, deletedRetweetAt, and deletedLikeAt values
const afterTweetRows = database.exec(controller.db, "SELECT * FROM tweet WHERE deletedAt IS NOT NULL ORDER BY id", [], "all") as Record<string, string>[];
expect(afterTweetRows.length).toBe(6);

expect(afterTweetRows[0].deletedTweetAt).toBe(null);
expect(afterTweetRows[0].deletedRetweetAt).toBeDefined();
expect(afterTweetRows[0].deletedLikeAt).toBe(null);

expect(afterTweetRows[1].deletedTweetAt).toBe(null);
expect(afterTweetRows[1].deletedRetweetAt).toBeDefined();
expect(afterTweetRows[1].deletedLikeAt).toBeDefined();

expect(afterTweetRows[2].deletedTweetAt).toBeDefined();
expect(afterTweetRows[2].deletedRetweetAt).toBe(null);
expect(afterTweetRows[2].deletedLikeAt).toBe(null);

expect(afterTweetRows[3].deletedTweetAt).toBeDefined();
expect(afterTweetRows[3].deletedRetweetAt).toBe(null);
expect(afterTweetRows[3].deletedLikeAt).toBe(null);

expect(afterTweetRows[4].deletedTweetAt).toBe(null);
expect(afterTweetRows[4].deletedRetweetAt).toBe(null);
expect(afterTweetRows[4].deletedLikeAt).toBeDefined();

expect(afterTweetRows[5].deletedTweetAt).toBe(null);
expect(afterTweetRows[5].deletedRetweetAt).toBeDefined();
expect(afterTweetRows[5].deletedLikeAt).toBeDefined();
})
Loading
Loading