From f83e61bb7e08551cabf7b807a3ec3411b1055f74 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Sat, 14 Dec 2024 18:48:09 -0800
Subject: [PATCH 01/14] Start adding bluesky tables
---
src/database.test.ts | 3 +-
src/database.ts | 67 ++++++++++++++++++++++++++
src/renderer/src/util.ts | 2 +
src/renderer/src/views/AccountView.vue | 18 +++++++
4 files changed, 89 insertions(+), 1 deletion(-)
diff --git a/src/database.test.ts b/src/database.test.ts
index 6a5c23d3..1a9d167f 100644
--- a/src/database.test.ts
+++ b/src/database.test.ts
@@ -43,7 +43,7 @@ afterEach(() => {
// database tests
-test("config, account, and xAccount tables should be created", async () => {
+test("config, account, and xAccount, blueskyAccount tables should be created", async () => {
const db = database.getMainDatabase();
const tables = await database.exec(
db,
@@ -55,6 +55,7 @@ test("config, account, and xAccount tables should be created", async () => {
expect.objectContaining({ name: 'config' }),
expect.objectContaining({ name: 'account' }),
expect.objectContaining({ name: 'xAccount' }),
+ expect.objectContaining({ name: 'blueskyAccount' }),
]));
})
diff --git a/src/database.ts b/src/database.ts
index 8a609a44..d4b5fa4f 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -157,6 +157,42 @@ export const runMainMigrations = () => {
`ALTER TABLE xAccount ADD COLUMN archiveMyData BOOLEAN DEFAULT 0;`,
]
},
+ // Add Bluesky table
+ {
+ name: "add Bluesky table",
+ sql: [
+ `CREATE TABLE blueskyAccount (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ accessedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ username TEXT,
+ profileImageDataURI TEXT,
+ saveMyData BOOLEAN DEFAULT 1,
+ deleteMyData BOOLEAN DEFAULT 0,
+ archivePosts BOOLEAN DEFAULT 1,
+ archivePostsHTML BOOLEAN DEFAULT 0,
+ archiveLikes BOOLEAN DEFAULT 1,
+ archiveDMs BOOLEAN DEFAULT 1,
+ deletePosts BOOLEAN DEFAULT 1,
+ deletePostsDaysOld INTEGER DEFAULT 0,
+ deletePostsLikesThresholdEnabled BOOLEAN DEFAULT 0,
+ deletePostsLikesThreshold INTEGER DEFAULT 20,
+ deletePostsRepostsThresholdEnabled BOOLEAN DEFAULT 0,
+ deletePostsRepostsThreshold INTEGER DEFAULT 20,
+ deleteReposts BOOLEAN DEFAULT 1,
+ deleteRepostsDaysOld INTEGER DEFAULT 0,
+ deleteLikes BOOLEAN DEFAULT 0,
+ deleteLikesDaysOld INTEGER DEFAULT 0,
+ deleteDMs BOOLEAN DEFAULT 0,
+ unfollowEveryone BOOLEAN DEFAULT 1,
+ followingCount INTEGER DEFAULT 0,
+ followersCount INTEGER DEFAULT 0,
+ postsCount INTEGER DEFAULT -1,
+ likesCount INTEGER DEFAULT -1
+);`,
+ ]
+ }
]);
}
@@ -216,6 +252,37 @@ interface XAccountRow {
likesCount: number;
}
+export interface BlueskyAccountRow {
+ id: number;
+ createdAt: string;
+ updatedAt: string;
+ accessedAt: string;
+ username: string;
+ profileImageDataURI: string;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archivePosts: boolean;
+ archivePostsHTML: boolean;
+ archiveLikes: boolean;
+ archiveDMs: boolean;
+ deletePosts: boolean;
+ deletePostsDaysOld: number;
+ deletePostsLikesThresholdEnabled: boolean;
+ deletePostsLikesThreshold: number;
+ deletePostsRepostsThresholdEnabled: boolean;
+ deletePostsRepostsThreshold: number;
+ deleteReposts: boolean;
+ deleteRepostsDaysOld: number;
+ deleteLikes: boolean;
+ deleteLikesDaysOld: number;
+ deleteDMs: boolean;
+ unfollowEveryone: boolean;
+ followingCount: number;
+ followersCount: number;
+ postsCount: number;
+ likesCount: number;
+}
+
export interface ErrorReportRow {
id: number;
createdAt: string;
diff --git a/src/renderer/src/util.ts b/src/renderer/src/util.ts
index 7567a9bf..84d14bb5 100644
--- a/src/renderer/src/util.ts
+++ b/src/renderer/src/util.ts
@@ -59,6 +59,8 @@ export function getAccountIcon(accountType: string): string {
// Not using the real X logo to avoid trademark issues
// return "fa-brands fa-x-twitter";
return "fa-solid fa-xmark";
+ case "Bluesky":
+ return "fa-brands fa-bluesky";
default:
return "fa-solid fa-gears";
}
diff --git a/src/renderer/src/views/AccountView.vue b/src/renderer/src/views/AccountView.vue
index 1fde5421..653f239d 100644
--- a/src/renderer/src/views/AccountView.vue
+++ b/src/renderer/src/views/AccountView.vue
@@ -73,6 +73,24 @@ onMounted(async () => {
+
+
+
+
+
+
+
+
+ Bluesky
+
+
+ Open source, decentralized social media platform
+
+
+
+
+
+
More platforms coming soon.
From 9172b06917342f8ad301c8838776b7c567a2fa3c Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Tue, 7 Jan 2025 17:31:13 -0800
Subject: [PATCH 02/14] Improve layout of platforms
---
src/renderer/src/views/AccountView.vue | 48 ++++++++++++++------------
1 file changed, 25 insertions(+), 23 deletions(-)
diff --git a/src/renderer/src/views/AccountView.vue b/src/renderer/src/views/AccountView.vue
index 653f239d..d8cc9b88 100644
--- a/src/renderer/src/views/AccountView.vue
+++ b/src/renderer/src/views/AccountView.vue
@@ -55,36 +55,38 @@ onMounted(async () => {
Ready to get started? Add a new account.
-
-
-
-
-
-
-
-
- X
+
+
+
+
+
+
-
- Formerly Twitter, owned by Elon Musk
+
+
+ X
+
+
+ Formerly Twitter, owned by Elon Musk
+
-
-
-
-
-
-
-
-
-
- Bluesky
+
+
+
+
+
-
- Open source, decentralized social media platform
+
+
+ Bluesky
+
+
+ Open source decentralized social media platform
+
From 95e7eedcda959f54d849babe0663bc9878c4a7b8 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Tue, 7 Jan 2025 17:31:31 -0800
Subject: [PATCH 03/14] Add bluesky database migrations and functions
---
src/database.ts | 218 ++++++++++++++++++++++++++++++++++++++++----
src/shared_types.ts | 32 +++++++
2 files changed, 234 insertions(+), 16 deletions(-)
diff --git a/src/database.ts b/src/database.ts
index 9a8f4c9b..ca78273d 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -7,7 +7,7 @@ import Database from 'better-sqlite3'
import { app, ipcMain, session } from 'electron';
import { getSettingsPath, packageExceptionForReport } from "./util"
-import { ErrorReport, Account, XAccount } from './shared_types'
+import { ErrorReport, Account, XAccount, BlueskyAccount } from './shared_types'
export type Migration = {
name: string;
@@ -74,7 +74,7 @@ export const runMainMigrations = () => {
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL DEFAULT 'unknown',
sortOrder INTEGER NOT NULL DEFAULT 0,
- xAccountId INTEGER DEFAULT NULL,
+ xAccountID INTEGER DEFAULT NULL,
uuid TEXT NOT NULL
);`, `CREATE TABLE xAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -165,9 +165,9 @@ export const runMainMigrations = () => {
`ALTER TABLE xAccount ADD COLUMN deleteBookmarks BOOLEAN DEFAULT 0;`,
]
},
- // Add Bluesky table
+ // Add Bluesky table, and blueskyAccountID to account
{
- name: "add Bluesky table",
+ name: "add Bluesky table, and blueskyAccountID to account",
sql: [
`CREATE TABLE blueskyAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -181,24 +181,26 @@ export const runMainMigrations = () => {
archivePosts BOOLEAN DEFAULT 1,
archivePostsHTML BOOLEAN DEFAULT 0,
archiveLikes BOOLEAN DEFAULT 1,
- archiveDMs BOOLEAN DEFAULT 1,
deletePosts BOOLEAN DEFAULT 1,
+ deletePostsDaysOldEnabled BOOLEAN DEFAULT 0,
deletePostsDaysOld INTEGER DEFAULT 0,
deletePostsLikesThresholdEnabled BOOLEAN DEFAULT 0,
deletePostsLikesThreshold INTEGER DEFAULT 20,
deletePostsRepostsThresholdEnabled BOOLEAN DEFAULT 0,
deletePostsRepostsThreshold INTEGER DEFAULT 20,
deleteReposts BOOLEAN DEFAULT 1,
+ deleteRepostsDaysOldEnabled BOOLEAN DEFAULT 0,
deleteRepostsDaysOld INTEGER DEFAULT 0,
deleteLikes BOOLEAN DEFAULT 0,
+ deleteLikesDaysOldEnabled BOOLEAN DEFAULT 0,
deleteLikesDaysOld INTEGER DEFAULT 0,
- deleteDMs BOOLEAN DEFAULT 0,
unfollowEveryone BOOLEAN DEFAULT 1,
followingCount INTEGER DEFAULT 0,
followersCount INTEGER DEFAULT 0,
postsCount INTEGER DEFAULT -1,
likesCount INTEGER DEFAULT -1
);`,
+ `ALTER TABLE account ADD COLUMN blueskyAccountID INTEGER DEFAULT NULL;`,
]
},
]);
@@ -222,7 +224,8 @@ interface AccountRow {
id: number;
type: string;
sortOrder: number;
- xAccountId: number | null;
+ xAccountID: number | null;
+ blueskyAccountID: number | null;
uuid: string;
}
@@ -274,19 +277,19 @@ export interface BlueskyAccountRow {
archivePosts: boolean;
archivePostsHTML: boolean;
archiveLikes: boolean;
- archiveDMs: boolean;
deletePosts: boolean;
+ deletePostsDaysOldEnabled: boolean;
deletePostsDaysOld: number;
deletePostsLikesThresholdEnabled: boolean;
deletePostsLikesThreshold: number;
deletePostsRepostsThresholdEnabled: boolean;
deletePostsRepostsThreshold: number;
deleteReposts: boolean;
+ deleteRepostsDaysOldEnabled: boolean;
deleteRepostsDaysOld: number;
deleteLikes: boolean;
+ deleteLikesDaysOldEnabled: boolean;
deleteLikesDaysOld: number;
- deleteDMs: boolean;
- unfollowEveryone: boolean;
followingCount: number;
followersCount: number;
postsCount: number;
@@ -614,6 +617,156 @@ export const saveXAccount = (account: XAccount) => {
]);
}
+// Bluesky accounts
+// Get a single Bluesky account by ID
+export const getBlueskyAccount = (id: number): BlueskyAccount | null => {
+ const row: BlueskyAccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount WHERE id = ?', [id], 'get') as BlueskyAccountRow | undefined;
+ if (!row) {
+ return null;
+ }
+ return {
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archivePosts: !!row.archivePosts,
+ archivePostsHTML: !!row.archivePostsHTML,
+ archiveLikes: !!row.archiveLikes,
+ deletePosts: !!row.deletePosts,
+ deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
+ deletePostsDaysOld: row.deletePostsDaysOld,
+ deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
+ deletePostsLikesThreshold: row.deletePostsLikesThreshold,
+ deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
+ deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
+ deleteReposts: !!row.deleteReposts,
+ deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
+ deleteRepostsDaysOld: row.deleteRepostsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
+ deleteLikesDaysOld: row.deleteLikesDaysOld,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ postsCount: row.postsCount,
+ likesCount: row.likesCount
+ };
+}
+
+// Get all Bluesky accounts
+export const getBlueskyAccounts = (): BlueskyAccount[] => {
+ const rows: BlueskyAccountRow[] = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount', [], 'all') as BlueskyAccountRow[];
+
+ const accounts: BlueskyAccount[] = [];
+ for (const row of rows) {
+ accounts.push({
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archivePosts: !!row.archivePosts,
+ archivePostsHTML: !!row.archivePostsHTML,
+ archiveLikes: !!row.archiveLikes,
+ deletePosts: !!row.deletePosts,
+ deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
+ deletePostsDaysOld: row.deletePostsDaysOld,
+ deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
+ deletePostsLikesThreshold: row.deletePostsLikesThreshold,
+ deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
+ deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
+ deleteReposts: !!row.deleteReposts,
+ deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
+ deleteRepostsDaysOld: row.deleteRepostsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
+ deleteLikesDaysOld: row.deleteLikesDaysOld,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ postsCount: row.postsCount,
+ likesCount: row.likesCount
+ });
+ }
+ return accounts;
+}
+
+// Create a new Bluesky account
+export const createBlueskyAccount = (): BlueskyAccount => {
+ const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO blueskyAccount DEFAULT VALUES') as Sqlite3Info;
+ const account = getBlueskyAccount(info.lastInsertRowid);
+ if (!account) {
+ throw new Error("Failed to create account");
+ }
+ return account;
+}
+
+// Update the Bluesky account based on account.id
+export const saveBlueskyAccount = (account: BlueskyAccount) => {
+ exec(getMainDatabase(), `
+ UPDATE blueskyAccount
+ SET
+ updatedAt = CURRENT_TIMESTAMP,
+ accessedAt = CURRENT_TIMESTAMP,
+ username = ?,
+ profileImageDataURI = ?,
+ saveMyData = ?,
+ deleteMyData = ?,
+ archivePosts = ?,
+ archivePostsHTML = ?,
+ archiveLikes = ?,
+ deletePosts = ?,
+ deletePostsDaysOld = ?,
+ deletePostsDaysOldEnabled = ?,
+ deletePostsLikesThresholdEnabled = ?,
+ deletePostsLikesThreshold = ?,
+ deletePostsRepostsThresholdEnabled = ?,
+ deletePostsRepostsThreshold = ?,
+ deleteReposts = ?,
+ deleteRepostsDaysOldEnabled = ?,
+ deleteRepostsDaysOld = ?,
+ deleteLikes = ?,
+ deleteLikesDaysOldEnabled = ?,
+ deleteLikesDaysOld = ?,
+ followingCount = ?,
+ followersCount = ?,
+ postsCount = ?,
+ likesCount = ?
+ WHERE id = ?
+ `, [
+ account.username,
+ account.profileImageDataURI,
+ account.saveMyData ? 1 : 0,
+ account.deleteMyData ? 1 : 0,
+ account.archivePosts ? 1 : 0,
+ account.archivePostsHTML ? 1 : 0,
+ account.archiveLikes ? 1 : 0,
+ account.deletePosts ? 1 : 0,
+ account.deletePostsDaysOld,
+ account.deletePostsDaysOldEnabled ? 1 : 0,
+ account.deletePostsLikesThresholdEnabled ? 1 : 0,
+ account.deletePostsLikesThreshold,
+ account.deletePostsRepostsThresholdEnabled ? 1 : 0,
+ account.deletePostsRepostsThreshold,
+ account.deleteReposts ? 1 : 0,
+ account.deleteRepostsDaysOldEnabled ? 1 : 0,
+ account.deleteRepostsDaysOld,
+ account.deleteLikes ? 1 : 0,
+ account.deleteLikesDaysOldEnabled ? 1 : 0,
+ account.deleteLikesDaysOld,
+ account.followingCount,
+ account.followersCount,
+ account.postsCount,
+ account.likesCount,
+ account.id
+ ]);
+}
+
// Accounts, which contain all others
export const getAccount = (id: number): Account | null => {
@@ -623,10 +776,17 @@ export const getAccount = (id: number): Account | null => {
}
let xAccount: XAccount | null = null;
+ let blueskyAccount: BlueskyAccount | null = null;
switch (row.type) {
case "X":
- if (row.xAccountId) {
- xAccount = getXAccount(row.xAccountId);
+ if (row.xAccountID) {
+ xAccount = getXAccount(row.xAccountID);
+ }
+ break;
+
+ case "Bluesky":
+ if (row.blueskyAccountID) {
+ blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
}
break;
}
@@ -636,6 +796,7 @@ export const getAccount = (id: number): Account | null => {
type: row.type,
sortOrder: row.sortOrder,
xAccount: xAccount,
+ blueskyAccount: blueskyAccount,
uuid: row.uuid
};
}
@@ -643,6 +804,8 @@ export const getAccount = (id: number): Account | null => {
export async function getAccountUsername(account: Account): Promise {
if (account.type == "X" && account.xAccount) {
return account.xAccount?.username;
+ } else if (account.type == "Bluesky" && account.blueskyAccount) {
+ return account.blueskyAccount?.username;
}
return null;
@@ -654,12 +817,18 @@ export const getAccounts = (): Account[] => {
const accounts: Account[] = [];
for (const row of rows) {
let xAccount: XAccount | null = null;
+ let blueskyAccount: BlueskyAccount | null = null;
switch (row.type) {
case "X":
- if (row.xAccountId) {
- xAccount = getXAccount(row.xAccountId);
+ if (row.xAccountID) {
+ xAccount = getXAccount(row.xAccountID);
}
break;
+ case "Bluesky":
+ if (row.blueskyAccountID) {
+ blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
+ }
+ break
}
accounts.push({
@@ -667,6 +836,7 @@ export const getAccounts = (): Account[] => {
type: row.type,
sortOrder: row.sortOrder,
xAccount: xAccount,
+ blueskyAccount: blueskyAccount,
uuid: row.uuid
});
}
@@ -706,20 +876,28 @@ export const selectAccountType = (accountID: number, type: string): Account => {
case "X":
account.xAccount = createXAccount();
break;
+ case "Bluesky":
+ account.blueskyAccount = createBlueskyAccount();
+ break;
default:
throw new Error("Unknown account type");
}
+ const xAccountID = account.xAccount ? account.xAccount.id : null;
+ const blueskyAccountID = account.blueskyAccount ? account.blueskyAccount.id : null;
+
// Update the account
exec(getMainDatabase(), `
UPDATE account
SET
type = ?,
- xAccountId = ?
+ xAccountID = ?,
+ blueskyAccountID = ?
WHERE id = ?
`, [
type,
- account.xAccount.id,
+ xAccountID,
+ blueskyAccountID,
account.id
]);
@@ -732,6 +910,9 @@ export const saveAccount = (account: Account) => {
if (account.xAccount) {
saveXAccount(account.xAccount);
}
+ else if (account.blueskyAccount) {
+ saveBlueskyAccount(account.blueskyAccount);
+ }
exec(getMainDatabase(), `
UPDATE account
@@ -760,6 +941,11 @@ export const deleteAccount = (accountID: number) => {
exec(getMainDatabase(), 'DELETE FROM xAccount WHERE id = ?', [account.xAccount.id]);
}
break;
+ case "Bluesky":
+ if (account.blueskyAccount) {
+ exec(getMainDatabase(), 'DELETE FROM blueskyAccount WHERE id = ?', [account.blueskyAccount.id]);
+ }
+ break;
}
// Delete the account
diff --git a/src/shared_types.ts b/src/shared_types.ts
index dae88400..15042c14 100644
--- a/src/shared_types.ts
+++ b/src/shared_types.ts
@@ -29,6 +29,7 @@ export type Account = {
type: string; // "X"
sortOrder: number;
xAccount: XAccount | null;
+ blueskyAccount: BlueskyAccount | null;
uuid: string;
}
@@ -68,6 +69,37 @@ export type XAccount = {
likesCount: number;
};
+export type BlueskyAccount = {
+ id: number;
+ createdAt: Date;
+ updatedAt: Date;
+ accessedAt: Date;
+ username: string;
+ profileImageDataURI: string;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archivePosts: boolean;
+ archivePostsHTML: boolean;
+ archiveLikes: boolean;
+ deletePosts: boolean;
+ deletePostsDaysOldEnabled: boolean;
+ deletePostsDaysOld: number;
+ deletePostsLikesThresholdEnabled: boolean;
+ deletePostsLikesThreshold: number;
+ deletePostsRepostsThresholdEnabled: boolean;
+ deletePostsRepostsThreshold: number;
+ deleteReposts: boolean;
+ deleteRepostsDaysOldEnabled: boolean;
+ deleteRepostsDaysOld: number;
+ deleteLikes: boolean;
+ deleteLikesDaysOldEnabled: boolean;
+ deleteLikesDaysOld: number;
+ followingCount: number;
+ followersCount: number;
+ postsCount: number;
+ likesCount: number;
+}
+
// X models
export type XJob = {
From 9a5d8769d9904d564d2bab29fd86646a4b36fd56 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Tue, 7 Jan 2025 17:35:49 -0800
Subject: [PATCH 04/14] Add some bluesky db tests
---
src/database.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/src/database.test.ts b/src/database.test.ts
index 1a9d167f..0593df91 100644
--- a/src/database.test.ts
+++ b/src/database.test.ts
@@ -137,6 +137,66 @@ test("getXAccounts should retrieve all XAccounts", () => {
expect(accounts).toEqual(expect.arrayContaining([xAccount1, xAccount2]));
});
+test("createBlueskyAccount should create a new BlueskyAccount", () => {
+ const blueskyAccount = database.createBlueskyAccount();
+ expect(blueskyAccount).toHaveProperty('id');
+ expect(blueskyAccount).toHaveProperty('createdAt');
+ expect(blueskyAccount).toHaveProperty('updatedAt');
+ expect(blueskyAccount).toHaveProperty('accessedAt');
+ expect(blueskyAccount).toHaveProperty('username');
+ expect(blueskyAccount).toHaveProperty('profileImageDataURI');
+ expect(blueskyAccount).toHaveProperty('saveMyData');
+ expect(blueskyAccount).toHaveProperty('deleteMyData');
+ expect(blueskyAccount).toHaveProperty('archivePosts');
+ expect(blueskyAccount).toHaveProperty('archivePostsHTML');
+ expect(blueskyAccount).toHaveProperty('archiveLikes');
+ expect(blueskyAccount).toHaveProperty('deletePosts');
+ expect(blueskyAccount).toHaveProperty('deletePostsDaysOld');
+ expect(blueskyAccount).toHaveProperty('deletePostsDaysOldEnabled');
+ expect(blueskyAccount).toHaveProperty('deletePostsLikesThresholdEnabled');
+ expect(blueskyAccount).toHaveProperty('deletePostsLikesThreshold');
+ expect(blueskyAccount).toHaveProperty('deletePostsRepostsThresholdEnabled');
+ expect(blueskyAccount).toHaveProperty('deletePostsRepostsThreshold');
+ expect(blueskyAccount).toHaveProperty('deleteReposts');
+ expect(blueskyAccount).toHaveProperty('deleteRepostsDaysOld');
+ expect(blueskyAccount).toHaveProperty('deleteRepostsDaysOldEnabled');
+ expect(blueskyAccount).toHaveProperty('deleteLikes');
+ expect(blueskyAccount).toHaveProperty('deleteLikesDaysOld');
+ expect(blueskyAccount).toHaveProperty('deleteLikesDaysOldEnabled');
+ expect(blueskyAccount).toHaveProperty('followingCount');
+ expect(blueskyAccount).toHaveProperty('followersCount');
+ expect(blueskyAccount).toHaveProperty('postsCount');
+ expect(blueskyAccount).toHaveProperty('likesCount');
+});
+
+test("saveBlueskyAccount should update an existing BlueskyAccount", () => {
+ const blueskyAccount = database.createBlueskyAccount();
+ blueskyAccount.username = 'newUsername';
+ database.saveBlueskyAccount(blueskyAccount);
+
+ const db = database.getMainDatabase();
+ const result = database.exec(db, 'SELECT * FROM blueskyAccount WHERE id = ?', [blueskyAccount.id], 'get');
+ expect(result).toEqual(expect.objectContaining({ username: 'newUsername' }));
+});
+
+test("getBlueskyAccount should retrieve the correct BlueskyAccount", () => {
+ const blueskyAccount = database.createBlueskyAccount();
+ database.saveBlueskyAccount(blueskyAccount);
+
+ const retrievedAccount = database.getBlueskyAccount(blueskyAccount.id);
+ expect(retrievedAccount).toEqual(blueskyAccount);
+});
+
+test("getBlueskyAccounts should retrieve all BlueskyAccounts", () => {
+ const blueskyAccount1 = database.createBlueskyAccount();
+ const blueskyAccount2 = database.createBlueskyAccount();
+ database.saveBlueskyAccount(blueskyAccount1);
+ database.saveBlueskyAccount(blueskyAccount2);
+
+ const accounts = database.getBlueskyAccounts();
+ expect(accounts).toEqual(expect.arrayContaining([blueskyAccount1, blueskyAccount2]));
+});
+
test("createAccount should create a new Account", () => {
const account = database.createAccount();
expect(account).toHaveProperty('id');
From ca0e34ed517ef4557cba21891014da797bfa32aa Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:02:29 -0800
Subject: [PATCH 05/14] Fix typescript annotations
---
src/account_x.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/account_x.ts b/src/account_x.ts
index cd028541..c60974c4 100644
--- a/src/account_x.ts
+++ b/src/account_x.ts
@@ -2603,10 +2603,10 @@ export const defineIPCX = () => {
}
});
- ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => {
+ ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => {
try {
const controller = getXAccountController(accountID);
- return await controller.deleteUnzippedXArchive(archivePath);
+ await controller.deleteUnzippedXArchive(archivePath);
} catch (error) {
throw new Error(packageExceptionForReport(error as Error));
}
From f64cc7ce0e8e2a5ff10dd53384f8590d9a431451 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:04:46 -0800
Subject: [PATCH 06/14] Keep naming xAccountId with Id instead of ID
---
src/database.ts | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/database.ts b/src/database.ts
index ca78273d..95008401 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -74,7 +74,7 @@ export const runMainMigrations = () => {
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL DEFAULT 'unknown',
sortOrder INTEGER NOT NULL DEFAULT 0,
- xAccountID INTEGER DEFAULT NULL,
+ xAccountId INTEGER DEFAULT NULL,
uuid TEXT NOT NULL
);`, `CREATE TABLE xAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -224,7 +224,7 @@ interface AccountRow {
id: number;
type: string;
sortOrder: number;
- xAccountID: number | null;
+ xAccountId: number | null;
blueskyAccountID: number | null;
uuid: string;
}
@@ -779,8 +779,8 @@ export const getAccount = (id: number): Account | null => {
let blueskyAccount: BlueskyAccount | null = null;
switch (row.type) {
case "X":
- if (row.xAccountID) {
- xAccount = getXAccount(row.xAccountID);
+ if (row.xAccountId) {
+ xAccount = getXAccount(row.xAccountId);
}
break;
@@ -820,8 +820,8 @@ export const getAccounts = (): Account[] => {
let blueskyAccount: BlueskyAccount | null = null;
switch (row.type) {
case "X":
- if (row.xAccountID) {
- xAccount = getXAccount(row.xAccountID);
+ if (row.xAccountId) {
+ xAccount = getXAccount(row.xAccountId);
}
break;
case "Bluesky":
@@ -883,7 +883,7 @@ export const selectAccountType = (accountID: number, type: string): Account => {
throw new Error("Unknown account type");
}
- const xAccountID = account.xAccount ? account.xAccount.id : null;
+ const xAccountId = account.xAccount ? account.xAccount.id : null;
const blueskyAccountID = account.blueskyAccount ? account.blueskyAccount.id : null;
// Update the account
@@ -891,12 +891,12 @@ export const selectAccountType = (accountID: number, type: string): Account => {
UPDATE account
SET
type = ?,
- xAccountID = ?,
+ xAccountId = ?,
blueskyAccountID = ?
WHERE id = ?
`, [
type,
- xAccountID,
+ xAccountId,
blueskyAccountID,
account.id
]);
From 04e5d709cfa152f55d616e40664d2d880a4d481f Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:14:00 -0800
Subject: [PATCH 07/14] Refactor shared_types to split into multiple files
---
src/shared_types/account.ts | 75 +++++++++++++++
src/shared_types/common.ts | 25 +++++
src/shared_types/index.ts | 3 +
src/{shared_types.ts => shared_types/x.ts} | 102 ---------------------
4 files changed, 103 insertions(+), 102 deletions(-)
create mode 100644 src/shared_types/account.ts
create mode 100644 src/shared_types/common.ts
create mode 100644 src/shared_types/index.ts
rename src/{shared_types.ts => shared_types/x.ts} (71%)
diff --git a/src/shared_types/account.ts b/src/shared_types/account.ts
new file mode 100644
index 00000000..9d85da22
--- /dev/null
+++ b/src/shared_types/account.ts
@@ -0,0 +1,75 @@
+export type Account = {
+ id: number;
+ type: string; // "X"
+ sortOrder: number;
+ xAccount: XAccount | null;
+ blueskyAccount: BlueskyAccount | null;
+ uuid: string;
+}
+
+export type XAccount = {
+ id: number;
+ createdAt: Date;
+ updatedAt: Date;
+ accessedAt: Date;
+ username: string;
+ profileImageDataURI: string;
+ importFromArchive: boolean;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archiveMyData: boolean;
+ archiveTweets: boolean;
+ archiveTweetsHTML: boolean;
+ archiveLikes: boolean;
+ archiveBookmarks: boolean;
+ archiveDMs: boolean;
+ deleteTweets: boolean;
+ deleteTweetsDaysOldEnabled: boolean;
+ deleteTweetsDaysOld: number;
+ deleteTweetsLikesThresholdEnabled: boolean;
+ deleteTweetsLikesThreshold: number;
+ deleteTweetsRetweetsThresholdEnabled: boolean;
+ deleteTweetsRetweetsThreshold: number;
+ deleteRetweets: boolean;
+ deleteRetweetsDaysOldEnabled: boolean;
+ deleteRetweetsDaysOld: number;
+ deleteLikes: boolean;
+ deleteBookmarks: boolean;
+ deleteDMs: boolean;
+ unfollowEveryone: boolean;
+ followingCount: number;
+ followersCount: number;
+ tweetsCount: number;
+ likesCount: number;
+};
+
+export type BlueskyAccount = {
+ id: number;
+ createdAt: Date;
+ updatedAt: Date;
+ accessedAt: Date;
+ username: string;
+ profileImageDataURI: string;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archivePosts: boolean;
+ archivePostsHTML: boolean;
+ archiveLikes: boolean;
+ deletePosts: boolean;
+ deletePostsDaysOldEnabled: boolean;
+ deletePostsDaysOld: number;
+ deletePostsLikesThresholdEnabled: boolean;
+ deletePostsLikesThreshold: number;
+ deletePostsRepostsThresholdEnabled: boolean;
+ deletePostsRepostsThreshold: number;
+ deleteReposts: boolean;
+ deleteRepostsDaysOldEnabled: boolean;
+ deleteRepostsDaysOld: number;
+ deleteLikes: boolean;
+ deleteLikesDaysOldEnabled: boolean;
+ deleteLikesDaysOld: number;
+ followingCount: number;
+ followersCount: number;
+ postsCount: number;
+ likesCount: number;
+}
\ No newline at end of file
diff --git a/src/shared_types/common.ts b/src/shared_types/common.ts
new file mode 100644
index 00000000..18548597
--- /dev/null
+++ b/src/shared_types/common.ts
@@ -0,0 +1,25 @@
+export type ResponseData = {
+ host: string;
+ url: string;
+ status: number;
+ headers: Record;
+ body: string;
+ processed: boolean;
+}
+
+// Models
+
+export type ErrorReport = {
+ id: number;
+ createdAt: string;
+ accountID: number;
+ appVersion: string;
+ clientPlatform: string;
+ accountType: string;
+ errorReportType: string;
+ errorReportData: string;
+ accountUsername: string;
+ screenshotDataURI: string;
+ sensitiveContextData: string;
+ status: string; // "new", "submitted", and "dismissed"
+}
\ No newline at end of file
diff --git a/src/shared_types/index.ts b/src/shared_types/index.ts
new file mode 100644
index 00000000..d35a1abb
--- /dev/null
+++ b/src/shared_types/index.ts
@@ -0,0 +1,3 @@
+export * from './common';
+export * from './account';
+export * from './x';
\ No newline at end of file
diff --git a/src/shared_types.ts b/src/shared_types/x.ts
similarity index 71%
rename from src/shared_types.ts
rename to src/shared_types/x.ts
index 15042c14..2268126b 100644
--- a/src/shared_types.ts
+++ b/src/shared_types/x.ts
@@ -1,105 +1,3 @@
-export type ResponseData = {
- host: string;
- url: string;
- status: number;
- headers: Record;
- body: string;
- processed: boolean;
-}
-
-// Models
-
-export type ErrorReport = {
- id: number;
- createdAt: string;
- accountID: number;
- appVersion: string;
- clientPlatform: string;
- accountType: string;
- errorReportType: string;
- errorReportData: string;
- accountUsername: string;
- screenshotDataURI: string;
- sensitiveContextData: string;
- status: string; // "new", "submitted", and "dismissed"
-}
-
-export type Account = {
- id: number;
- type: string; // "X"
- sortOrder: number;
- xAccount: XAccount | null;
- blueskyAccount: BlueskyAccount | null;
- uuid: string;
-}
-
-export type XAccount = {
- id: number;
- createdAt: Date;
- updatedAt: Date;
- accessedAt: Date;
- username: string;
- profileImageDataURI: string;
- importFromArchive: boolean;
- saveMyData: boolean;
- deleteMyData: boolean;
- archiveMyData: boolean;
- archiveTweets: boolean;
- archiveTweetsHTML: boolean;
- archiveLikes: boolean;
- archiveBookmarks: boolean;
- archiveDMs: boolean;
- deleteTweets: boolean;
- deleteTweetsDaysOldEnabled: boolean;
- deleteTweetsDaysOld: number;
- deleteTweetsLikesThresholdEnabled: boolean;
- deleteTweetsLikesThreshold: number;
- deleteTweetsRetweetsThresholdEnabled: boolean;
- deleteTweetsRetweetsThreshold: number;
- deleteRetweets: boolean;
- deleteRetweetsDaysOldEnabled: boolean;
- deleteRetweetsDaysOld: number;
- deleteLikes: boolean;
- deleteBookmarks: boolean;
- deleteDMs: boolean;
- unfollowEveryone: boolean;
- followingCount: number;
- followersCount: number;
- tweetsCount: number;
- likesCount: number;
-};
-
-export type BlueskyAccount = {
- id: number;
- createdAt: Date;
- updatedAt: Date;
- accessedAt: Date;
- username: string;
- profileImageDataURI: string;
- saveMyData: boolean;
- deleteMyData: boolean;
- archivePosts: boolean;
- archivePostsHTML: boolean;
- archiveLikes: boolean;
- deletePosts: boolean;
- deletePostsDaysOldEnabled: boolean;
- deletePostsDaysOld: number;
- deletePostsLikesThresholdEnabled: boolean;
- deletePostsLikesThreshold: number;
- deletePostsRepostsThresholdEnabled: boolean;
- deletePostsRepostsThreshold: number;
- deleteReposts: boolean;
- deleteRepostsDaysOldEnabled: boolean;
- deleteRepostsDaysOld: number;
- deleteLikes: boolean;
- deleteLikesDaysOldEnabled: boolean;
- deleteLikesDaysOld: number;
- followingCount: number;
- followersCount: number;
- postsCount: number;
- likesCount: number;
-}
-
// X models
export type XJob = {
From 6d4b1bf48e4b13d83de198e525c03ee1fd145ece Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:15:51 -0800
Subject: [PATCH 08/14] Add classes back in for component tests to work
---
src/renderer/src/views/AccountView.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/renderer/src/views/AccountView.vue b/src/renderer/src/views/AccountView.vue
index d8cc9b88..201ace4d 100644
--- a/src/renderer/src/views/AccountView.vue
+++ b/src/renderer/src/views/AccountView.vue
@@ -57,7 +57,7 @@ onMounted(async () => {
-
+
@@ -75,7 +75,7 @@ onMounted(async () => {
-
+
From 21807bccdc9f0ac9a7f91f3585f450e13d16cea3 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:21:50 -0800
Subject: [PATCH 09/14] Fix typescript problems with component tests
---
src/renderer/src/test_util.ts | 120 +++++++++++++-------------
src/renderer/src/views/TabsView.cy.ts | 1 +
2 files changed, 63 insertions(+), 58 deletions(-)
diff --git a/src/renderer/src/test_util.ts b/src/renderer/src/test_util.ts
index 3c6e56f5..6e9fc491 100644
--- a/src/renderer/src/test_util.ts
+++ b/src/renderer/src/test_util.ts
@@ -1,87 +1,91 @@
export const stubElectron = () => {
return {
checkForUpdates: cy.stub(),
- getVersion: cy.stub(),
- getMode: cy.stub(),
- getPlatform: cy.stub(),
- getAPIURL: cy.stub(),
- getDashURL: cy.stub(),
- trackEvent: cy.stub(),
- shouldOpenDevtools: cy.stub(),
+ getVersion: cy.stub().resolves('0.0.1'),
+ getMode: cy.stub().resolves('prod'),
+ getPlatform: cy.stub().resolves('win32'),
+ getAPIURL: cy.stub().resolves('https://api.example.com'),
+ getDashURL: cy.stub().resolves('https://dash.example.com'),
+ trackEvent: cy.stub().resolves('tracked'),
+ shouldOpenDevtools: cy.stub().resolves(false),
showMessage: cy.stub(),
showError: cy.stub(),
- showQuestion: cy.stub(),
- showSelectFolderDialog: cy.stub(),
+ showQuestion: cy.stub().resolves(true),
+ showSelectZIPFileDialog: cy.stub().resolves(null),
+ showSelectFolderDialog: cy.stub().resolves(null),
openURL: cy.stub(),
loadFileInWebview: cy.stub(),
- getAccountDataPath: cy.stub(),
- startPowerSaveBlocker: cy.stub(),
+ getAccountDataPath: cy.stub().resolves(null),
+ startPowerSaveBlocker: cy.stub().resolves(1),
stopPowerSaveBlocker: cy.stub(),
deleteSettingsAndRestart: cy.stub(),
database: {
- getConfig: cy.stub(),
+ getConfig: cy.stub().resolves(null),
setConfig: cy.stub(),
- getErrorReport: cy.stub(),
- getNewErrorReports: cy.stub(),
- createErrorReport: cy.stub(),
+ getErrorReport: cy.stub().resolves(null),
+ getNewErrorReports: cy.stub().resolves([]),
+ createErrorReport: cy.stub().resolves(),
updateErrorReportSubmitted: cy.stub(),
dismissNewErrorReports: cy.stub(),
- getAccount: cy.stub(),
- getAccounts: cy.stub(),
- createAccount: cy.stub(),
- selectAccountType: cy.stub(),
+ getAccount: cy.stub().resolves(null),
+ getAccounts: cy.stub().resolves([]),
+ createAccount: cy.stub().resolves({ id: 1, type: 'X', sortOrder: 0, xAccount: null, blueskyAccount: null, uuid: 'uuid' }),
+ selectAccountType: cy.stub().resolves({ id: 1, type: 'X', sortOrder: 0, xAccount: null, blueskyAccount: null, uuid: 'uuid' }),
saveAccount: cy.stub(),
deleteAccount: cy.stub(),
},
archive: {
- isPageAlreadySaved: cy.stub(),
- savePage: cy.stub(),
+ isPageAlreadySaved: cy.stub().resolves(false),
+ savePage: cy.stub().resolves(false),
},
X: {
- resetProgress: cy.stub(),
- createJobs: cy.stub(),
- getLastFinishedJob: cy.stub(),
+ resetProgress: cy.stub().resolves({}),
+ createJobs: cy.stub().resolves([]),
+ getLastFinishedJob: cy.stub().resolves(null),
updateJob: cy.stub(),
indexStart: cy.stub(),
indexStop: cy.stub(),
- indexParseAllJSON: cy.stub(),
- indexParseTweets: cy.stub(),
- indexParseConversations: cy.stub(),
- indexIsThereMore: cy.stub(),
- resetThereIsMore: cy.stub(),
- indexMessagesStart: cy.stub(),
- indexParseMessages: cy.stub(),
- indexConversationFinished: cy.stub(),
- archiveTweetsStart: cy.stub(),
- archiveTweetsOutputPath: cy.stub(),
- archiveTweet: cy.stub(),
- archiveTweetCheckDate: cy.stub(),
- archiveBuild: cy.stub(),
+ indexParseAllJSON: cy.stub().resolves({}),
+ indexParseTweets: cy.stub().resolves({}),
+ indexParseConversations: cy.stub().resolves({}),
+ indexIsThereMore: cy.stub().resolves(false),
+ resetThereIsMore: cy.stub().resolves(),
+ indexMessagesStart: cy.stub().resolves({}),
+ indexParseMessages: cy.stub().resolves({}),
+ indexConversationFinished: cy.stub().resolves(),
+ archiveTweetsStart: cy.stub().resolves({}),
+ archiveTweetsOutputPath: cy.stub().resolves(''),
+ archiveTweet: cy.stub().resolves(),
+ archiveTweetCheckDate: cy.stub().resolves(),
+ archiveBuild: cy.stub().resolves(),
syncProgress: cy.stub(),
openFolder: cy.stub(),
- getArchiveInfo: cy.stub(),
- resetRateLimitInfo: cy.stub(),
- isRateLimited: cy.stub(),
- getProgress: cy.stub(),
- getProgressInfo: cy.stub(),
- getDatabaseStats: cy.stub(),
- getDeleteReviewStats: cy.stub(),
- saveProfileImage: cy.stub(),
- getLatestResponseData: cy.stub(),
- deleteTweetsStart: cy.stub(),
- deleteTweetsCountNotArchived: cy.stub(),
- deleteRetweetsStart: cy.stub(),
- deleteLikesStart: cy.stub(),
- deleteBookmarksStart: cy.stub(),
- deleteTweet: cy.stub(),
- deleteDMsMarkAllDeleted: cy.stub(),
- deleteDMsScrollToBottom: cy.stub(),
- verifyXArchive: cy.stub(),
- importXArchive: cy.stub(),
- getConfig: cy.stub(),
+ getArchiveInfo: cy.stub().resolves({}),
+ resetRateLimitInfo: cy.stub().resolves(),
+ isRateLimited: cy.stub().resolves({}),
+ getProgress: cy.stub().resolves({}),
+ getProgressInfo: cy.stub().resolves({}),
+ getDatabaseStats: cy.stub().resolves({}),
+ getDeleteReviewStats: cy.stub().resolves({}),
+ saveProfileImage: cy.stub().resolves(),
+ getLatestResponseData: cy.stub().resolves(null),
+ deleteTweetsStart: cy.stub().resolves({}),
+ deleteTweetsCountNotArchived: cy.stub().resolves(0),
+ deleteRetweetsStart: cy.stub().resolves({}),
+ deleteLikesStart: cy.stub().resolves({}),
+ deleteBookmarksStart: cy.stub().resolves({}),
+ deleteTweet: cy.stub().resolves(),
+ deleteDMsMarkAllDeleted: cy.stub().resolves(),
+ deleteDMsScrollToBottom: cy.stub().resolves(),
+ unzipXArchive: cy.stub().resolves(null),
+ deleteUnzippedXArchive: cy.stub().resolves(null),
+ verifyXArchive: cy.stub().resolves(null),
+ importXArchive: cy.stub().resolves({}),
+ getCookie: cy.stub().resolves(null),
+ getConfig: cy.stub().resolves(null),
setConfig: cy.stub(),
},
onPowerMonitorSuspend: cy.stub(),
onPowerMonitorResume: cy.stub(),
};
-}
+};
\ No newline at end of file
diff --git a/src/renderer/src/views/TabsView.cy.ts b/src/renderer/src/views/TabsView.cy.ts
index 8fef23e9..e479d88c 100644
--- a/src/renderer/src/views/TabsView.cy.ts
+++ b/src/renderer/src/views/TabsView.cy.ts
@@ -48,6 +48,7 @@ describe('', () => {
type: 'unknown',
sortOrder: 0,
xAccount: null,
+ blueskyAccount: null,
uuid: accountUUID,
};
testDatabase.accounts.push(newAccount);
From 25534d1e108c69abc851de19be415d17482ec005 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 09:43:24 -0800
Subject: [PATCH 10/14] Refactor database.ts into a folder with multiple files
---
src/database.ts | 1063 -------------------------------
src/database/account.ts | 259 ++++++++
src/database/bluesky_account.ts | 186 ++++++
src/database/common.ts | 105 +++
src/database/config.ts | 49 ++
src/database/error_report.ts | 156 +++++
src/database/index.ts | 9 +
src/database/migrations.ts | 147 +++++
src/database/x_account.ts | 208 ++++++
9 files changed, 1119 insertions(+), 1063 deletions(-)
delete mode 100644 src/database.ts
create mode 100644 src/database/account.ts
create mode 100644 src/database/bluesky_account.ts
create mode 100644 src/database/common.ts
create mode 100644 src/database/config.ts
create mode 100644 src/database/error_report.ts
create mode 100644 src/database/index.ts
create mode 100644 src/database/migrations.ts
create mode 100644 src/database/x_account.ts
diff --git a/src/database.ts b/src/database.ts
deleted file mode 100644
index 95008401..00000000
--- a/src/database.ts
+++ /dev/null
@@ -1,1063 +0,0 @@
-import path from "path"
-import crypto from 'crypto';
-import os from 'os';
-
-import log from 'electron-log/main';
-import Database from 'better-sqlite3'
-import { app, ipcMain, session } from 'electron';
-
-import { getSettingsPath, packageExceptionForReport } from "./util"
-import { ErrorReport, Account, XAccount, BlueskyAccount } from './shared_types'
-
-export type Migration = {
- name: string;
- sql: string[];
-};
-
-let mainDatabase: Database.Database | null = null;
-
-export const getMainDatabase = () => {
- if (mainDatabase) {
- return mainDatabase;
- }
-
- const dbPath = path.join(getSettingsPath(), 'db.sqlite');
- mainDatabase = new Database(dbPath, {});
- mainDatabase.pragma('journal_mode = WAL');
- return mainDatabase;
-}
-
-export const closeMainDatabase = () => {
- if (mainDatabase) {
- mainDatabase.close();
- mainDatabase = null;
- }
-}
-
-export const runMigrations = (db: Database.Database, migrations: Migration[]) => {
- // Create a migrations table if necessary
- const migrationsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'").get();
- if (!migrationsTable) {
- // Create the migrations table
- log.debug("Creating migrations table");
- db.prepare(`CREATE TABLE migrations (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- runAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-)`).run();
- }
-
- // Apply the migrations in order
- for (const migration of migrations) {
- const migrationRecord = db.prepare("SELECT * FROM migrations WHERE name = ?").get(migration.name);
- if (!migrationRecord) {
- log.info(`Running migration: ${migration.name}`);
- for (const sql of migration.sql) {
- db.exec(sql);
- }
- db.prepare("INSERT INTO migrations (name) VALUES (?)").run(migration.name);
- }
- }
-}
-
-export const runMainMigrations = () => {
- runMigrations(getMainDatabase(), [
- // Create the tables
- {
- name: "initial",
- sql: [
- `CREATE TABLE config (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- key TEXT NOT NULL UNIQUE,
- value TEXT NOT NULL
-);`, `CREATE TABLE account (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- type TEXT NOT NULL DEFAULT 'unknown',
- sortOrder INTEGER NOT NULL DEFAULT 0,
- xAccountId INTEGER DEFAULT NULL,
- uuid TEXT NOT NULL
-);`, `CREATE TABLE xAccount (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- accessedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- username TEXT,
- profileImageDataURI TEXT,
- saveMyData BOOLEAN DEFAULT 1,
- deleteMyData BOOLEAN DEFAULT 0,
- archiveTweets BOOLEAN DEFAULT 1,
- archiveTweetsHTML BOOLEAN DEFAULT 0,
- archiveLikes BOOLEAN DEFAULT 1,
- archiveDMs BOOLEAN DEFAULT 1,
- deleteTweets BOOLEAN DEFAULT 1,
- deleteTweetsDaysOld INTEGER DEFAULT 0,
- deleteTweetsLikesThresholdEnabled BOOLEAN DEFAULT 0,
- deleteTweetsLikesThreshold INTEGER DEFAULT 20,
- deleteTweetsRetweetsThresholdEnabled BOOLEAN DEFAULT 0,
- deleteTweetsRetweetsThreshold INTEGER DEFAULT 20,
- deleteRetweets BOOLEAN DEFAULT 1,
- deleteRetweetsDaysOld INTEGER DEFAULT 0,
- deleteLikes BOOLEAN DEFAULT 0,
- deleteLikesDaysOld INTEGER DEFAULT 0,
- deleteDMs BOOLEAN DEFAULT 0
-);`,
- ]
- },
- // Add importFromArchive, followingCount, follwersCount, tweetsCount, likesCount to xAccount
- {
- name: "add importFromArchive, followingCount, follwersCount, tweetsCount, likesCount to xAccount",
- sql: [
- `ALTER TABLE xAccount ADD COLUMN importFromArchive BOOLEAN DEFAULT 1;`,
- `ALTER TABLE xAccount ADD COLUMN followingCount INTEGER DEFAULT 0;`,
- `ALTER TABLE xAccount ADD COLUMN followersCount INTEGER DEFAULT 0;`,
- `ALTER TABLE xAccount ADD COLUMN tweetsCount INTEGER DEFAULT -1;`,
- `ALTER TABLE xAccount ADD COLUMN likesCount INTEGER DEFAULT -1;`,
- ]
- },
- // Add unfollowEveryone to xAccount
- {
- name: "add unfollowEveryone to xAccount",
- sql: [
- `ALTER TABLE xAccount ADD COLUMN unfollowEveryone BOOLEAN DEFAULT 1;`,
- ]
- },
- // Add deleteTweetsDaysOldEnabled, deleteRetweetsDaysOldEnabled, deleteLikesDaysOldEnabled to xAccount
- {
- name: "add deleteTweetsDaysOldEnabled, deleteRetweetsDaysOldEnabled, deleteLikesDaysOldEnabled to xAccount",
- sql: [
- `ALTER TABLE xAccount ADD COLUMN deleteTweetsDaysOldEnabled BOOLEAN DEFAULT 0;`,
- `ALTER TABLE xAccount ADD COLUMN deleteRetweetsDaysOldEnabled BOOLEAN DEFAULT 0;`,
- `ALTER TABLE xAccount ADD COLUMN deleteLikesDaysOldEnabled BOOLEAN DEFAULT 0;`,
- ]
- },
- // Add errorReport table. Status can be "new", "submitted", and "dismissed"
- {
- name: "add errorReport table",
- sql: [
- `CREATE TABLE errorReport (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- accountID INTEGER DEFAULT NULL,
- appVersion TEXT DEFAULT NULL,
- clientPlatform TEXT DEFAULT NULL,
- accountType TEXT DEFAULT NULL,
- errorReportType TEXT NOT NULL,
- errorReportData TEXT DEFAULT NULL,
- accountUsername TEXT DEFAULT NULL,
- screenshotDataURI TEXT DEFAULT NULL,
- sensitiveContextData TEXT DEFAULT NULL,
- status TEXT DEFAULT 'new'
-);`,
- ]
- },
- // Add archiveMyData to xAccount
- {
- name: "add archiveMyData to xAccount",
- sql: [
- `ALTER TABLE xAccount ADD COLUMN archiveMyData BOOLEAN DEFAULT 0;`,
- ]
- },
- // Add archiveBookmarks, deleteBookmarks to xAccount
- {
- name: "add archiveBookmarks, deleteBookmarks to xAccount",
- sql: [
- `ALTER TABLE xAccount ADD COLUMN archiveBookmarks BOOLEAN DEFAULT 1;`,
- `ALTER TABLE xAccount ADD COLUMN deleteBookmarks BOOLEAN DEFAULT 0;`,
- ]
- },
- // Add Bluesky table, and blueskyAccountID to account
- {
- name: "add Bluesky table, and blueskyAccountID to account",
- sql: [
- `CREATE TABLE blueskyAccount (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- accessedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
- username TEXT,
- profileImageDataURI TEXT,
- saveMyData BOOLEAN DEFAULT 1,
- deleteMyData BOOLEAN DEFAULT 0,
- archivePosts BOOLEAN DEFAULT 1,
- archivePostsHTML BOOLEAN DEFAULT 0,
- archiveLikes BOOLEAN DEFAULT 1,
- deletePosts BOOLEAN DEFAULT 1,
- deletePostsDaysOldEnabled BOOLEAN DEFAULT 0,
- deletePostsDaysOld INTEGER DEFAULT 0,
- deletePostsLikesThresholdEnabled BOOLEAN DEFAULT 0,
- deletePostsLikesThreshold INTEGER DEFAULT 20,
- deletePostsRepostsThresholdEnabled BOOLEAN DEFAULT 0,
- deletePostsRepostsThreshold INTEGER DEFAULT 20,
- deleteReposts BOOLEAN DEFAULT 1,
- deleteRepostsDaysOldEnabled BOOLEAN DEFAULT 0,
- deleteRepostsDaysOld INTEGER DEFAULT 0,
- deleteLikes BOOLEAN DEFAULT 0,
- deleteLikesDaysOldEnabled BOOLEAN DEFAULT 0,
- deleteLikesDaysOld INTEGER DEFAULT 0,
- unfollowEveryone BOOLEAN DEFAULT 1,
- followingCount INTEGER DEFAULT 0,
- followersCount INTEGER DEFAULT 0,
- postsCount INTEGER DEFAULT -1,
- likesCount INTEGER DEFAULT -1
-);`,
- `ALTER TABLE account ADD COLUMN blueskyAccountID INTEGER DEFAULT NULL;`,
- ]
- },
- ]);
-}
-
-export interface Sqlite3Info {
- lastInsertRowid: number;
- changes: number;
-}
-
-export interface Sqlite3Count {
- count: number;
-}
-
-interface ConfigRow {
- key: string;
- value: string;
-}
-
-interface AccountRow {
- id: number;
- type: string;
- sortOrder: number;
- xAccountId: number | null;
- blueskyAccountID: number | null;
- uuid: string;
-}
-
-interface XAccountRow {
- id: number;
- createdAt: string;
- updatedAt: string;
- accessedAt: string;
- username: string;
- profileImageDataURI: string;
- importFromArchive: boolean;
- saveMyData: boolean;
- deleteMyData: boolean;
- archiveMyData: boolean;
- archiveTweets: boolean;
- archiveTweetsHTML: boolean;
- archiveLikes: boolean;
- archiveBookmarks: boolean;
- archiveDMs: boolean;
- deleteTweets: boolean;
- deleteTweetsDaysOldEnabled: boolean;
- deleteTweetsDaysOld: number;
- deleteTweetsLikesThresholdEnabled: boolean;
- deleteTweetsLikesThreshold: number;
- deleteTweetsRetweetsThresholdEnabled: boolean;
- deleteTweetsRetweetsThreshold: number;
- deleteRetweets: boolean;
- deleteRetweetsDaysOldEnabled: boolean;
- deleteRetweetsDaysOld: number;
- deleteLikes: boolean;
- deleteBookmarks: number;
- deleteDMs: boolean;
- unfollowEveryone: boolean;
- followingCount: number;
- followersCount: number;
- tweetsCount: number;
- likesCount: number;
-}
-
-export interface BlueskyAccountRow {
- id: number;
- createdAt: string;
- updatedAt: string;
- accessedAt: string;
- username: string;
- profileImageDataURI: string;
- saveMyData: boolean;
- deleteMyData: boolean;
- archivePosts: boolean;
- archivePostsHTML: boolean;
- archiveLikes: boolean;
- deletePosts: boolean;
- deletePostsDaysOldEnabled: boolean;
- deletePostsDaysOld: number;
- deletePostsLikesThresholdEnabled: boolean;
- deletePostsLikesThreshold: number;
- deletePostsRepostsThresholdEnabled: boolean;
- deletePostsRepostsThreshold: number;
- deleteReposts: boolean;
- deleteRepostsDaysOldEnabled: boolean;
- deleteRepostsDaysOld: number;
- deleteLikes: boolean;
- deleteLikesDaysOldEnabled: boolean;
- deleteLikesDaysOld: number;
- followingCount: number;
- followersCount: number;
- postsCount: number;
- likesCount: number;
-}
-
-export interface ErrorReportRow {
- id: number;
- createdAt: string;
- accountID: number;
- appVersion: string;
- clientPlatform: string;
- accountType: string;
- errorReportType: string;
- errorReportData: string;
- accountUsername: string;
- screenshotDataURI: string;
- sensitiveContextData: string;
- status: string; // "new", "submitted", and "dismissed"
-}
-
-// Utils
-
-export const exec = (db: Database.Database | null, sql: string, params: Array = [], cmd: 'run' | 'all' | 'get' = 'run') => {
- if (!db) {
- throw new Error("Database not initialized");
- }
-
- // Convert Date objects to ISO strings
- const paramsConverted: Array = [];
- for (const param of params) {
- if (param instanceof Date) {
- paramsConverted.push(param.toISOString());
- } else {
- paramsConverted.push(param);
- }
- }
-
- // Execute the query
- log.debug("Executing SQL:", sql, "Params:", paramsConverted);
- try {
- const stmt = db.prepare(sql);
- const ret = stmt[cmd](...paramsConverted);
- return ret
- } catch (error) {
- const exception = JSON.parse(packageExceptionForReport(error as Error))
- throw new Error(JSON.stringify({
- exception: exception,
- sql: sql,
- params: paramsConverted
- }));
- }
-}
-
-// Config
-
-export const getConfig = (key: string, db: Database.Database | null = null): string | null => {
- if (!db) {
- db = getMainDatabase();
- }
- const row: ConfigRow | undefined = exec(db, 'SELECT value FROM config WHERE key = ?', [key], 'get') as ConfigRow | undefined;
- return row ? row.value : null;
-}
-
-export const setConfig = (key: string, value: string, db: Database.Database | null = null) => {
- if (!db) {
- db = getMainDatabase();
- }
- exec(db, 'INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', [key, value]);
-}
-
-// Error reports
-
-export const getErrorReport = (id: number): ErrorReport | null => {
- const row: ErrorReportRow | undefined = exec(getMainDatabase(), 'SELECT * FROM errorReport WHERE id = ?', [id], 'get') as ErrorReportRow | undefined;
- if (!row) {
- return null;
- }
- return {
- id: row.id,
- createdAt: row.createdAt,
- accountID: row.accountID,
- appVersion: row.appVersion,
- clientPlatform: row.clientPlatform,
- accountType: row.accountType,
- errorReportType: row.errorReportType,
- errorReportData: row.errorReportData,
- accountUsername: row.accountUsername,
- screenshotDataURI: row.screenshotDataURI,
- sensitiveContextData: row.sensitiveContextData,
- status: row.status
- }
-}
-
-export const getNewErrorReports = (accountID: number): ErrorReport[] => {
- const rows: ErrorReportRow[] = exec(getMainDatabase(), 'SELECT * FROM errorReport WHERE accountID = ? AND status = ?', [accountID, 'new'], 'all') as ErrorReportRow[];
- const errorReports: ErrorReport[] = [];
- for (const row of rows) {
- errorReports.push({
- id: row.id,
- createdAt: row.createdAt,
- accountID: row.accountID,
- appVersion: row.appVersion,
- clientPlatform: row.clientPlatform,
- accountType: row.accountType,
- errorReportType: row.errorReportType,
- errorReportData: row.errorReportData,
- accountUsername: row.accountUsername,
- screenshotDataURI: row.screenshotDataURI,
- sensitiveContextData: row.sensitiveContextData,
- status: row.status
- });
- }
- return errorReports;
-}
-
-export const createErrorReport = (accountID: number, accountType: string, errorReportType: string, errorReportData: string, accountUsername: string | null, screenshotDataURI: string | null, sensitiveContextData: string | null) => {
- const info: Sqlite3Info = exec(getMainDatabase(), `
- INSERT INTO errorReport (
- accountID,
- appVersion,
- clientPlatform,
- accountType,
- errorReportType,
- errorReportData,
- accountUsername,
- screenshotDataURI,
- sensitiveContextData
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- `, [
- accountID,
- app.getVersion(),
- os.platform(),
- accountType,
- errorReportType,
- errorReportData,
- accountUsername,
- screenshotDataURI,
- sensitiveContextData
- ]) as Sqlite3Info;
- const report = getErrorReport(info.lastInsertRowid);
- if (!report) {
- throw new Error("Failed to create error report");
- }
-}
-
-export const updateErrorReportSubmitted = (id: number) => {
- exec(getMainDatabase(), 'DELETE FROM errorReport WHERE id = ?', [id]);
-}
-
-export const dismissNewErrorReports = (accountID: number) => {
- exec(getMainDatabase(), 'DELETE FROM errorReport WHERE accountID = ? AND status = ?', [accountID, 'new']);
-}
-
-export const dismissAllNewErrorReports = () => {
- exec(getMainDatabase(), 'DELETE FROM errorReport WHERE status = ?', ['new']);
-}
-
-// X accounts
-
-export const getXAccount = (id: number): XAccount | null => {
- const row: XAccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM xAccount WHERE id = ?', [id], 'get') as XAccountRow | undefined;
- if (!row) {
- return null;
- }
- return {
- id: row.id,
- createdAt: new Date(row.createdAt),
- updatedAt: new Date(row.updatedAt),
- accessedAt: new Date(row.accessedAt),
- username: row.username,
- profileImageDataURI: row.profileImageDataURI,
- importFromArchive: !!row.importFromArchive,
- saveMyData: !!row.saveMyData,
- deleteMyData: !!row.deleteMyData,
- archiveMyData: !!row.archiveMyData,
- archiveTweets: !!row.archiveTweets,
- archiveTweetsHTML: !!row.archiveTweetsHTML,
- archiveLikes: !!row.archiveLikes,
- archiveBookmarks: !!row.archiveBookmarks,
- archiveDMs: !!row.archiveDMs,
- deleteTweets: !!row.deleteTweets,
- deleteTweetsDaysOldEnabled: !!row.deleteTweetsDaysOldEnabled,
- deleteTweetsDaysOld: row.deleteTweetsDaysOld,
- deleteTweetsLikesThresholdEnabled: !!row.deleteTweetsLikesThresholdEnabled,
- deleteTweetsLikesThreshold: row.deleteTweetsLikesThreshold,
- deleteTweetsRetweetsThresholdEnabled: !!row.deleteTweetsRetweetsThresholdEnabled,
- deleteTweetsRetweetsThreshold: row.deleteTweetsRetweetsThreshold,
- deleteRetweets: !!row.deleteRetweets,
- deleteRetweetsDaysOldEnabled: !!row.deleteRetweetsDaysOldEnabled,
- deleteRetweetsDaysOld: row.deleteRetweetsDaysOld,
- deleteLikes: !!row.deleteLikes,
- deleteBookmarks: !!row.deleteBookmarks,
- deleteDMs: !!row.deleteDMs,
- unfollowEveryone: !!row.unfollowEveryone,
- followingCount: row.followingCount,
- followersCount: row.followersCount,
- tweetsCount: row.tweetsCount,
- likesCount: row.likesCount
- };
-}
-
-export const getXAccounts = (): XAccount[] => {
- const rows: XAccountRow[] = exec(getMainDatabase(), 'SELECT * FROM xAccount', [], 'all') as XAccountRow[];
-
- const accounts: XAccount[] = [];
- for (const row of rows) {
- accounts.push({
- id: row.id,
- createdAt: new Date(row.createdAt),
- updatedAt: new Date(row.updatedAt),
- accessedAt: new Date(row.accessedAt),
- username: row.username,
- profileImageDataURI: row.profileImageDataURI,
- importFromArchive: !!row.importFromArchive,
- saveMyData: !!row.saveMyData,
- deleteMyData: !!row.deleteMyData,
- archiveMyData: !!row.archiveMyData,
- archiveTweets: !!row.archiveTweets,
- archiveTweetsHTML: !!row.archiveTweetsHTML,
- archiveLikes: !!row.archiveLikes,
- archiveBookmarks: !!row.archiveBookmarks,
- archiveDMs: !!row.archiveDMs,
- deleteTweets: !!row.deleteTweets,
- deleteTweetsDaysOldEnabled: !!row.deleteTweetsDaysOldEnabled,
- deleteTweetsDaysOld: row.deleteTweetsDaysOld,
- deleteTweetsLikesThresholdEnabled: !!row.deleteTweetsLikesThresholdEnabled,
- deleteTweetsLikesThreshold: row.deleteTweetsLikesThreshold,
- deleteTweetsRetweetsThresholdEnabled: !!row.deleteTweetsRetweetsThresholdEnabled,
- deleteTweetsRetweetsThreshold: row.deleteTweetsRetweetsThreshold,
- deleteRetweets: !!row.deleteRetweets,
- deleteRetweetsDaysOldEnabled: !!row.deleteRetweetsDaysOldEnabled,
- deleteRetweetsDaysOld: row.deleteRetweetsDaysOld,
- deleteLikes: !!row.deleteLikes,
- deleteBookmarks: !!row.deleteBookmarks,
- deleteDMs: !!row.deleteDMs,
- unfollowEveryone: !!row.unfollowEveryone,
- followingCount: row.followingCount,
- followersCount: row.followersCount,
- tweetsCount: row.tweetsCount,
- likesCount: row.likesCount
- });
- }
- return accounts;
-}
-
-export const createXAccount = (): XAccount => {
- const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO xAccount DEFAULT VALUES') as Sqlite3Info;
- const account = getXAccount(info.lastInsertRowid);
- if (!account) {
- throw new Error("Failed to create account");
- }
- return account;
-}
-
-// Update the account based on account.id
-export const saveXAccount = (account: XAccount) => {
- exec(getMainDatabase(), `
- UPDATE xAccount
- SET
- updatedAt = CURRENT_TIMESTAMP,
- accessedAt = CURRENT_TIMESTAMP,
- username = ?,
- profileImageDataURI = ?,
- importFromArchive = ?,
- saveMyData = ?,
- deleteMyData = ?,
- archiveMyData = ?,
- archiveTweets = ?,
- archiveTweetsHTML = ?,
- archiveLikes = ?,
- archiveBookmarks = ?,
- archiveDMs = ?,
- deleteTweets = ?,
- deleteTweetsDaysOld = ?,
- deleteTweetsDaysOldEnabled = ?,
- deleteTweetsLikesThresholdEnabled = ?,
- deleteTweetsLikesThreshold = ?,
- deleteTweetsRetweetsThresholdEnabled = ?,
- deleteTweetsRetweetsThreshold = ?,
- deleteRetweets = ?,
- deleteRetweetsDaysOldEnabled = ?,
- deleteRetweetsDaysOld = ?,
- deleteLikes = ?,
- deleteBookmarks = ?,
- deleteDMs = ?,
- unfollowEveryone = ?,
- followingCount = ?,
- followersCount = ?,
- tweetsCount = ?,
- likesCount = ?
- WHERE id = ?
- `, [
- account.username,
- account.profileImageDataURI,
- account.importFromArchive ? 1 : 0,
- account.saveMyData ? 1 : 0,
- account.deleteMyData ? 1 : 0,
- account.archiveMyData ? 1 : 0,
- account.archiveTweets ? 1 : 0,
- account.archiveTweetsHTML ? 1 : 0,
- account.archiveLikes ? 1 : 0,
- account.archiveBookmarks ? 1 : 0,
- account.archiveDMs ? 1 : 0,
- account.deleteTweets ? 1 : 0,
- account.deleteTweetsDaysOld,
- account.deleteTweetsDaysOldEnabled ? 1 : 0,
- account.deleteTweetsLikesThresholdEnabled ? 1 : 0,
- account.deleteTweetsLikesThreshold,
- account.deleteTweetsRetweetsThresholdEnabled ? 1 : 0,
- account.deleteTweetsRetweetsThreshold,
- account.deleteRetweets ? 1 : 0,
- account.deleteRetweetsDaysOldEnabled ? 1 : 0,
- account.deleteRetweetsDaysOld,
- account.deleteLikes ? 1 : 0,
- account.deleteBookmarks ? 1 : 0,
- account.deleteDMs ? 1 : 0,
- account.unfollowEveryone ? 1 : 0,
- account.followingCount,
- account.followersCount,
- account.tweetsCount,
- account.likesCount,
- account.id
- ]);
-}
-
-// Bluesky accounts
-// Get a single Bluesky account by ID
-export const getBlueskyAccount = (id: number): BlueskyAccount | null => {
- const row: BlueskyAccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount WHERE id = ?', [id], 'get') as BlueskyAccountRow | undefined;
- if (!row) {
- return null;
- }
- return {
- id: row.id,
- createdAt: new Date(row.createdAt),
- updatedAt: new Date(row.updatedAt),
- accessedAt: new Date(row.accessedAt),
- username: row.username,
- profileImageDataURI: row.profileImageDataURI,
- saveMyData: !!row.saveMyData,
- deleteMyData: !!row.deleteMyData,
- archivePosts: !!row.archivePosts,
- archivePostsHTML: !!row.archivePostsHTML,
- archiveLikes: !!row.archiveLikes,
- deletePosts: !!row.deletePosts,
- deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
- deletePostsDaysOld: row.deletePostsDaysOld,
- deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
- deletePostsLikesThreshold: row.deletePostsLikesThreshold,
- deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
- deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
- deleteReposts: !!row.deleteReposts,
- deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
- deleteRepostsDaysOld: row.deleteRepostsDaysOld,
- deleteLikes: !!row.deleteLikes,
- deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
- deleteLikesDaysOld: row.deleteLikesDaysOld,
- followingCount: row.followingCount,
- followersCount: row.followersCount,
- postsCount: row.postsCount,
- likesCount: row.likesCount
- };
-}
-
-// Get all Bluesky accounts
-export const getBlueskyAccounts = (): BlueskyAccount[] => {
- const rows: BlueskyAccountRow[] = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount', [], 'all') as BlueskyAccountRow[];
-
- const accounts: BlueskyAccount[] = [];
- for (const row of rows) {
- accounts.push({
- id: row.id,
- createdAt: new Date(row.createdAt),
- updatedAt: new Date(row.updatedAt),
- accessedAt: new Date(row.accessedAt),
- username: row.username,
- profileImageDataURI: row.profileImageDataURI,
- saveMyData: !!row.saveMyData,
- deleteMyData: !!row.deleteMyData,
- archivePosts: !!row.archivePosts,
- archivePostsHTML: !!row.archivePostsHTML,
- archiveLikes: !!row.archiveLikes,
- deletePosts: !!row.deletePosts,
- deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
- deletePostsDaysOld: row.deletePostsDaysOld,
- deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
- deletePostsLikesThreshold: row.deletePostsLikesThreshold,
- deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
- deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
- deleteReposts: !!row.deleteReposts,
- deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
- deleteRepostsDaysOld: row.deleteRepostsDaysOld,
- deleteLikes: !!row.deleteLikes,
- deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
- deleteLikesDaysOld: row.deleteLikesDaysOld,
- followingCount: row.followingCount,
- followersCount: row.followersCount,
- postsCount: row.postsCount,
- likesCount: row.likesCount
- });
- }
- return accounts;
-}
-
-// Create a new Bluesky account
-export const createBlueskyAccount = (): BlueskyAccount => {
- const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO blueskyAccount DEFAULT VALUES') as Sqlite3Info;
- const account = getBlueskyAccount(info.lastInsertRowid);
- if (!account) {
- throw new Error("Failed to create account");
- }
- return account;
-}
-
-// Update the Bluesky account based on account.id
-export const saveBlueskyAccount = (account: BlueskyAccount) => {
- exec(getMainDatabase(), `
- UPDATE blueskyAccount
- SET
- updatedAt = CURRENT_TIMESTAMP,
- accessedAt = CURRENT_TIMESTAMP,
- username = ?,
- profileImageDataURI = ?,
- saveMyData = ?,
- deleteMyData = ?,
- archivePosts = ?,
- archivePostsHTML = ?,
- archiveLikes = ?,
- deletePosts = ?,
- deletePostsDaysOld = ?,
- deletePostsDaysOldEnabled = ?,
- deletePostsLikesThresholdEnabled = ?,
- deletePostsLikesThreshold = ?,
- deletePostsRepostsThresholdEnabled = ?,
- deletePostsRepostsThreshold = ?,
- deleteReposts = ?,
- deleteRepostsDaysOldEnabled = ?,
- deleteRepostsDaysOld = ?,
- deleteLikes = ?,
- deleteLikesDaysOldEnabled = ?,
- deleteLikesDaysOld = ?,
- followingCount = ?,
- followersCount = ?,
- postsCount = ?,
- likesCount = ?
- WHERE id = ?
- `, [
- account.username,
- account.profileImageDataURI,
- account.saveMyData ? 1 : 0,
- account.deleteMyData ? 1 : 0,
- account.archivePosts ? 1 : 0,
- account.archivePostsHTML ? 1 : 0,
- account.archiveLikes ? 1 : 0,
- account.deletePosts ? 1 : 0,
- account.deletePostsDaysOld,
- account.deletePostsDaysOldEnabled ? 1 : 0,
- account.deletePostsLikesThresholdEnabled ? 1 : 0,
- account.deletePostsLikesThreshold,
- account.deletePostsRepostsThresholdEnabled ? 1 : 0,
- account.deletePostsRepostsThreshold,
- account.deleteReposts ? 1 : 0,
- account.deleteRepostsDaysOldEnabled ? 1 : 0,
- account.deleteRepostsDaysOld,
- account.deleteLikes ? 1 : 0,
- account.deleteLikesDaysOldEnabled ? 1 : 0,
- account.deleteLikesDaysOld,
- account.followingCount,
- account.followersCount,
- account.postsCount,
- account.likesCount,
- account.id
- ]);
-}
-
-// Accounts, which contain all others
-
-export const getAccount = (id: number): Account | null => {
- const row: AccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM account WHERE id = ?', [id], 'get') as AccountRow | undefined;
- if (!row) {
- return null;
- }
-
- let xAccount: XAccount | null = null;
- let blueskyAccount: BlueskyAccount | null = null;
- switch (row.type) {
- case "X":
- if (row.xAccountId) {
- xAccount = getXAccount(row.xAccountId);
- }
- break;
-
- case "Bluesky":
- if (row.blueskyAccountID) {
- blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
- }
- break;
- }
-
- return {
- id: row.id,
- type: row.type,
- sortOrder: row.sortOrder,
- xAccount: xAccount,
- blueskyAccount: blueskyAccount,
- uuid: row.uuid
- };
-}
-
-export async function getAccountUsername(account: Account): Promise {
- if (account.type == "X" && account.xAccount) {
- return account.xAccount?.username;
- } else if (account.type == "Bluesky" && account.blueskyAccount) {
- return account.blueskyAccount?.username;
- }
-
- return null;
-}
-
-export const getAccounts = (): Account[] => {
- const rows: AccountRow[] = exec(getMainDatabase(), 'SELECT * FROM account', [], 'all') as AccountRow[];
-
- const accounts: Account[] = [];
- for (const row of rows) {
- let xAccount: XAccount | null = null;
- let blueskyAccount: BlueskyAccount | null = null;
- switch (row.type) {
- case "X":
- if (row.xAccountId) {
- xAccount = getXAccount(row.xAccountId);
- }
- break;
- case "Bluesky":
- if (row.blueskyAccountID) {
- blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
- }
- break
- }
-
- accounts.push({
- id: row.id,
- type: row.type,
- sortOrder: row.sortOrder,
- xAccount: xAccount,
- blueskyAccount: blueskyAccount,
- uuid: row.uuid
- });
- }
- return accounts;
-}
-
-export const createAccount = (): Account => {
- // Figure out the sortOrder for the new account
- const row: { maxSortOrder: number } = exec(getMainDatabase(), 'SELECT MAX(sortOrder) as maxSortOrder FROM account', [], 'get') as { maxSortOrder: number };
- const sortOrder = row.maxSortOrder ? row.maxSortOrder + 1 : 0;
-
- // Insert it
- const accountUUID = crypto.randomUUID();
- const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO account (sortOrder, uuid) VALUES (?, ?)', [sortOrder, accountUUID]) as Sqlite3Info;
-
- // Return it
- const account = getAccount(info.lastInsertRowid);
- if (!account) {
- throw new Error("Failed to create account");
- }
- return account;
-}
-
-// Set account.type to type, create a new account of that type (right now, just xAccount), and return the account
-export const selectAccountType = (accountID: number, type: string): Account => {
- // Get the account
- const account = getAccount(accountID);
- if (!account) {
- throw new Error("Account not found");
- }
- if (account.type != "unknown") {
- throw new Error("Account already has a type");
- }
-
- // Create the new account type
- switch (type) {
- case "X":
- account.xAccount = createXAccount();
- break;
- case "Bluesky":
- account.blueskyAccount = createBlueskyAccount();
- break;
- default:
- throw new Error("Unknown account type");
- }
-
- const xAccountId = account.xAccount ? account.xAccount.id : null;
- const blueskyAccountID = account.blueskyAccount ? account.blueskyAccount.id : null;
-
- // Update the account
- exec(getMainDatabase(), `
- UPDATE account
- SET
- type = ?,
- xAccountId = ?,
- blueskyAccountID = ?
- WHERE id = ?
- `, [
- type,
- xAccountId,
- blueskyAccountID,
- account.id
- ]);
-
- account.type = type;
- return account;
-}
-
-// Update the account based on account.id
-export const saveAccount = (account: Account) => {
- if (account.xAccount) {
- saveXAccount(account.xAccount);
- }
- else if (account.blueskyAccount) {
- saveBlueskyAccount(account.blueskyAccount);
- }
-
- exec(getMainDatabase(), `
- UPDATE account
- SET
- type = ?,
- sortOrder = ?
- WHERE id = ?
- `, [
- account.type,
- account.sortOrder,
- account.id
- ]);
-}
-
-export const deleteAccount = (accountID: number) => {
- // Get the account
- const account = getAccount(accountID);
- if (!account) {
- throw new Error("Account not found");
- }
-
- // Delete the account type
- switch (account.type) {
- case "X":
- if (account.xAccount) {
- exec(getMainDatabase(), 'DELETE FROM xAccount WHERE id = ?', [account.xAccount.id]);
- }
- break;
- case "Bluesky":
- if (account.blueskyAccount) {
- exec(getMainDatabase(), 'DELETE FROM blueskyAccount WHERE id = ?', [account.blueskyAccount.id]);
- }
- break;
- }
-
- // Delete the account
- exec(getMainDatabase(), 'DELETE FROM account WHERE id = ?', [accountID]);
-}
-
-export const defineIPCDatabase = () => {
- ipcMain.handle('database:getConfig', async (_, key) => {
- try {
- return getConfig(key);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:setConfig', async (_, key, value) => {
- try {
- setConfig(key, value);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:getErrorReport', async (_, id): Promise => {
- try {
- return getErrorReport(id);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:getNewErrorReports', async (_, accountID: number): Promise => {
- try {
- return getNewErrorReports(accountID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:createErrorReport', async (_, accountID: number, accountType: string, errorReportType: string, errorReportData: string, accountUsername: string | null, screenshotDataURI: string | null, sensitiveContextData: string | null): Promise => {
- try {
- createErrorReport(accountID, accountType, errorReportType, errorReportData, accountUsername, screenshotDataURI, sensitiveContextData);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:updateErrorReportSubmitted', async (_, id): Promise => {
- try {
- updateErrorReportSubmitted(id);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:dismissNewErrorReports', async (_, accountID: number): Promise => {
- try {
- dismissNewErrorReports(accountID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:getAccount', async (_, accountID): Promise => {
- try {
- return getAccount(accountID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:getAccounts', async (_): Promise => {
- try {
- return getAccounts();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:createAccount', async (_) => {
- try {
- return createAccount();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:selectAccountType', async (_, accountID, type) => {
- try {
- return selectAccountType(accountID, type);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:saveAccount', async (_, accountJson) => {
- try {
- const account = JSON.parse(accountJson);
- return saveAccount(account);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('database:deleteAccount', async (_, accountID) => {
- try {
- const ses = session.fromPartition(`persist:account-${accountID}`);
- await ses.closeAllConnections();
- await ses.clearStorageData();
- deleteAccount(accountID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-}
\ No newline at end of file
diff --git a/src/database/account.ts b/src/database/account.ts
new file mode 100644
index 00000000..d5eb5b20
--- /dev/null
+++ b/src/database/account.ts
@@ -0,0 +1,259 @@
+import { ipcMain, session } from 'electron';
+
+import { exec, getMainDatabase, Sqlite3Info } from './common';
+import { createXAccount, getXAccount, saveXAccount } from './x_account';
+import { createBlueskyAccount, getBlueskyAccount, saveBlueskyAccount } from './bluesky_account';
+import { Account, XAccount, BlueskyAccount } from '../shared_types'
+import { packageExceptionForReport } from "../util"
+
+// Types
+
+interface AccountRow {
+ id: number;
+ type: string;
+ sortOrder: number;
+ xAccountId: number | null;
+ blueskyAccountID: number | null;
+ uuid: string;
+}
+
+// Functions
+
+export const getAccount = (id: number): Account | null => {
+ const row: AccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM account WHERE id = ?', [id], 'get') as AccountRow | undefined;
+ if (!row) {
+ return null;
+ }
+
+ let xAccount: XAccount | null = null;
+ let blueskyAccount: BlueskyAccount | null = null;
+ switch (row.type) {
+ case "X":
+ if (row.xAccountId) {
+ xAccount = getXAccount(row.xAccountId);
+ }
+ break;
+
+ case "Bluesky":
+ if (row.blueskyAccountID) {
+ blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
+ }
+ break;
+ }
+
+ return {
+ id: row.id,
+ type: row.type,
+ sortOrder: row.sortOrder,
+ xAccount: xAccount,
+ blueskyAccount: blueskyAccount,
+ uuid: row.uuid
+ };
+}
+
+export async function getAccountUsername(account: Account): Promise {
+ if (account.type == "X" && account.xAccount) {
+ return account.xAccount?.username;
+ } else if (account.type == "Bluesky" && account.blueskyAccount) {
+ return account.blueskyAccount?.username;
+ }
+
+ return null;
+}
+
+export const getAccounts = (): Account[] => {
+ const rows: AccountRow[] = exec(getMainDatabase(), 'SELECT * FROM account', [], 'all') as AccountRow[];
+
+ const accounts: Account[] = [];
+ for (const row of rows) {
+ let xAccount: XAccount | null = null;
+ let blueskyAccount: BlueskyAccount | null = null;
+ switch (row.type) {
+ case "X":
+ if (row.xAccountId) {
+ xAccount = getXAccount(row.xAccountId);
+ }
+ break;
+ case "Bluesky":
+ if (row.blueskyAccountID) {
+ blueskyAccount = getBlueskyAccount(row.blueskyAccountID);
+ }
+ break
+ }
+
+ accounts.push({
+ id: row.id,
+ type: row.type,
+ sortOrder: row.sortOrder,
+ xAccount: xAccount,
+ blueskyAccount: blueskyAccount,
+ uuid: row.uuid
+ });
+ }
+ return accounts;
+}
+
+export const createAccount = (): Account => {
+ // Figure out the sortOrder for the new account
+ const row: { maxSortOrder: number } = exec(getMainDatabase(), 'SELECT MAX(sortOrder) as maxSortOrder FROM account', [], 'get') as { maxSortOrder: number };
+ const sortOrder = row.maxSortOrder ? row.maxSortOrder + 1 : 0;
+
+ // Insert it
+ const accountUUID = crypto.randomUUID();
+ const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO account (sortOrder, uuid) VALUES (?, ?)', [sortOrder, accountUUID]) as Sqlite3Info;
+
+ // Return it
+ const account = getAccount(info.lastInsertRowid);
+ if (!account) {
+ throw new Error("Failed to create account");
+ }
+ return account;
+}
+
+// Set account.type to type, create a new account of that type (right now, just xAccount), and return the account
+export const selectAccountType = (accountID: number, type: string): Account => {
+ // Get the account
+ const account = getAccount(accountID);
+ if (!account) {
+ throw new Error("Account not found");
+ }
+ if (account.type != "unknown") {
+ throw new Error("Account already has a type");
+ }
+
+ // Create the new account type
+ switch (type) {
+ case "X":
+ account.xAccount = createXAccount();
+ break;
+ case "Bluesky":
+ account.blueskyAccount = createBlueskyAccount();
+ break;
+ default:
+ throw new Error("Unknown account type");
+ }
+
+ const xAccountId = account.xAccount ? account.xAccount.id : null;
+ const blueskyAccountID = account.blueskyAccount ? account.blueskyAccount.id : null;
+
+ // Update the account
+ exec(getMainDatabase(), `
+ UPDATE account
+ SET
+ type = ?,
+ xAccountId = ?,
+ blueskyAccountID = ?
+ WHERE id = ?
+ `, [
+ type,
+ xAccountId,
+ blueskyAccountID,
+ account.id
+ ]);
+
+ account.type = type;
+ return account;
+}
+
+// Update the account based on account.id
+export const saveAccount = (account: Account) => {
+ if (account.xAccount) {
+ saveXAccount(account.xAccount);
+ }
+ else if (account.blueskyAccount) {
+ saveBlueskyAccount(account.blueskyAccount);
+ }
+
+ exec(getMainDatabase(), `
+ UPDATE account
+ SET
+ type = ?,
+ sortOrder = ?
+ WHERE id = ?
+ `, [
+ account.type,
+ account.sortOrder,
+ account.id
+ ]);
+}
+
+export const deleteAccount = (accountID: number) => {
+ // Get the account
+ const account = getAccount(accountID);
+ if (!account) {
+ throw new Error("Account not found");
+ }
+
+ // Delete the account type
+ switch (account.type) {
+ case "X":
+ if (account.xAccount) {
+ exec(getMainDatabase(), 'DELETE FROM xAccount WHERE id = ?', [account.xAccount.id]);
+ }
+ break;
+ case "Bluesky":
+ if (account.blueskyAccount) {
+ exec(getMainDatabase(), 'DELETE FROM blueskyAccount WHERE id = ?', [account.blueskyAccount.id]);
+ }
+ break;
+ }
+
+ // Delete the account
+ exec(getMainDatabase(), 'DELETE FROM account WHERE id = ?', [accountID]);
+}
+
+// IPC
+
+export const defineIPCDatabaseAccount = () => {
+ ipcMain.handle('database:getAccount', async (_, accountID): Promise => {
+ try {
+ return getAccount(accountID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:getAccounts', async (_): Promise => {
+ try {
+ return getAccounts();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:createAccount', async (_) => {
+ try {
+ return createAccount();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:selectAccountType', async (_, accountID, type) => {
+ try {
+ return selectAccountType(accountID, type);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:saveAccount', async (_, accountJson) => {
+ try {
+ const account = JSON.parse(accountJson);
+ return saveAccount(account);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:deleteAccount', async (_, accountID) => {
+ try {
+ const ses = session.fromPartition(`persist:account-${accountID}`);
+ await ses.closeAllConnections();
+ await ses.clearStorageData();
+ deleteAccount(accountID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/database/bluesky_account.ts b/src/database/bluesky_account.ts
new file mode 100644
index 00000000..fff67ff1
--- /dev/null
+++ b/src/database/bluesky_account.ts
@@ -0,0 +1,186 @@
+import { exec, getMainDatabase, Sqlite3Info } from './common';
+import { BlueskyAccount } from '../shared_types'
+
+// Types
+
+export interface BlueskyAccountRow {
+ id: number;
+ createdAt: string;
+ updatedAt: string;
+ accessedAt: string;
+ username: string;
+ profileImageDataURI: string;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archivePosts: boolean;
+ archivePostsHTML: boolean;
+ archiveLikes: boolean;
+ deletePosts: boolean;
+ deletePostsDaysOldEnabled: boolean;
+ deletePostsDaysOld: number;
+ deletePostsLikesThresholdEnabled: boolean;
+ deletePostsLikesThreshold: number;
+ deletePostsRepostsThresholdEnabled: boolean;
+ deletePostsRepostsThreshold: number;
+ deleteReposts: boolean;
+ deleteRepostsDaysOldEnabled: boolean;
+ deleteRepostsDaysOld: number;
+ deleteLikes: boolean;
+ deleteLikesDaysOldEnabled: boolean;
+ deleteLikesDaysOld: number;
+ followingCount: number;
+ followersCount: number;
+ postsCount: number;
+ likesCount: number;
+}
+
+// Functions
+
+// Get a single Bluesky account by ID
+export const getBlueskyAccount = (id: number): BlueskyAccount | null => {
+ const row: BlueskyAccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount WHERE id = ?', [id], 'get') as BlueskyAccountRow | undefined;
+ if (!row) {
+ return null;
+ }
+ return {
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archivePosts: !!row.archivePosts,
+ archivePostsHTML: !!row.archivePostsHTML,
+ archiveLikes: !!row.archiveLikes,
+ deletePosts: !!row.deletePosts,
+ deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
+ deletePostsDaysOld: row.deletePostsDaysOld,
+ deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
+ deletePostsLikesThreshold: row.deletePostsLikesThreshold,
+ deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
+ deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
+ deleteReposts: !!row.deleteReposts,
+ deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
+ deleteRepostsDaysOld: row.deleteRepostsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
+ deleteLikesDaysOld: row.deleteLikesDaysOld,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ postsCount: row.postsCount,
+ likesCount: row.likesCount
+ };
+}
+
+// Get all Bluesky accounts
+export const getBlueskyAccounts = (): BlueskyAccount[] => {
+ const rows: BlueskyAccountRow[] = exec(getMainDatabase(), 'SELECT * FROM blueskyAccount', [], 'all') as BlueskyAccountRow[];
+
+ const accounts: BlueskyAccount[] = [];
+ for (const row of rows) {
+ accounts.push({
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archivePosts: !!row.archivePosts,
+ archivePostsHTML: !!row.archivePostsHTML,
+ archiveLikes: !!row.archiveLikes,
+ deletePosts: !!row.deletePosts,
+ deletePostsDaysOldEnabled: !!row.deletePostsDaysOldEnabled,
+ deletePostsDaysOld: row.deletePostsDaysOld,
+ deletePostsLikesThresholdEnabled: !!row.deletePostsLikesThresholdEnabled,
+ deletePostsLikesThreshold: row.deletePostsLikesThreshold,
+ deletePostsRepostsThresholdEnabled: !!row.deletePostsRepostsThresholdEnabled,
+ deletePostsRepostsThreshold: row.deletePostsRepostsThreshold,
+ deleteReposts: !!row.deleteReposts,
+ deleteRepostsDaysOldEnabled: !!row.deleteRepostsDaysOldEnabled,
+ deleteRepostsDaysOld: row.deleteRepostsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteLikesDaysOldEnabled: !!row.deleteLikesDaysOldEnabled,
+ deleteLikesDaysOld: row.deleteLikesDaysOld,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ postsCount: row.postsCount,
+ likesCount: row.likesCount
+ });
+ }
+ return accounts;
+}
+
+// Create a new Bluesky account
+export const createBlueskyAccount = (): BlueskyAccount => {
+ const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO blueskyAccount DEFAULT VALUES') as Sqlite3Info;
+ const account = getBlueskyAccount(info.lastInsertRowid);
+ if (!account) {
+ throw new Error("Failed to create account");
+ }
+ return account;
+}
+
+// Update the Bluesky account based on account.id
+export const saveBlueskyAccount = (account: BlueskyAccount) => {
+ exec(getMainDatabase(), `
+ UPDATE blueskyAccount
+ SET
+ updatedAt = CURRENT_TIMESTAMP,
+ accessedAt = CURRENT_TIMESTAMP,
+ username = ?,
+ profileImageDataURI = ?,
+ saveMyData = ?,
+ deleteMyData = ?,
+ archivePosts = ?,
+ archivePostsHTML = ?,
+ archiveLikes = ?,
+ deletePosts = ?,
+ deletePostsDaysOld = ?,
+ deletePostsDaysOldEnabled = ?,
+ deletePostsLikesThresholdEnabled = ?,
+ deletePostsLikesThreshold = ?,
+ deletePostsRepostsThresholdEnabled = ?,
+ deletePostsRepostsThreshold = ?,
+ deleteReposts = ?,
+ deleteRepostsDaysOldEnabled = ?,
+ deleteRepostsDaysOld = ?,
+ deleteLikes = ?,
+ deleteLikesDaysOldEnabled = ?,
+ deleteLikesDaysOld = ?,
+ followingCount = ?,
+ followersCount = ?,
+ postsCount = ?,
+ likesCount = ?
+ WHERE id = ?
+ `, [
+ account.username,
+ account.profileImageDataURI,
+ account.saveMyData ? 1 : 0,
+ account.deleteMyData ? 1 : 0,
+ account.archivePosts ? 1 : 0,
+ account.archivePostsHTML ? 1 : 0,
+ account.archiveLikes ? 1 : 0,
+ account.deletePosts ? 1 : 0,
+ account.deletePostsDaysOld,
+ account.deletePostsDaysOldEnabled ? 1 : 0,
+ account.deletePostsLikesThresholdEnabled ? 1 : 0,
+ account.deletePostsLikesThreshold,
+ account.deletePostsRepostsThresholdEnabled ? 1 : 0,
+ account.deletePostsRepostsThreshold,
+ account.deleteReposts ? 1 : 0,
+ account.deleteRepostsDaysOldEnabled ? 1 : 0,
+ account.deleteRepostsDaysOld,
+ account.deleteLikes ? 1 : 0,
+ account.deleteLikesDaysOldEnabled ? 1 : 0,
+ account.deleteLikesDaysOld,
+ account.followingCount,
+ account.followersCount,
+ account.postsCount,
+ account.likesCount,
+ account.id
+ ]);
+}
diff --git a/src/database/common.ts b/src/database/common.ts
new file mode 100644
index 00000000..361fc226
--- /dev/null
+++ b/src/database/common.ts
@@ -0,0 +1,105 @@
+import path from "path"
+
+import log from 'electron-log/main';
+import Database from 'better-sqlite3'
+
+import { getSettingsPath, packageExceptionForReport } from "../util"
+
+// Types
+
+export interface Sqlite3Info {
+ lastInsertRowid: number;
+ changes: number;
+}
+
+export interface Sqlite3Count {
+ count: number;
+}
+
+// Migrations
+
+export type Migration = {
+ name: string;
+ sql: string[];
+};
+
+export const runMigrations = (db: Database.Database, migrations: Migration[]) => {
+ // Create a migrations table if necessary
+ const migrationsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'").get();
+ if (!migrationsTable) {
+ // Create the migrations table
+ log.debug("Creating migrations table");
+ db.prepare(`CREATE TABLE migrations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ runAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+)`).run();
+ }
+
+ // Apply the migrations in order
+ for (const migration of migrations) {
+ const migrationRecord = db.prepare("SELECT * FROM migrations WHERE name = ?").get(migration.name);
+ if (!migrationRecord) {
+ log.info(`Running migration: ${migration.name}`);
+ for (const sql of migration.sql) {
+ db.exec(sql);
+ }
+ db.prepare("INSERT INTO migrations (name) VALUES (?)").run(migration.name);
+ }
+ }
+}
+
+// Main database
+
+let mainDatabase: Database.Database | null = null;
+
+export const getMainDatabase = () => {
+ if (mainDatabase) {
+ return mainDatabase;
+ }
+
+ const dbPath = path.join(getSettingsPath(), 'db.sqlite');
+ mainDatabase = new Database(dbPath, {});
+ mainDatabase.pragma('journal_mode = WAL');
+ return mainDatabase;
+}
+
+export const closeMainDatabase = () => {
+ if (mainDatabase) {
+ mainDatabase.close();
+ mainDatabase = null;
+ }
+}
+
+// Utils
+
+export const exec = (db: Database.Database | null, sql: string, params: Array = [], cmd: 'run' | 'all' | 'get' = 'run') => {
+ if (!db) {
+ throw new Error("Database not initialized");
+ }
+
+ // Convert Date objects to ISO strings
+ const paramsConverted: Array = [];
+ for (const param of params) {
+ if (param instanceof Date) {
+ paramsConverted.push(param.toISOString());
+ } else {
+ paramsConverted.push(param);
+ }
+ }
+
+ // Execute the query
+ log.debug("Executing SQL:", sql, "Params:", paramsConverted);
+ try {
+ const stmt = db.prepare(sql);
+ const ret = stmt[cmd](...paramsConverted);
+ return ret
+ } catch (error) {
+ const exception = JSON.parse(packageExceptionForReport(error as Error))
+ throw new Error(JSON.stringify({
+ exception: exception,
+ sql: sql,
+ params: paramsConverted
+ }));
+ }
+}
diff --git a/src/database/config.ts b/src/database/config.ts
new file mode 100644
index 00000000..22150854
--- /dev/null
+++ b/src/database/config.ts
@@ -0,0 +1,49 @@
+import Database from 'better-sqlite3'
+import { ipcMain } from 'electron';
+
+import { exec, getMainDatabase } from './common';
+import { packageExceptionForReport } from "../util"
+
+// Types
+
+interface ConfigRow {
+ key: string;
+ value: string;
+}
+
+// Functions
+
+export const getConfig = (key: string, db: Database.Database | null = null): string | null => {
+ if (!db) {
+ db = getMainDatabase();
+ }
+ const row: ConfigRow | undefined = exec(db, 'SELECT value FROM config WHERE key = ?', [key], 'get') as ConfigRow | undefined;
+ return row ? row.value : null;
+}
+
+export const setConfig = (key: string, value: string, db: Database.Database | null = null) => {
+ if (!db) {
+ db = getMainDatabase();
+ }
+ exec(db, 'INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', [key, value]);
+}
+
+// IPC
+
+export const defineIPCDatabaseConfig = () => {
+ ipcMain.handle('database:getConfig', async (_, key) => {
+ try {
+ return getConfig(key);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:setConfig', async (_, key, value) => {
+ try {
+ setConfig(key, value);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/database/error_report.ts b/src/database/error_report.ts
new file mode 100644
index 00000000..3cae53e1
--- /dev/null
+++ b/src/database/error_report.ts
@@ -0,0 +1,156 @@
+import os from 'os';
+
+import { app, ipcMain } from 'electron';
+
+import { exec, getMainDatabase, Sqlite3Info } from './common';
+import { ErrorReport } from '../shared_types'
+import { packageExceptionForReport } from "../util"
+
+// Types
+
+export interface ErrorReportRow {
+ id: number;
+ createdAt: string;
+ accountID: number;
+ appVersion: string;
+ clientPlatform: string;
+ accountType: string;
+ errorReportType: string;
+ errorReportData: string;
+ accountUsername: string;
+ screenshotDataURI: string;
+ sensitiveContextData: string;
+ status: string; // "new", "submitted", and "dismissed"
+}
+
+// Functions
+
+export const getErrorReport = (id: number): ErrorReport | null => {
+ const row: ErrorReportRow | undefined = exec(getMainDatabase(), 'SELECT * FROM errorReport WHERE id = ?', [id], 'get') as ErrorReportRow | undefined;
+ if (!row) {
+ return null;
+ }
+ return {
+ id: row.id,
+ createdAt: row.createdAt,
+ accountID: row.accountID,
+ appVersion: row.appVersion,
+ clientPlatform: row.clientPlatform,
+ accountType: row.accountType,
+ errorReportType: row.errorReportType,
+ errorReportData: row.errorReportData,
+ accountUsername: row.accountUsername,
+ screenshotDataURI: row.screenshotDataURI,
+ sensitiveContextData: row.sensitiveContextData,
+ status: row.status
+ }
+}
+
+export const getNewErrorReports = (accountID: number): ErrorReport[] => {
+ const rows: ErrorReportRow[] = exec(getMainDatabase(), 'SELECT * FROM errorReport WHERE accountID = ? AND status = ?', [accountID, 'new'], 'all') as ErrorReportRow[];
+ const errorReports: ErrorReport[] = [];
+ for (const row of rows) {
+ errorReports.push({
+ id: row.id,
+ createdAt: row.createdAt,
+ accountID: row.accountID,
+ appVersion: row.appVersion,
+ clientPlatform: row.clientPlatform,
+ accountType: row.accountType,
+ errorReportType: row.errorReportType,
+ errorReportData: row.errorReportData,
+ accountUsername: row.accountUsername,
+ screenshotDataURI: row.screenshotDataURI,
+ sensitiveContextData: row.sensitiveContextData,
+ status: row.status
+ });
+ }
+ return errorReports;
+}
+
+export const createErrorReport = (accountID: number, accountType: string, errorReportType: string, errorReportData: string, accountUsername: string | null, screenshotDataURI: string | null, sensitiveContextData: string | null) => {
+ const info: Sqlite3Info = exec(getMainDatabase(), `
+ INSERT INTO errorReport (
+ accountID,
+ appVersion,
+ clientPlatform,
+ accountType,
+ errorReportType,
+ errorReportData,
+ accountUsername,
+ screenshotDataURI,
+ sensitiveContextData
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [
+ accountID,
+ app.getVersion(),
+ os.platform(),
+ accountType,
+ errorReportType,
+ errorReportData,
+ accountUsername,
+ screenshotDataURI,
+ sensitiveContextData
+ ]) as Sqlite3Info;
+ const report = getErrorReport(info.lastInsertRowid);
+ if (!report) {
+ throw new Error("Failed to create error report");
+ }
+}
+
+export const updateErrorReportSubmitted = (id: number) => {
+ exec(getMainDatabase(), 'DELETE FROM errorReport WHERE id = ?', [id]);
+}
+
+export const dismissNewErrorReports = (accountID: number) => {
+ exec(getMainDatabase(), 'DELETE FROM errorReport WHERE accountID = ? AND status = ?', [accountID, 'new']);
+}
+
+export const dismissAllNewErrorReports = () => {
+ exec(getMainDatabase(), 'DELETE FROM errorReport WHERE status = ?', ['new']);
+}
+
+// IPC
+
+export const defineIPCDatabaseErrorReport = () => {
+ ipcMain.handle('database:getErrorReport', async (_, id): Promise => {
+ try {
+ return getErrorReport(id);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:getNewErrorReports', async (_, accountID: number): Promise => {
+ try {
+ return getNewErrorReports(accountID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:createErrorReport', async (_, accountID: number, accountType: string, errorReportType: string, errorReportData: string, accountUsername: string | null, screenshotDataURI: string | null, sensitiveContextData: string | null): Promise => {
+ try {
+ createErrorReport(accountID, accountType, errorReportType, errorReportData, accountUsername, screenshotDataURI, sensitiveContextData);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:updateErrorReportSubmitted', async (_, id): Promise => {
+ try {
+ updateErrorReportSubmitted(id);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('database:dismissNewErrorReports', async (_, accountID: number): Promise => {
+ try {
+ dismissNewErrorReports(accountID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/database/index.ts b/src/database/index.ts
new file mode 100644
index 00000000..43b2af74
--- /dev/null
+++ b/src/database/index.ts
@@ -0,0 +1,9 @@
+export * from './common';
+export * from './migrations';
+
+export * from './config';
+export * from './error_report';
+
+export * from './account';
+export * from './x_account';
+export * from './bluesky_account';
diff --git a/src/database/migrations.ts b/src/database/migrations.ts
new file mode 100644
index 00000000..3abe8afa
--- /dev/null
+++ b/src/database/migrations.ts
@@ -0,0 +1,147 @@
+import { runMigrations, getMainDatabase } from "./common";
+
+export const runMainMigrations = () => {
+ runMigrations(getMainDatabase(), [
+ // Create the tables
+ {
+ name: "initial",
+ sql: [
+ `CREATE TABLE config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ key TEXT NOT NULL UNIQUE,
+ value TEXT NOT NULL
+);`, `CREATE TABLE account (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ type TEXT NOT NULL DEFAULT 'unknown',
+ sortOrder INTEGER NOT NULL DEFAULT 0,
+ xAccountId INTEGER DEFAULT NULL,
+ uuid TEXT NOT NULL
+);`, `CREATE TABLE xAccount (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ accessedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ username TEXT,
+ profileImageDataURI TEXT,
+ saveMyData BOOLEAN DEFAULT 1,
+ deleteMyData BOOLEAN DEFAULT 0,
+ archiveTweets BOOLEAN DEFAULT 1,
+ archiveTweetsHTML BOOLEAN DEFAULT 0,
+ archiveLikes BOOLEAN DEFAULT 1,
+ archiveDMs BOOLEAN DEFAULT 1,
+ deleteTweets BOOLEAN DEFAULT 1,
+ deleteTweetsDaysOld INTEGER DEFAULT 0,
+ deleteTweetsLikesThresholdEnabled BOOLEAN DEFAULT 0,
+ deleteTweetsLikesThreshold INTEGER DEFAULT 20,
+ deleteTweetsRetweetsThresholdEnabled BOOLEAN DEFAULT 0,
+ deleteTweetsRetweetsThreshold INTEGER DEFAULT 20,
+ deleteRetweets BOOLEAN DEFAULT 1,
+ deleteRetweetsDaysOld INTEGER DEFAULT 0,
+ deleteLikes BOOLEAN DEFAULT 0,
+ deleteLikesDaysOld INTEGER DEFAULT 0,
+ deleteDMs BOOLEAN DEFAULT 0
+);`,
+ ]
+ },
+ // Add importFromArchive, followingCount, follwersCount, tweetsCount, likesCount to xAccount
+ {
+ name: "add importFromArchive, followingCount, follwersCount, tweetsCount, likesCount to xAccount",
+ sql: [
+ `ALTER TABLE xAccount ADD COLUMN importFromArchive BOOLEAN DEFAULT 1;`,
+ `ALTER TABLE xAccount ADD COLUMN followingCount INTEGER DEFAULT 0;`,
+ `ALTER TABLE xAccount ADD COLUMN followersCount INTEGER DEFAULT 0;`,
+ `ALTER TABLE xAccount ADD COLUMN tweetsCount INTEGER DEFAULT -1;`,
+ `ALTER TABLE xAccount ADD COLUMN likesCount INTEGER DEFAULT -1;`,
+ ]
+ },
+ // Add unfollowEveryone to xAccount
+ {
+ name: "add unfollowEveryone to xAccount",
+ sql: [
+ `ALTER TABLE xAccount ADD COLUMN unfollowEveryone BOOLEAN DEFAULT 1;`,
+ ]
+ },
+ // Add deleteTweetsDaysOldEnabled, deleteRetweetsDaysOldEnabled, deleteLikesDaysOldEnabled to xAccount
+ {
+ name: "add deleteTweetsDaysOldEnabled, deleteRetweetsDaysOldEnabled, deleteLikesDaysOldEnabled to xAccount",
+ sql: [
+ `ALTER TABLE xAccount ADD COLUMN deleteTweetsDaysOldEnabled BOOLEAN DEFAULT 0;`,
+ `ALTER TABLE xAccount ADD COLUMN deleteRetweetsDaysOldEnabled BOOLEAN DEFAULT 0;`,
+ `ALTER TABLE xAccount ADD COLUMN deleteLikesDaysOldEnabled BOOLEAN DEFAULT 0;`,
+ ]
+ },
+ // Add errorReport table. Status can be "new", "submitted", and "dismissed"
+ {
+ name: "add errorReport table",
+ sql: [
+ `CREATE TABLE errorReport (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ accountID INTEGER DEFAULT NULL,
+ appVersion TEXT DEFAULT NULL,
+ clientPlatform TEXT DEFAULT NULL,
+ accountType TEXT DEFAULT NULL,
+ errorReportType TEXT NOT NULL,
+ errorReportData TEXT DEFAULT NULL,
+ accountUsername TEXT DEFAULT NULL,
+ screenshotDataURI TEXT DEFAULT NULL,
+ sensitiveContextData TEXT DEFAULT NULL,
+ status TEXT DEFAULT 'new'
+);`,
+ ]
+ },
+ // Add archiveMyData to xAccount
+ {
+ name: "add archiveMyData to xAccount",
+ sql: [
+ `ALTER TABLE xAccount ADD COLUMN archiveMyData BOOLEAN DEFAULT 0;`,
+ ]
+ },
+ // Add archiveBookmarks, deleteBookmarks to xAccount
+ {
+ name: "add archiveBookmarks, deleteBookmarks to xAccount",
+ sql: [
+ `ALTER TABLE xAccount ADD COLUMN archiveBookmarks BOOLEAN DEFAULT 1;`,
+ `ALTER TABLE xAccount ADD COLUMN deleteBookmarks BOOLEAN DEFAULT 0;`,
+ ]
+ },
+ // Add Bluesky table, and blueskyAccountID to account
+ {
+ name: "add Bluesky table, and blueskyAccountID to account",
+ sql: [
+ `CREATE TABLE blueskyAccount (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ accessedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ username TEXT,
+ profileImageDataURI TEXT,
+ saveMyData BOOLEAN DEFAULT 1,
+ deleteMyData BOOLEAN DEFAULT 0,
+ archivePosts BOOLEAN DEFAULT 1,
+ archivePostsHTML BOOLEAN DEFAULT 0,
+ archiveLikes BOOLEAN DEFAULT 1,
+ deletePosts BOOLEAN DEFAULT 1,
+ deletePostsDaysOldEnabled BOOLEAN DEFAULT 0,
+ deletePostsDaysOld INTEGER DEFAULT 0,
+ deletePostsLikesThresholdEnabled BOOLEAN DEFAULT 0,
+ deletePostsLikesThreshold INTEGER DEFAULT 20,
+ deletePostsRepostsThresholdEnabled BOOLEAN DEFAULT 0,
+ deletePostsRepostsThreshold INTEGER DEFAULT 20,
+ deleteReposts BOOLEAN DEFAULT 1,
+ deleteRepostsDaysOldEnabled BOOLEAN DEFAULT 0,
+ deleteRepostsDaysOld INTEGER DEFAULT 0,
+ deleteLikes BOOLEAN DEFAULT 0,
+ deleteLikesDaysOldEnabled BOOLEAN DEFAULT 0,
+ deleteLikesDaysOld INTEGER DEFAULT 0,
+ unfollowEveryone BOOLEAN DEFAULT 1,
+ followingCount INTEGER DEFAULT 0,
+ followersCount INTEGER DEFAULT 0,
+ postsCount INTEGER DEFAULT -1,
+ likesCount INTEGER DEFAULT -1
+);`,
+ `ALTER TABLE account ADD COLUMN blueskyAccountID INTEGER DEFAULT NULL;`,
+ ]
+ },
+ ]);
+}
\ No newline at end of file
diff --git a/src/database/x_account.ts b/src/database/x_account.ts
new file mode 100644
index 00000000..4885bd87
--- /dev/null
+++ b/src/database/x_account.ts
@@ -0,0 +1,208 @@
+import { exec, getMainDatabase, Sqlite3Info } from './common';
+import { XAccount } from '../shared_types'
+
+// Types
+
+interface XAccountRow {
+ id: number;
+ createdAt: string;
+ updatedAt: string;
+ accessedAt: string;
+ username: string;
+ profileImageDataURI: string;
+ importFromArchive: boolean;
+ saveMyData: boolean;
+ deleteMyData: boolean;
+ archiveMyData: boolean;
+ archiveTweets: boolean;
+ archiveTweetsHTML: boolean;
+ archiveLikes: boolean;
+ archiveBookmarks: boolean;
+ archiveDMs: boolean;
+ deleteTweets: boolean;
+ deleteTweetsDaysOldEnabled: boolean;
+ deleteTweetsDaysOld: number;
+ deleteTweetsLikesThresholdEnabled: boolean;
+ deleteTweetsLikesThreshold: number;
+ deleteTweetsRetweetsThresholdEnabled: boolean;
+ deleteTweetsRetweetsThreshold: number;
+ deleteRetweets: boolean;
+ deleteRetweetsDaysOldEnabled: boolean;
+ deleteRetweetsDaysOld: number;
+ deleteLikes: boolean;
+ deleteBookmarks: number;
+ deleteDMs: boolean;
+ unfollowEveryone: boolean;
+ followingCount: number;
+ followersCount: number;
+ tweetsCount: number;
+ likesCount: number;
+}
+
+// Functions
+
+export const getXAccount = (id: number): XAccount | null => {
+ const row: XAccountRow | undefined = exec(getMainDatabase(), 'SELECT * FROM xAccount WHERE id = ?', [id], 'get') as XAccountRow | undefined;
+ if (!row) {
+ return null;
+ }
+ return {
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ importFromArchive: !!row.importFromArchive,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archiveMyData: !!row.archiveMyData,
+ archiveTweets: !!row.archiveTweets,
+ archiveTweetsHTML: !!row.archiveTweetsHTML,
+ archiveLikes: !!row.archiveLikes,
+ archiveBookmarks: !!row.archiveBookmarks,
+ archiveDMs: !!row.archiveDMs,
+ deleteTweets: !!row.deleteTweets,
+ deleteTweetsDaysOldEnabled: !!row.deleteTweetsDaysOldEnabled,
+ deleteTweetsDaysOld: row.deleteTweetsDaysOld,
+ deleteTweetsLikesThresholdEnabled: !!row.deleteTweetsLikesThresholdEnabled,
+ deleteTweetsLikesThreshold: row.deleteTweetsLikesThreshold,
+ deleteTweetsRetweetsThresholdEnabled: !!row.deleteTweetsRetweetsThresholdEnabled,
+ deleteTweetsRetweetsThreshold: row.deleteTweetsRetweetsThreshold,
+ deleteRetweets: !!row.deleteRetweets,
+ deleteRetweetsDaysOldEnabled: !!row.deleteRetweetsDaysOldEnabled,
+ deleteRetweetsDaysOld: row.deleteRetweetsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteBookmarks: !!row.deleteBookmarks,
+ deleteDMs: !!row.deleteDMs,
+ unfollowEveryone: !!row.unfollowEveryone,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ tweetsCount: row.tweetsCount,
+ likesCount: row.likesCount
+ };
+}
+
+export const getXAccounts = (): XAccount[] => {
+ const rows: XAccountRow[] = exec(getMainDatabase(), 'SELECT * FROM xAccount', [], 'all') as XAccountRow[];
+
+ const accounts: XAccount[] = [];
+ for (const row of rows) {
+ accounts.push({
+ id: row.id,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt),
+ accessedAt: new Date(row.accessedAt),
+ username: row.username,
+ profileImageDataURI: row.profileImageDataURI,
+ importFromArchive: !!row.importFromArchive,
+ saveMyData: !!row.saveMyData,
+ deleteMyData: !!row.deleteMyData,
+ archiveMyData: !!row.archiveMyData,
+ archiveTweets: !!row.archiveTweets,
+ archiveTweetsHTML: !!row.archiveTweetsHTML,
+ archiveLikes: !!row.archiveLikes,
+ archiveBookmarks: !!row.archiveBookmarks,
+ archiveDMs: !!row.archiveDMs,
+ deleteTweets: !!row.deleteTweets,
+ deleteTweetsDaysOldEnabled: !!row.deleteTweetsDaysOldEnabled,
+ deleteTweetsDaysOld: row.deleteTweetsDaysOld,
+ deleteTweetsLikesThresholdEnabled: !!row.deleteTweetsLikesThresholdEnabled,
+ deleteTweetsLikesThreshold: row.deleteTweetsLikesThreshold,
+ deleteTweetsRetweetsThresholdEnabled: !!row.deleteTweetsRetweetsThresholdEnabled,
+ deleteTweetsRetweetsThreshold: row.deleteTweetsRetweetsThreshold,
+ deleteRetweets: !!row.deleteRetweets,
+ deleteRetweetsDaysOldEnabled: !!row.deleteRetweetsDaysOldEnabled,
+ deleteRetweetsDaysOld: row.deleteRetweetsDaysOld,
+ deleteLikes: !!row.deleteLikes,
+ deleteBookmarks: !!row.deleteBookmarks,
+ deleteDMs: !!row.deleteDMs,
+ unfollowEveryone: !!row.unfollowEveryone,
+ followingCount: row.followingCount,
+ followersCount: row.followersCount,
+ tweetsCount: row.tweetsCount,
+ likesCount: row.likesCount
+ });
+ }
+ return accounts;
+}
+
+export const createXAccount = (): XAccount => {
+ const info: Sqlite3Info = exec(getMainDatabase(), 'INSERT INTO xAccount DEFAULT VALUES') as Sqlite3Info;
+ const account = getXAccount(info.lastInsertRowid);
+ if (!account) {
+ throw new Error("Failed to create account");
+ }
+ return account;
+}
+
+// Update the account based on account.id
+export const saveXAccount = (account: XAccount) => {
+ exec(getMainDatabase(), `
+ UPDATE xAccount
+ SET
+ updatedAt = CURRENT_TIMESTAMP,
+ accessedAt = CURRENT_TIMESTAMP,
+ username = ?,
+ profileImageDataURI = ?,
+ importFromArchive = ?,
+ saveMyData = ?,
+ deleteMyData = ?,
+ archiveMyData = ?,
+ archiveTweets = ?,
+ archiveTweetsHTML = ?,
+ archiveLikes = ?,
+ archiveBookmarks = ?,
+ archiveDMs = ?,
+ deleteTweets = ?,
+ deleteTweetsDaysOld = ?,
+ deleteTweetsDaysOldEnabled = ?,
+ deleteTweetsLikesThresholdEnabled = ?,
+ deleteTweetsLikesThreshold = ?,
+ deleteTweetsRetweetsThresholdEnabled = ?,
+ deleteTweetsRetweetsThreshold = ?,
+ deleteRetweets = ?,
+ deleteRetweetsDaysOldEnabled = ?,
+ deleteRetweetsDaysOld = ?,
+ deleteLikes = ?,
+ deleteBookmarks = ?,
+ deleteDMs = ?,
+ unfollowEveryone = ?,
+ followingCount = ?,
+ followersCount = ?,
+ tweetsCount = ?,
+ likesCount = ?
+ WHERE id = ?
+ `, [
+ account.username,
+ account.profileImageDataURI,
+ account.importFromArchive ? 1 : 0,
+ account.saveMyData ? 1 : 0,
+ account.deleteMyData ? 1 : 0,
+ account.archiveMyData ? 1 : 0,
+ account.archiveTweets ? 1 : 0,
+ account.archiveTweetsHTML ? 1 : 0,
+ account.archiveLikes ? 1 : 0,
+ account.archiveBookmarks ? 1 : 0,
+ account.archiveDMs ? 1 : 0,
+ account.deleteTweets ? 1 : 0,
+ account.deleteTweetsDaysOld,
+ account.deleteTweetsDaysOldEnabled ? 1 : 0,
+ account.deleteTweetsLikesThresholdEnabled ? 1 : 0,
+ account.deleteTweetsLikesThreshold,
+ account.deleteTweetsRetweetsThresholdEnabled ? 1 : 0,
+ account.deleteTweetsRetweetsThreshold,
+ account.deleteRetweets ? 1 : 0,
+ account.deleteRetweetsDaysOldEnabled ? 1 : 0,
+ account.deleteRetweetsDaysOld,
+ account.deleteLikes ? 1 : 0,
+ account.deleteBookmarks ? 1 : 0,
+ account.deleteDMs ? 1 : 0,
+ account.unfollowEveryone ? 1 : 0,
+ account.followingCount,
+ account.followersCount,
+ account.tweetsCount,
+ account.likesCount,
+ account.id
+ ]);
+}
From 68665c3c86800f8ace28539ea26c474bdc905cf1 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 10:03:49 -0800
Subject: [PATCH 11/14] Define IPC in database again
---
src/database/index.ts | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/database/index.ts b/src/database/index.ts
index 43b2af74..d74bec2c 100644
--- a/src/database/index.ts
+++ b/src/database/index.ts
@@ -7,3 +7,15 @@ export * from './error_report';
export * from './account';
export * from './x_account';
export * from './bluesky_account';
+
+// IPC
+
+import { defineIPCDatabaseConfig } from './config';
+import { defineIPCDatabaseErrorReport } from './error_report';
+import { defineIPCDatabaseAccount } from './account';
+
+export const defineIPCDatabase = () => {
+ defineIPCDatabaseConfig();
+ defineIPCDatabaseErrorReport();
+ defineIPCDatabaseAccount();
+}
From 2d786230d8f04edbfc81912d1b7cbc2bb028acc5 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 10:21:11 -0800
Subject: [PATCH 12/14] Split x_account into multiple files
---
src/account_x/index.ts | 3 +
src/account_x/ipc.ts | 430 ++++++++++++++
.../types.ts} | 122 +++-
.../x_account_controller.ts} | 548 +-----------------
4 files changed, 571 insertions(+), 532 deletions(-)
create mode 100644 src/account_x/index.ts
create mode 100644 src/account_x/ipc.ts
rename src/{account_x_types.ts => account_x/types.ts} (79%)
rename src/{account_x.ts => account_x/x_account_controller.ts} (82%)
diff --git a/src/account_x/index.ts b/src/account_x/index.ts
new file mode 100644
index 00000000..151effcc
--- /dev/null
+++ b/src/account_x/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export * from './x_account_controller';
+export * from './ipc';
diff --git a/src/account_x/ipc.ts b/src/account_x/ipc.ts
new file mode 100644
index 00000000..3a0f7260
--- /dev/null
+++ b/src/account_x/ipc.ts
@@ -0,0 +1,430 @@
+import { ipcMain } from 'electron'
+
+import { XAccountController } from './x_account_controller';
+
+import {
+ XAccount,
+ XJob,
+ XProgress,
+ XArchiveStartResponse,
+ XRateLimitInfo,
+ XIndexMessagesStartResponse,
+ XDeleteTweetsStartResponse,
+ XProgressInfo,
+ ResponseData,
+ XDatabaseStats,
+ XDeleteReviewStats,
+ XArchiveInfo,
+ XImportArchiveResponse
+} from '../shared_types'
+import { getMITMController } from '../mitm';
+import { packageExceptionForReport } from '../util'
+
+const controllers: Record = {};
+
+const getXAccountController = (accountID: number): XAccountController => {
+ if (!controllers[accountID]) {
+ controllers[accountID] = new XAccountController(accountID, getMITMController(accountID));
+ }
+ controllers[accountID].refreshAccount();
+ return controllers[accountID];
+}
+
+export const defineIPCX = () => {
+ ipcMain.handle('X:resetProgress', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return controller.resetProgress();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:createJobs', async (_, accountID: number, jobTypes: string[]): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return controller.createJobs(jobTypes);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getLastFinishedJob', async (_, accountID: number, jobType: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return controller.getLastFinishedJob(jobType);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:updateJob', async (_, accountID: number, jobJSON: string) => {
+ try {
+ const controller = getXAccountController(accountID);
+ const job = JSON.parse(jobJSON) as XJob;
+ controller.updateJob(job);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexStart', async (_, accountID: number) => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.indexStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexStop', async (_, accountID: number) => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.indexStop();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexParseAllJSON', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexParseAllJSON();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexParseTweets', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexParseTweets();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexParseConversations', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexParseConversations();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexIsThereMore', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexIsThereMore();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:resetThereIsMore', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.resetThereIsMore();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexMessagesStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexMessagesStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexParseMessages', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.indexParseMessages();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:indexConversationFinished', async (_, accountID: number, conversationID: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.indexConversationFinished(conversationID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:archiveTweetsStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.archiveTweetsStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:archiveTweetsOutputPath', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.archiveTweetsOutputPath();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:archiveTweet', async (_, accountID: number, tweetID: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.archiveTweet(tweetID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:archiveTweetCheckDate', async (_, accountID: number, tweetID: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.archiveTweetCheckDate(tweetID);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:archiveBuild', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.archiveBuild();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:syncProgress', async (_, accountID: number, progressJSON: string) => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.syncProgress(progressJSON);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:openFolder', async (_, accountID: number, folderName: string) => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.openFolder(folderName);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getArchiveInfo', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getArchiveInfo();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:resetRateLimitInfo', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.resetRateLimitInfo();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:isRateLimited', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.isRateLimited();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getProgress', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getProgress();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getProgressInfo', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getProgressInfo();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getDatabaseStats', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getDatabaseStats();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getDeleteReviewStats', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getDeleteReviewStats();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:saveProfileImage', async (_, accountID: number, url: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.saveProfileImage(url);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getLatestResponseData', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getLatestResponseData();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteTweetsStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.deleteTweetsStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteTweetsCountNotArchived', async (_, accountID: number, total: boolean): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.deleteTweetsCountNotArchived(total);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteRetweetsStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.deleteRetweetsStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteLikesStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.deleteLikesStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteBookmarksStart', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.deleteBookmarksStart();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteTweet', async (_, accountID: number, tweetID: string, deleteType: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.deleteTweet(tweetID, deleteType);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteDMsMarkAllDeleted', async (_, accountID: number): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.deleteDMsMarkAllDeleted();
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:unzipXArchive', async (_, accountID: number, archivePath: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.unzipXArchive(archivePath);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ await controller.deleteUnzippedXArchive(archivePath);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:verifyXArchive', async (_, accountID: number, archivePath: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.verifyXArchive(archivePath);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:importXArchive', async (_, accountID: number, archivePath: string, dataType: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.importXArchive(archivePath, dataType);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getCookie', async (_, accountID: number, name: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getCookie(name);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:getConfig', async (_, accountID: number, key: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.getConfig(key);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+
+ ipcMain.handle('X:setConfig', async (_, accountID: number, key: string, value: string): Promise => {
+ try {
+ const controller = getXAccountController(accountID);
+ return await controller.setConfig(key, value);
+ } catch (error) {
+ throw new Error(packageExceptionForReport(error as Error));
+ }
+ });
+};
diff --git a/src/account_x_types.ts b/src/account_x/types.ts
similarity index 79%
rename from src/account_x_types.ts
rename to src/account_x/types.ts
index 5780166e..80c28970 100644
--- a/src/account_x_types.ts
+++ b/src/account_x/types.ts
@@ -1,5 +1,125 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+import { XJob, XTweetItem, XTweetItemArchive } from '../shared_types'
+
+// Models
+
+export interface XJobRow {
+ id: number;
+ jobType: string;
+ status: string;
+ scheduledAt: string;
+ startedAt: string | null;
+ finishedAt: string | null;
+ progressJSON: string | null;
+ error: string | null;
+}
+
+export interface XTweetRow {
+ id: number;
+ username: string;
+ tweetID: string;
+ conversationID: string;
+ createdAt: string;
+ likeCount: number;
+ quoteCount: number;
+ replyCount: number;
+ retweetCount: number;
+ isLiked: boolean;
+ isRetweeted: boolean;
+ isBookmarked: boolean;
+ text: string;
+ path: string;
+ addedToDatabaseAt: string;
+ archivedAt: string | null;
+ deletedTweetAt: string | null;
+ deletedRetweetAt: string | null;
+ deletedLikeAt: string | null;
+ deletedBookmarkAt: string | null;
+}
+
+export interface XUserRow {
+ id: number;
+ userID: string;
+ name: string | null;
+ screenName: string;
+ profileImageDataURI: string | null;
+}
+
+export interface XConversationRow {
+ id: number;
+ conversationID: string;
+ type: string;
+ sortTimestamp: string | null;
+ minEntryID: string | null;
+ maxEntryID: string | null;
+ isTrusted: boolean | null;
+ shouldIndexMessages: boolean | null;
+ addedToDatabaseAt: string;
+ updatedInDatabaseAt: string | null;
+ deletedAt: string | null;
+}
+
+export interface XConversationParticipantRow {
+ id: number;
+ conversationID: string;
+ userID: string;
+}
+
+export interface XMessageRow {
+ id: number;
+ messageID: string;
+ conversationID: string;
+ createdAt: string;
+ senderID: string;
+ text: string;
+ deletedAt: string | null;
+}
+
+// Converters
+
+export function convertXJobRowToXJob(row: XJobRow): XJob {
+ return {
+ id: row.id,
+ jobType: row.jobType,
+ status: row.status,
+ scheduledAt: new Date(row.scheduledAt),
+ startedAt: row.startedAt ? new Date(row.startedAt) : null,
+ finishedAt: row.finishedAt ? new Date(row.finishedAt) : null,
+ progressJSON: row.progressJSON ? JSON.parse(row.progressJSON) : null,
+ error: row.error,
+ };
+}
+
+export function convertTweetRowToXTweetItem(row: XTweetRow): XTweetItem {
+ return {
+ id: row.tweetID,
+ t: row.text,
+ l: row.likeCount,
+ r: row.retweetCount,
+ d: row.createdAt,
+ };
+}
+
+function formatDateToYYYYMMDD(dateString: string): string {
+ const date = new Date(dateString);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+}
+
+export function convertTweetRowToXTweetItemArchive(row: XTweetRow): XTweetItemArchive {
+ return {
+ url: `https://x.com/${row.path}`,
+ tweetID: row.tweetID,
+ basename: `${formatDateToYYYYMMDD(row.createdAt)}_${row.tweetID}`,
+ username: row.username
+ };
+}
+
+// X API types
+
// Index tweets
export interface XAPILegacyTweet {
@@ -424,4 +544,4 @@ export interface XArchiveDMConversation {
conversationId: string,
messages: XArchiveDMMessage[],
}
-}
\ No newline at end of file
+}
diff --git a/src/account_x.ts b/src/account_x/x_account_controller.ts
similarity index 82%
rename from src/account_x.ts
rename to src/account_x/x_account_controller.ts
index c60974c4..6a35ffa8 100644
--- a/src/account_x.ts
+++ b/src/account_x/x_account_controller.ts
@@ -5,7 +5,7 @@ import os from 'os'
import fetch from 'node-fetch';
import unzipper from 'unzipper';
-import { app, ipcMain, session, shell } from 'electron'
+import { app, session, shell } from 'electron'
import log from 'electron-log/main';
import Database from 'better-sqlite3'
import { glob } from 'glob';
@@ -13,14 +13,12 @@ import { glob } from 'glob';
import {
getResourcesPath,
getAccountDataPath,
- packageExceptionForReport,
getTimestampDaysAgo
-} from './util'
+} from '../util'
import {
XAccount,
XJob,
XProgress, emptyXProgress,
- XTweetItem,
XTweetItemArchive,
XArchiveStartResponse, emptyXArchiveStartResponse,
XRateLimitInfo, emptyXRateLimitInfo,
@@ -32,7 +30,7 @@ import {
XDeleteReviewStats, emptyXDeleteReviewStats,
XArchiveInfo, emptyXArchiveInfo,
XImportArchiveResponse
-} from './shared_types'
+} from '../shared_types'
import {
runMigrations,
getAccount,
@@ -41,9 +39,19 @@ import {
Sqlite3Count,
getConfig,
setConfig,
-} from './database'
-import { IMITMController, getMITMController } from './mitm';
+} from '../database'
+import { IMITMController } from '../mitm';
import {
+ XJobRow,
+ XTweetRow,
+ XUserRow,
+ XConversationRow,
+ XMessageRow,
+ XConversationParticipantRow,
+ convertXJobRowToXJob,
+ convertTweetRowToXTweetItem,
+ convertTweetRowToXTweetItemArchive,
+ // X API types
XAPILegacyUser,
XAPILegacyTweet,
XAPIData,
@@ -65,121 +73,8 @@ import {
isXArchiveLikeContainer,
isXAPIBookmarksData,
isXAPIData,
- // XArchiveDMConversation,
-} from './account_x_types'
-import * as XArchiveTypes from '../archive-static-sites/x-archive/src/types';
-
-function formatDateToYYYYMMDD(dateString: string): string {
- const date = new Date(dateString);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
-}
-
-export interface XJobRow {
- id: number;
- jobType: string;
- status: string;
- scheduledAt: string;
- startedAt: string | null;
- finishedAt: string | null;
- progressJSON: string | null;
- error: string | null;
-}
-
-export interface XTweetRow {
- id: number;
- username: string;
- tweetID: string;
- conversationID: string;
- createdAt: string;
- likeCount: number;
- quoteCount: number;
- replyCount: number;
- retweetCount: number;
- isLiked: boolean;
- isRetweeted: boolean;
- isBookmarked: boolean;
- text: string;
- path: string;
- addedToDatabaseAt: string;
- archivedAt: string | null;
- deletedTweetAt: string | null;
- deletedRetweetAt: string | null;
- deletedLikeAt: string | null;
- deletedBookmarkAt: string | null;
-}
-
-export interface XUserRow {
- id: number;
- userID: string;
- name: string | null;
- screenName: string;
- profileImageDataURI: string | null;
-}
-
-export interface XConversationRow {
- id: number;
- conversationID: string;
- type: string;
- sortTimestamp: string | null;
- minEntryID: string | null;
- maxEntryID: string | null;
- isTrusted: boolean | null;
- shouldIndexMessages: boolean | null;
- addedToDatabaseAt: string;
- updatedInDatabaseAt: string | null;
- deletedAt: string | null;
-}
-
-export interface XConversationParticipantRow {
- id: number;
- conversationID: string;
- userID: string;
-}
-
-export interface XMessageRow {
- id: number;
- messageID: string;
- conversationID: string;
- createdAt: string;
- senderID: string;
- text: string;
- deletedAt: string | null;
-}
-
-function convertXJobRowToXJob(row: XJobRow): XJob {
- return {
- id: row.id,
- jobType: row.jobType,
- status: row.status,
- scheduledAt: new Date(row.scheduledAt),
- startedAt: row.startedAt ? new Date(row.startedAt) : null,
- finishedAt: row.finishedAt ? new Date(row.finishedAt) : null,
- progressJSON: row.progressJSON ? JSON.parse(row.progressJSON) : null,
- error: row.error,
- };
-}
-
-function convertTweetRowToXTweetItem(row: XTweetRow): XTweetItem {
- return {
- id: row.tweetID,
- t: row.text,
- l: row.likeCount,
- r: row.retweetCount,
- d: row.createdAt,
- };
-}
-
-function convertTweetRowToXTweetItemArchive(row: XTweetRow): XTweetItemArchive {
- return {
- url: `https://x.com/${row.path}`,
- tweetID: row.tweetID,
- basename: `${formatDateToYYYYMMDD(row.createdAt)}_${row.tweetID}`,
- username: row.username
- };
-}
+} from './types'
+import * as XArchiveTypes from '../../archive-static-sites/x-archive/src/types';
export class XAccountController {
private accountUUID: string = "";
@@ -2248,412 +2143,3 @@ export class XAccountController {
return setConfig(key, value, this.db);
}
}
-
-const controllers: Record = {};
-
-const getXAccountController = (accountID: number): XAccountController => {
- if (!controllers[accountID]) {
- controllers[accountID] = new XAccountController(accountID, getMITMController(accountID));
- }
- controllers[accountID].refreshAccount();
- return controllers[accountID];
-}
-
-export const defineIPCX = () => {
- ipcMain.handle('X:resetProgress', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return controller.resetProgress();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:createJobs', async (_, accountID: number, jobTypes: string[]): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return controller.createJobs(jobTypes);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getLastFinishedJob', async (_, accountID: number, jobType: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return controller.getLastFinishedJob(jobType);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:updateJob', async (_, accountID: number, jobJSON: string) => {
- try {
- const controller = getXAccountController(accountID);
- const job = JSON.parse(jobJSON) as XJob;
- controller.updateJob(job);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexStart', async (_, accountID: number) => {
- try {
- const controller = getXAccountController(accountID);
- await controller.indexStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexStop', async (_, accountID: number) => {
- try {
- const controller = getXAccountController(accountID);
- await controller.indexStop();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexParseAllJSON', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexParseAllJSON();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexParseTweets', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexParseTweets();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexParseConversations', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexParseConversations();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexIsThereMore', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexIsThereMore();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:resetThereIsMore', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.resetThereIsMore();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexMessagesStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexMessagesStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexParseMessages', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.indexParseMessages();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:indexConversationFinished', async (_, accountID: number, conversationID: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.indexConversationFinished(conversationID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:archiveTweetsStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.archiveTweetsStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:archiveTweetsOutputPath', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.archiveTweetsOutputPath();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:archiveTweet', async (_, accountID: number, tweetID: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.archiveTweet(tweetID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:archiveTweetCheckDate', async (_, accountID: number, tweetID: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.archiveTweetCheckDate(tweetID);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:archiveBuild', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.archiveBuild();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:syncProgress', async (_, accountID: number, progressJSON: string) => {
- try {
- const controller = getXAccountController(accountID);
- await controller.syncProgress(progressJSON);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:openFolder', async (_, accountID: number, folderName: string) => {
- try {
- const controller = getXAccountController(accountID);
- await controller.openFolder(folderName);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getArchiveInfo', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getArchiveInfo();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:resetRateLimitInfo', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.resetRateLimitInfo();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:isRateLimited', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.isRateLimited();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getProgress', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getProgress();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getProgressInfo', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getProgressInfo();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getDatabaseStats', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getDatabaseStats();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getDeleteReviewStats', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getDeleteReviewStats();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:saveProfileImage', async (_, accountID: number, url: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.saveProfileImage(url);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getLatestResponseData', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getLatestResponseData();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteTweetsStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.deleteTweetsStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteTweetsCountNotArchived', async (_, accountID: number, total: boolean): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.deleteTweetsCountNotArchived(total);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteRetweetsStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.deleteRetweetsStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteLikesStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.deleteLikesStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteBookmarksStart', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.deleteBookmarksStart();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteTweet', async (_, accountID: number, tweetID: string, deleteType: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.deleteTweet(tweetID, deleteType);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteDMsMarkAllDeleted', async (_, accountID: number): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.deleteDMsMarkAllDeleted();
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:unzipXArchive', async (_, accountID: number, archivePath: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.unzipXArchive(archivePath);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- await controller.deleteUnzippedXArchive(archivePath);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:verifyXArchive', async (_, accountID: number, archivePath: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.verifyXArchive(archivePath);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:importXArchive', async (_, accountID: number, archivePath: string, dataType: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.importXArchive(archivePath, dataType);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getCookie', async (_, accountID: number, name: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getCookie(name);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:getConfig', async (_, accountID: number, key: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.getConfig(key);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-
- ipcMain.handle('X:setConfig', async (_, accountID: number, key: string, value: string): Promise => {
- try {
- const controller = getXAccountController(accountID);
- return await controller.setConfig(key, value);
- } catch (error) {
- throw new Error(packageExceptionForReport(error as Error));
- }
- });
-};
From 875164151a7ae85bdcb4338af12910489ad4e800 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 10:21:55 -0800
Subject: [PATCH 13/14] Fix import on test
---
src/account_x.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/account_x.test.ts b/src/account_x.test.ts
index b2e4e8c8..af7be693 100644
--- a/src/account_x.test.ts
+++ b/src/account_x.test.ts
@@ -9,7 +9,7 @@ import {
XAPILegacyTweet,
XAPIConversation,
XAPIUser
-} from './account_x_types';
+} from './account_x';
import {
XTweetRow,
XUserRow,
From 7a56fd5e6c271fc3e31d5663b7ff101877691343 Mon Sep 17 00:00:00 2001
From: Micah Lee
Date: Wed, 8 Jan 2025 10:28:49 -0800
Subject: [PATCH 14/14] Comment out Bluesky account selection for this PR
---
src/renderer/src/views/AccountView.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/renderer/src/views/AccountView.vue b/src/renderer/src/views/AccountView.vue
index 201ace4d..910f4f41 100644
--- a/src/renderer/src/views/AccountView.vue
+++ b/src/renderer/src/views/AccountView.vue
@@ -74,7 +74,7 @@ onMounted(async () => {
-
+